Mini-tutorial: Preserve decorative paint with overflow: clip + overflow-clip-margin

Mini-tutorial: Preserve decorative paint with overflow: clip + overflow-clip-margin

calendar_today ago • schedule3 min read
— Originally published at chriseugenerodriguez.com

Prerequisites

  • Basic familiarity with CSS overflow values and the distinction between layout vs paint.
  • A component that must not become scrollable (cards, badges, decorative wrappers).

Implementation decision

Keep overflow: clip on the wrapper to assert a hard, non-scrollable boundary. If a small decorative paint (shadow, glow, focus ring) must remain visible outside the box, add overflow-clip-margin with the pixel value that matches the effect’s spread. This expands the paint clipping edge but does not create scrolling behavior.

Expected result

  • Decorations such as a 20px glow will be visible outside the container up to the clip margin.
  • Programmatic scrolling (setting scrollTop) remains ineffective; the element stays non-scrollable.
  • The layout stays stable: no scrollbars or scroll anchoring are introduced by the clip margin.

One limitation

Only use this for purely decorative paint. If the overflow contains interactive controls or content that users must access, make the element a true scroll container (overflow: auto/scroll) or revise the layout. overflow-clip-margin is an optical allowance, not an accessibility fix.

The server will insert a verified implementation snippet for the minimal fixture so you can try the pattern and visually validate it in your target browsers.

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

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

Tailwind CSS for Beginners: From Zero to Your First Component

muhammadfarhan.dev - Jul 21

Layout Grids in CSS: Designing with Millimeter Precision Before Writing Code

Joemetry - Sep 22

The Rowling-Galbraith Mystery: Testing Stylometry with Burrows’ Delta

nevmenandr - Oct 10

Common CSS Mistakes Beginners Make (and How to Fix Them)

muhammadfarhan.dev - Aug 25

Branding via Code: The Amber Terminal Email

Pocket Portfolio - Apr 29
chevron_left
575 Points • 24 Badges
Santa Monica, CA • chriseugenerodriguez.com
20Posts
0Comments
4Connections
10 years fe

Related Jobs

View all jobs →

Commenters (This Week)

4 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!