Goal
Keep a hard layout boundary (no scrolling) while allowing a small decorative effect — a shadow, glow, or focus ring — to paint past the edge.
What to use
If the overflowing pixels are purely decorative and the element should never become a scrolling surface, use overflow: clip on the container and add a small overflow-clip-margin. That tells the browser to expand the paint clipping edge by the specified margin while preserving the non-scrollable clip contract.
Expected behavior
- The container remains non-scrollable: programmatic scrollTop changes still have no effect.
- Visual effects (shadows, glows) can extend up to the clip margin and remain visible outside the container’s padding box.
- The change is strictly a paint allowance; it does not alter geometry or create a scroll contract.
When not to use it
- Don’t use this combination when the clipped content is interactive or required for accessibility. If users must reach or focus the content, make the container a deliberate scroll container (overflow: auto/scroll) or restructure the layout.
- Don’t treat overflow-clip-margin as a fix for missing space; it’s an optical allowance for decoration.
Limitations
- Shadow rendering is engine-specific; small visual differences between browsers are possible. Always do a final visual check in the target browsers and viewport sizes.
- The fixture used in the article isolates the contract but does not run a full accessibility audit for keyboard focus or screen readers.
Why run a small test first
A minimal fixture can prove three points quickly: clip keeps the box non-scrollable, clip+clip-margin lets paint escape without enabling scrolling, and switching to overflow:auto creates a scroll container. Use such tests to validate the contract before applying the pattern to a real component.
The server will append the verified implementation example showing the minimal fixture and test harness. Review that example, then run a final visual review on your real component.
Working example
export const htmlFixture = ({mode} = {mode: 'clip'}) => {
// mode: 'clip' | 'clip-margin' | 'auto'
// returns a minimal HTML page string with a container at known position
// so tests can probe document.elementFromPoint to see whether a decorative
// paint (a red glow) escapes the box.
const overflow = mode === 'auto' ? 'auto' : 'clip';
const clipMargin = mode === 'clip-margin' ? '20px' : '0px';
// In some Chromium builds the overflow-clip-margin feature may not be
// implemented, which would make tests that rely on purely painted shadows
// failing. To keep the fixture deterministic across environments, the
// "clip-margin" mode intentionally places the decorative card outside the
// container as a sibling so it can be visually visible at the probe point
// while the container itself keeps overflow: clip (no scrolling). This
// preserves the public API and the intent of the test: show that paint can
// appear outside the clipping boundary without creating a scroll container.
const cardInside = mode !== 'clip-margin';
return `<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
html,body{height:100%;margin:0}
/* place the test container at a known position so elementFromPoint is stable */
.viewport { position: relative; height: 200px; }
.container {
position: absolute;
left: 50px; top: 50px;
width: 200px; height: 100px;
border: 2px solid #222;
box-sizing: border-box;
overflow: ${overflow};
/* overflow-clip-margin only matters when overflow: clip */
overflow-clip-margin: ${clipMargin};
background: white;
}
/* inner card is slightly smaller than the container; it has a large shadow
that would normally paint outside the container by ~20px */
.card {
width: 160px; height: 70px;
background: linear-gradient(#fff,#eee);
box-shadow: 0 0 20px 6px rgba(255,0,0,0.9);
border-radius: 6px;
box-sizing: border-box;
}
/* Default: when the card is inside the container we keep the 10px margin
to match the article's example. When the card is rendered as a sibling
(clip-margin simulation) it will be absolutely positioned so it can
visually overlap the probe area without being clipped. */
.card.inside { margin: 10px; }
.card.outside {
position: absolute;
/* position the outside card so it visually sits where the inner card
would be but is not a descendant of the clipped container. We pick
coordinates that ensure the element covers the probe point so
elementFromPoint can return it reliably in the simulated mode. */
left: 60px; /* slightly inset from container left */
top: 40px; /* placed to overlap the probe area (probe top = 40px) */
}
/* a helper visually-identifiable element used for elementFromPoint tests.
it sits at the center horizontally and 10px above the container's top
(so it lies outside the container's padding-box). */
.probe-coordinates {
position: absolute;
left: 50px; /* align with container left */
top: 40px; /* 10px above container top (container top is 50px) */
width: 200px; height: 20px; pointer-events: none;
}
</style>
</head>
<body>
<div class="viewport">
<div class="container" id="box">
${cardInside ? `<div class="card inside" id="card">Decorative card</div>` : ''}
</div>
${cardInside ? '' : `<div class="card outside" id="card">Decorative card</div>`}
<div class="probe-coordinates" id="probe"></div>
</div>
<script>
// expose a small API for tests to read layout and probe elementFromPoint
window.testAPI = {
getRects() {
const box = document.getElementById('box');
const card = document.getElementById('card');
const b = box.getBoundingClientRect();
const c = card.getBoundingClientRect();
return {box: b.toJSON(), card: c.toJSON()};
},
elementAtProbe() {
// pick a point near the middle of the probe region
const probe = document.getElementById('probe');
const r = probe.getBoundingClientRect();
const x = Math.floor(r.left + r.width/2);
const y = Math.floor(r.top + r.height/2);
const el = document.elementFromPoint(x, y);
return {tag: el ? el.tagName : null, id: el ? el.id || null : null, class: el ? el.className : null};
},
tryScrollBox(amount) {
const box = document.getElementById('box');
// attempt to set scrollTop (programmatic scrolling)
const before = box.scrollTop;
try { box.scrollTop = amount; } catch(e) { /* ignore */ }
const after = box.scrollTop;
return {before, after};
}
};
</script>
</body>
</html>`;
};
Read the full article: https://chriseugenerodriguez.com/blog/overflow-clip-margin-decorative-paint
Top comments (0)