Skip to content

Backdrop

The scrim beneath every overlay. Opacity, blur, click-through, scroll locking, and what happens when two overlays collide.

Also called Scrim, Overlay, Dim Layer — in this system all of them are Backdrop.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Playground

api-gateway

Deployment 4021 finished in 42 seconds across three regions.

Roll back?

This returns api-gateway to build 4019.

The page behind should read as paused, not gone. Try 0.42 and 0.72.

The opacity is theme-dependent

A white page needs far less dimming to read as inactive. One value for both themes leaves one of them wrong.

Dark · 72%
api-gatewayDeployment 4021 · 42sRoll back?
Light · 42%
api-gatewayDeployment 4021 · 42sRoll back?

Paused, not gone

At 90% the context that told the user what they were confirming is unreadable. At 20% the overlay does not read as the subject at all.

20%Overlay is not the subject
api-gatewayDeployment 4021 · 42sRoll back?
72%Right
api-gatewayDeployment 4021 · 42sRoll back?
92%Context destroyed
api-gatewayDeployment 4021 · 42sRoll back?

Blur is separate from opacity

A small blur lets you use less dimming for the same sense of separation — the page reads as out of focus rather than darkened.

No blur · 72%
api-gatewayDeployment 4021 · 42sRoll back?
2px blur · 60%
api-gatewayDeployment 4021 · 42sRoll back?

When two overlays stack

A dialog opened from a drawer gets its own scrim, and the result is double-dimmed. Either reuse one backdrop or accept that the second overlay is the only readable thing on screen.

Page content
Drawer
Dialog — everything else is now 92% dark

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

api-gatewayDeployment 4021 · 42sRoll back?
Dark 72%
api-gatewayDeployment 4021 · 42sRoll back?
Light 42%
api-gatewayDeployment 4021 · 42sRoll back?
With blur
api-gatewayDeployment 4021 · 42sRoll back?
Too light
api-gatewayDeployment 4021 · 42sRoll back?
Too dark
api-gatewayDeployment 4021 · 42sRoll back?
None
Entering
Over an overlay

Anatomy

Every part, every measurement, and the reason it is that number.

api-gateway

Deployment 4021 finished in 42 seconds across three regions.

Dialog

A full-viewport layer beneath the overlay, dimming and blurring the page and absorbing the dismissing click.

  1. Coverageposition: fixed; inset: 0

    The whole viewport, including under a fixed header. A scrim that stops at the header leaves an interactive strip the user can still reach.

  2. Opacity72% dark, 42% light

    Theme-dependent, not a constant. A white page reads as inactive at far less dimming than a dark one.

  3. Blur2px, optional

    Separate from opacity. A little blur buys the same separation with less dimming, so the context stays readable.

  4. Z-indexOne below its overlay

    The pair moves together. A scrim and an overlay in different stacking contexts is how you get a dialog behind its own backdrop.

  5. EntranceFade, 180ms

    Slightly slower than the overlay’s own entrance, so the dimming reads as settling behind rather than arriving with it.

  6. Click targetThe whole layer

    Dismisses by default. Suppress it only where dismissal would lose work — and then the user needs another obvious way out.

  7. Inert backgroundNot visual at all

    The scrim dims; inert is what actually removes the page from the tab order. Without it the modal is one Tab press from being escaped invisibly.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed for
--ds-layer-scrim—The scrim fill — theme-aware by definition
--ds-surface-overlay—The surface that sits on top of it

Shadow

TokenValueUsed for
--shadow-e5—The overlay above the scrim

Motion

TokenValueUsed for
--duration-normalFade in and out
--ease-standard—Fade curve

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeHeightMin widthWhen to use
Coverage100dvh100vwFixed to the viewport, dvh so a mobile URL bar collapsing does not reveal a strip.
Dark opacity72%—Enough that a dark page reads as paused rather than merely tinted.
Light opacity42%—A white page needs far less. The same value as dark looks like a rendering fault.
Blur2px—Optional. Buys separation without extra dimming, at a real compositing cost.
Fade180ms—Slightly slower than the overlay it sits behind.

Do

appRoot.inert = true
// not just opacity
Make the page behind inertThe scrim is visual only. Without inert or aria-hidden, a keyboard user tabs straight into the dimmed page and cannot tell where they are.
const w = innerWidth − documentElement.clientWidth
body.style.paddingRight = `${w}px`
Compensate for the scrollbar when locking scrollSetting overflow: hidden removes the scrollbar and the page jumps sideways by its width. Pad the body by exactly that amount.
api-gatewayDeployment 4021 · 42sRoll back?api-gatewayDeployment 4021 · 42sRoll back?
Use different opacity per themeA white page reads as inactive at 42%; a dark one needs 72%. One constant leaves whichever theme you did not test looking wrong.
<Portal>
  <Scrim /> <Dialog />
