← Back to gallery
CSS

Responsive Dialog Side Sheet

Native <dialog> element + ::backdrop with viewport-driven layout that becomes a centered modal on desktop and an edge-anchored side sheet on mobile. Three motion programs: spring scale-in, slide-from-edge, and a soft fade.

dialogbackdropshowModalresponsiveside-sheetesc-closefocus-trap

Modal transition · <dialog> · transform slide

Responsive Dialog Side Sheet

Three side-sheet treatments that each tune the transform-driven slide for their own product context — a right-edge Settings Drawer (medium width, brisk slide), a left-edge Filter Sheet (narrower, sharper slide for catalogue pages), and a wider right-edge Cart Slide-In (slower, weightier entry for commerce). All three lean on transform: translateX so the slide stays GPU-composited, and the @starting-style block on the [open] state replays the entry animation through the native top-layer. Stage cards JS-cycle the open state every 2.4s; inspector preview is fully interactive with Tab captured to refocus the trigger.

Right entry · ~2/3 width commerce panel

Cart Detail

A right-entry sheet that takes ~2/3 of the frame so the page mock peeks behind on the left. Heavier sheet, slower 0.32s entry on cubic-bezier(0.2, 0.8, 0.2, 1) so the slide reads as commerce gravitas. Backdrop blur darkens the leftover page underneath.

  • cart
  • right entry
  • 2/3 coverage

Bottom entry · ~2/3 height action menu

Action Sheet

A bottom-entry sheet that takes ~2/3 of the frame so the page above stays partially visible — the canonical mobile action-menu pattern. translateY(100%) → 0 over 0.26s so the surface "rises" into view; rounded top corners + a small grab handle telegraph that this is a dismissible sheet.

  • action menu
  • bottom entry
  • 2/3 coverage

Left entry · full-frame navigation takeover

Navigation Drawer

A left-entry sheet that covers the entire frame because the navigation IS the destination, not an overlay. Sharper 0.22s slide so the takeover feels intentional — users open the menu, pick a route, and the sheet collapses back. Active route is highlighted with the accent.

  • navigation
  • left entry
  • full takeover

Side sheet inspector

Cart Detail

click trigger
  • cart
  • right entry
  • 2/3 coverage

A right-entry sheet that takes ~2/3 of the frame so the page mock peeks behind on the left. Heavier sheet, slower 0.32s entry on cubic-bezier(0.2, 0.8, 0.2, 1) so the slide reads as commerce gravitas. Backdrop blur darkens the leftover page underneath.

Helped you ship something? 🐟 Send my cat a churu

/* Right entry — slides in from the right edge and covers the full frame. */
.side-sheet {
  inset: 0;
  border: 0;
  background: var(--surface);
  padding: 24px;
  transform: translateX(100%);
  transition:
    transform 0.32s cubic-bezier(0.2, 0.8, 0.2, 1),
    overlay 0.32s allow-discrete,
    display 0.32s allow-discrete;
}

.side-sheet[open] {
  transform: translateX(0);
}

/* Replays the entry transition through the native top-layer. */
@starting-style {
  .side-sheet[open] {
    transform: translateX(100%);
  }
}

.side-sheet::backdrop {
  background: rgba(2, 6, 23, 0.6);
  backdrop-filter: blur(4px);
}

@media (prefers-reduced-motion: reduce) {
  .side-sheet { transition-duration: 0s; }
}

How to make this

A responsive dialog side sheet uses the native dialog top layer, transform-based entry, @starting-style for the open animation, and media queries to become a bottom sheet on small screens.

