DEV Community

Cover image for When to use overflow: clip with overflow-clip-margin (and when not to)
Chris Rodriguez
Chris Rodriguez

Posted on Originally published at chriseugenerodriguez.com

When to use overflow: clip with overflow-clip-margin (and when not to)

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>`;
};
Enter fullscreen mode Exit fullscreen mode

Read the full article: https://chriseugenerodriguez.com/blog/overflow-clip-margin-decorative-paint

Top comments (0)