</Portal>
Keep the scrim and its overlay in one stacking contextSplit across two, and a z-index anywhere else in the app can slide between them — producing a dialog rendered behind its own backdrop.

Don't

api-gatewayDeployment 4021 · 42sRoll back?
Do not dim past about 80%The page behind is the context that told the user what they are confirming. Erase it and the dialog is a question with no subject.
Dialog
Do not stack two scrimsA dialog opened from a drawer double-dims the page to about 92%. Reuse one backdrop, or accept that the lower overlay is now unreadable.
still clickable
Do not stop short of the viewport edgesA scrim that misses a fixed header leaves an interactive strip above a supposedly modal surface, and users find it immediately.
transition: backdrop-filter 300ms
Do not animate the blurbackdrop-filter is expensive to composite, and animating it drops frames on exactly the mid-range devices where the dialog needs to feel instant.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.4.11Non-text ContrastAA2.1.2No Keyboard TrapA2.4.3Focus OrderA4.1.2Name, Role, ValueA

Contrast

  • The scrim itself has no contrast requirement — it is decoration. What matters is that the overlay above it still reaches its own ratios, which a very dark scrim can quietly help with and a very light one cannot.
  • Text remaining visible through the scrim is not required to meet contrast, because it is explicitly unavailable. It must not look available either.
  • In forced-colors mode the scrim is typically removed entirely, so the overlay must be distinguishable by its border alone.

Keyboard

EscDismisses, matching an outside click. If clicking away closes it, Escape must too.
TabCycles within the overlay only. The scrim is never focusable and the background is inert.
ClickOn the scrim, dismisses. Suppress only where work would be lost, and then provide an obvious alternative.

Screen readers

  • A scrim should be entirely silent. If a screen reader announces anything about it, it has a role or a label it should not have.
  • The background must be genuinely removed, not merely dimmed. A user who can still arrow into the page behind has no way to know the dialog is open.
  • Restore the background when the overlay closes, including on an error path. A page left permanently inert is unusable and looks like a crash.

Focus & touch

  • The scrim is never focusable. Focus moves into the overlay on open and returns to the trigger on close. The background being inert is what makes the focus trap reliable — a trap implemented purely in JavaScript will eventually be escaped by a browser-specific tab order.
  • Use 100dvh rather than 100vh, or a collapsing mobile URL bar reveals an undimmed strip at the bottom. Scroll locking on iOS needs position: fixed with the scroll offset restored on close — overflow: hidden alone does not hold. Backdrop blur is expensive on mid-range phones; measure before shipping it as a default.
AttributeApplied toNotes
aria-hidden="true"The scrim elementIt is pure decoration and must never be announced.
inertThe application rootThe real mechanism. Removes the background from the tab order and the accessibility tree in one attribute.
aria-hidden="true"The application rootThe fallback where inert is unsupported. It hides from screen readers but does not remove tab stops — you still need a focus trap.
aria-modal="true"The overlayOn the dialog, not the scrim. It tells assistive tech the background is unavailable.

Code

Example usage

tsx
1import { Backdrop } from '@/ui/Overlay'23// The scrim and its overlay live in ONE portal, so nothing in the app can4// slide a z-index between them.5<Portal>6  <Backdrop open={open} onClose={close} />7  <Dialog open={open} onClose={close} />8</Portal>910// The scrim is visual. This is what actually makes the page unavailable.11React.useEffect(() => {12  if (!open) return13  const root = document.getElementById('app')!14  root.inert = true15  return () => { root.inert = false }   // including on an error path16}, [open])1718// Scroll lock without the sideways jump. Removing the scrollbar shifts the19// page by its width — pad the body by exactly that amount.20function useScrollLock(locked: boolean) {21  React.useEffect(() => {22    if (!locked) return23    const width = window.innerWidth - document.documentElement.clientWidth24    const prev = {25      overflow: document.body.style.overflow,26      padding: document.body.style.paddingRight,27    }28    document.body.style.overflow = 'hidden'29    document.body.style.paddingRight = `${width}px`30    return () => {31      document.body.style.overflow = prev.overflow32      document.body.style.paddingRight = prev.padding33    }34  }, [locked])35}3637// Two overlays: reuse one scrim rather than stacking two to 92%.38const scrimVisible = dialogOpen || drawerOpen

Framework-free HTML