html
1<main class="sheet-page" inert>  <h1>Workspace</h1>  <p>The page remains behind the modal sheet.</p></main> 6<dialog class="side-sheet" open aria-labelledby="sheet-title">  <header class="side-sheet__header">    <p>Cart · 3 items</p>    <h2 id="sheet-title">Your cart</h2>  </header>  <ul class="side-sheet__list">    <li>Studio Lamp · $84</li>    <li>Linen Throw · $42</li>    <li>Walnut Tray · $28</li>  </ul>16  <form method="dialog">    <button>Close</button>  </form></dialog> <style>.sheet-page {  min-height: 100dvh;  padding: 2rem;  background: #020617;  color: #e2e8f0;}.side-sheet {  position: fixed;  inset: 0 0 0 auto;  width: min(26rem, 100vw);  height: 100dvh;  margin: 0;  border: 0;  padding: 1.25rem;  color: #e2e8f0;  background: #0f172a;  box-shadow: -24px 0 48px rgba(2, 6, 23, .42);39  transform: translateX(0);  opacity: 1;  transition: transform .28s cubic-bezier(.22,1,.36,1), opacity .28s ease;}.side-sheet::backdrop {  background: rgba(2, 6, 23, .58);  backdrop-filter: blur(2px);}47@starting-style {  .side-sheet[open] {    transform: translateX(100%);    opacity: 0;  }  .side-sheet[open]::backdrop { opacity: 0; }}.side-sheet__header {  border-bottom: 1px solid rgba(148, 163, 184, .18);  padding-bottom: .75rem;}.side-sheet__header p { color: #7dd3fc; margin: 0 0 .25rem; }.side-sheet__header h2 { margin: 0; }.side-sheet__list {  display: grid;  gap: .5rem;  padding: 1rem 0;}65@media (max-width: 640px) {  .side-sheet {    inset: auto 0 0;    width: 100vw;    height: min(70dvh, 32rem);    border-radius: 18px 18px 0 0;    transform: translateY(0);  }  @starting-style {    .side-sheet[open] { transform: translateY(100%); opacity: 0; }  }}@media (prefers-reduced-motion: reduce) {78  .side-sheet { transition: none; }}</style>

Annotated snippet

  1. Line 1The background page is inert while the modal sheet is open. In production, dialog.showModal() applies modal focus behavior; inert is shown here to make the relationship explicit in a static snippet.
    PitfallWhy use dialog for a responsive side sheet?

    A modal side sheet has the same constraints as a modal dialog: top-layer rendering, focus containment, Escape handling, backdrop behavior, and focus restoration. The native dialog element gives you the right platform primitive before custom animation is added.

  2. Line 6Use the native dialog element for modal sheets so the browser can place it in the top layer. That avoids z-index battles with headers, transforms, and local stacking contexts.
    PitfallWhy use dialog for a responsive side sheet?

    A modal side sheet has the same constraints as a modal dialog: top-layer rendering, focus containment, Escape handling, backdrop behavior, and focus restoration. The native dialog element gives you the right platform primitive before custom animation is added.

  3. Line 16method="dialog" gives the close button a native close path. Real apps can still listen for close events, restore focus, and update route or state outside the markup.
  4. Line 39The sheet moves with transform, not right or width. Transform keeps the panel composited and prevents the page behind it from reflowing during entry.

    Simulated: animating right or width on a side sheet can drop to ~5fps on slow devices, looking choppy like the left card. transform sits on the compositor and stays smooth.

    PitfallWhy animate transform instead of right or width for a side sheet?

    right and width are layout properties — every animation frame triggers layout for the whole page, then paint for the panel. On a low-end Android with content behind the sheet (long lists, complex headers), the slide can drop from 60fps to under 10fps. transform sits on the composite layer, so the slide runs on the GPU with zero layout cost. Same visual endpoint, far less main-thread work, no risk of dragging the underlying page into the entry animation.

  5. Line 47@starting-style supplies the pre-open frame for an element that appears directly in its open state. Without it, the browser has no starting transform to animate from when the dialog enters the top layer.

    A dialog rendered straight in its open state has no starting frame to transition from, so it snaps in. @starting-style declares the pre-open transform, giving the browser a starting point to slide from.

    PitfallWhy does my dialog open without an entry animation?

    When a dialog enters the top layer, the browser may only see the final open styles. Add @starting-style for the [open] state so the first frame has the off-screen transform and opacity needed for the transition.

  6. Line 65The responsive breakpoint changes the same dialog into a bottom sheet. Keep the modal semantics constant; only the placement and entry axis change.
    PitfallHow should side sheets handle mobile layouts?

    Keep the same dialog markup and switch placement with media queries. A right sheet often becomes a bottom sheet on small screens because vertical entry leaves more width for content and matches mobile action-sheet expectations.

  7. Line 78Reduced motion removes sheet travel. The dialog still opens in the top layer with the same content, backdrop, and close behavior.
    PitfallWhat should prefers-reduced-motion do for modal sheets?

    Remove the slide and fade transitions, but preserve focus behavior, backdrop, close controls, and content. Users who reduce motion should not receive a different modal architecture.

Other pitfalls

Is @starting-style supported everywhere?
@starting-style is supported in current Chromium and Safari, with modern Firefox support arriving later than basic dialog. Treat it as progressive enhancement: the sheet should still open instantly if the entry transition is unsupported.

Notes

Overview

The native <dialog> element with ::backdrop gives you focus trapping, Esc dismissal, and inert behavior for content outside the dialog for free — no third-party modal library required. This pattern wraps the native element with a viewport-driven responsive layout that becomes a centered modal on desktop and an edge-anchored side sheet on mobile.

When to use it

Reach for native dialog on any modal interaction — confirmations, settings panels, filter sidebars, login flows. Skip it for non-blocking notifications (use a toast) and for content where the user might need to compare with the underlying page (a non-modal popover is better). On mobile, skip the modal layout and reach for full-screen sheets when the form has more than three inputs.

How it works

Open the dialog with dialog.showModal() — this is the critical call (not show()) because it activates the top layer (which renders above all z-indices), the ::backdrop pseudo-element, the inert tree outside the dialog, focus trapping, and Esc dismissal. Style the open dialog with a media query: on wide viewports center it with margin: auto and a fixed width; on narrow viewports anchor it to the bottom with position: fixed; inset: auto 0 0 0 and a slide-up transform. Animate both the dialog and the ::backdrop with a paired @keyframes rule on open attribute presence.

Production gotchas

The dialog needs an aria-labelledby pointing to its heading, or screen readers announce “dialog” with no further context. Focus does not auto-move to the dialog on open in older Safari; call firstFocusable.focus() after showModal() as a belt-and-suspenders fix. Closing the dialog must return focus to the trigger element — store a reference on open and call trigger.focus() after close(). Animating dialog display from none to block requires @starting-style + the transition-behavior: allow-discrete property in modern browsers; without it the enter animation skips.

Accessibility

Native dialog gets focus trap and inert-outside for free, but you still need to wire the trigger-return path yourself. Mobile users dismissing the side sheet with the backdrop tap should also have an explicit close button focusable from the keyboard. Under prefers-reduced-motion: reduce drop the slide-up transform and use a simple opacity fade, or skip the enter animation entirely. The ::backdrop color should pass 3:1 against the page so the modal feels properly isolated.

References

Implementation depth

The responsive side sheet should stay a dialog first. showModal, backdrop, Escape behavior, and focus containment define the interaction; the sheet animation only changes how the dialog enters at different widths.

Mobile layout changes need the same dismissal and focus rules as desktop. Test the small viewport sheet, large viewport modal, and reduced-motion open state so the component does not fork into two behaviors.