html
<!-- The page. inert is the mechanism; the scrim is the appearance. -->
<div id="app" inert>
  <h1>api-gateway</h1>
  <button type="button">Deploy</button>
</div>

<!-- Both in one stacking context. -->
<div class="ds-overlay-root">
  <!-- Pure decoration: never announced, never focusable. -->
  <div class="ds-backdrop" aria-hidden="true"></div>

  <div role="dialog" aria-modal="true" aria-labelledby="t">
    <h2 id="t">Roll back?</h2>
  </div>
</div>

CSS

css
.ds-backdrop {
  position: fixed;
  /* dvh, not vh: a collapsing mobile URL bar otherwise reveals an
     undimmed strip at the bottom. */
  inset: 0;
  block-size: 100dvh;
  z-index: 95;                       /* exactly one below its overlay */
  background: var(--ds-layer-scrim);
  animation: fade-in 180ms var(--ease-standard) both;
}

/* Theme-dependent, not a constant. A white page reads as inactive at far
   less dimming than a dark one. */
[data-theme='dark']  { --ds-layer-scrim: rgb(0 0 0 / 0.72); }
[data-theme='light'] { --ds-layer-scrim: rgb(0 0 0 / 0.42); }

/* Buys separation without extra dimming — at a real compositing cost.
   Never animate it. */
.ds-backdrop--blur { backdrop-filter: blur(2px); }

/* The scrim and the overlay must share one stacking context, or a z-index
   elsewhere in the app can slide between them. */
.ds-overlay-root { position: fixed; inset: 0; z-index: 95; }

@media (prefers-reduced-motion: reduce) {
  .ds-backdrop { animation: none; }
}

/* Scrims are typically removed here, so the overlay must stand on its
   border alone. */
@media (forced-colors: active) {
  .ds-backdrop { background: transparent; }
  [role='dialog'] { border: 1px solid; }
}

Component API

Backdrop

PropTypeDefaultDescription
open*boolean—Controlled. Mount only while open — a permanently mounted scrim at opacity 0 still composites.
onClose() => void—Called on click. Omit only where dismissal would lose work, and then provide an obvious alternative.
opacitynumbertheme tokenOverrides the theme value. Rarely correct — the token already accounts for light and dark.
blurnumber0Pixels of backdrop blur. Expensive on mid-range devices; measure before defaulting it on.
lockScrollbooleantrueLocks body scroll and compensates for the scrollbar width so the page does not jump.

Notes

Professional tips

  • Reuse one backdrop instance across every overlay in the app. Two stacked scrims is the most common way this component goes wrong.
  • Fade the scrim slightly slower than the overlay it sits behind, so the dimming reads as settling rather than arriving.
  • If clicking the scrim would lose unsaved work, do not silently suppress it — confirm instead. A dialog that ignores an outside click with no feedback reads as frozen.
  • Match Escape to outside-click behaviour exactly. If one dismisses, both must.
  • Test with the page scrolled halfway down. Scroll-lock bugs are invisible at the top of the page and obvious anywhere else.

Performance

  • backdrop-filter forces a new compositing layer over everything beneath it. On a complex page and a mid-range phone this is measurable, and it is why blur is opt-in here.
  • Animate opacity only. Animating the blur radius re-composites the whole layer on every frame.
  • Unmount the scrim when closed. A permanently mounted element at opacity 0 still participates in compositing.
  • On iOS, overflow: hidden on the body does not reliably lock scroll — position: fixed with the offset restored on close is the version that works.

Common mistakes

  • Relying on the scrim for accessibility, leaving the background reachable by Tab.
  • Scroll lock with no scrollbar compensation, so the page jumps sideways on open.
  • One opacity for both themes, leaving the light theme looking untouched.
  • Two stacked scrims when a dialog opens from a drawer.
  • 100vh instead of 100dvh, leaving an undimmed strip on mobile.
  • The scrim and overlay in different stacking contexts, producing a dialog behind its own backdrop.
  • Animating backdrop-filter, dropping frames on the devices that can least afford it.
  • Leaving the background inert after an error closes the overlay.

Real-world recommendations

  • Users click the scrim to dismiss constantly — it is the most-used dismissal path in most products, ahead of both Escape and the close button. Suppressing it needs a real reason.
  • The scroll-position jump on open is the bug users notice most and report least, because it is hard to describe. Fix it once in the shared component.
  • Blur looks excellent in a design review on a fast laptop and costs real frames on a three-year-old Android. Measure on the hardware your users actually have.
  • If a product has several overlay types, one shared backdrop with a reference count is worth building early. Retrofitting it after four components have their own is far harder.