Accessibility pattern · Overlays

Bottom sheet

A bottom sheet is a modal dialog pinned to the bottom edge, in reach of a thumb. Every gesture has a button: the drag handle is decoration, Expand changes the height, and the close button and Escape dismiss it.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Photo essay · 6 min read

Monsoon on the Kerala backwaters

Share

Monsoon on the Kerala backwaters

Photo essay by Meera Nair

More options

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Enter or SpaceOn Share, opens the sheet with focus on Copy link.
Tab or ShiftTabMoves through the sheet's buttons only; the page behind cannot be reached.
Enter or SpaceOn Expand, raises the sheet to full height; the button becomes Collapse, which lowers it again.
Enter or SpaceOn an option, carries it out, closes the sheet and reports what happened.
EscapeCloses the sheet without sharing and returns focus to Share.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
The sheet opensShare, dialog. Copy link, button
Focus reaches ExpandExpand, button, collapsed
Expand is pressedCollapse, button, expanded
An option is chosenLink copied (focus is back on Share)

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-bottom-sheet" data-ap-bottom-sheet>
  <article class="ap-bottom-sheet__story">
    <svg class="ap-bottom-sheet__art" viewBox="0 0 320 132" preserveAspectRatio="xMidYMid slice" aria-hidden="true" focusable="false">
      <rect class="ap-bottom-sheet__sky" width="320" height="132"/>
      <path class="ap-bottom-sheet__cloud" d="M188 44a15 15 0 0 1 28-9 13 13 0 0 1 24 7 11 11 0 0 1-1 22h-49a10 10 0 0 1-2-20Z"/>
      <path class="ap-bottom-sheet__cloud" d="M84 30a10 10 0 0 1 19-6 9 9 0 0 1 16 5 7.5 7.5 0 0 1-1 15H86a7 7 0 0 1-2-14Z"/>
      <path class="ap-bottom-sheet__rain" d="M196 70l-4 9M210 72l-4 9M224 70l-4 9M238 72l-4 9M92 50l-4 9M104 52l-4 9M116 50l-4 9M30 18l-4 9M150 14l-4 9M282 24l-4 9M300 54l-4 9M160 62l-4 9"/>
      <path class="ap-bottom-sheet__bank" d="M0 90c34-9 66-11 100-6s68 5 108-3 82-8 112 1v18H0Z"/>
      <rect class="ap-bottom-sheet__water" y="96" width="320" height="36"/>
      <path class="ap-bottom-sheet__ripple" d="M108 120h28M150 124h44M248 112h40M262 124h26M20 118h34"/>
      <path class="ap-bottom-sheet__near" d="M0 100c20-7 46-9 78-4v8H0Z"/>
      <path class="ap-bottom-sheet__palm" d="M40 99c3-20 3-35-3-50M37 49c-9-6-20-6-28 1M37 49c3-9 12-15 24-13M37 49c-2-10-9-16-18-18M37 49c9-2 19 3 23 12M62 99c1-12 0-21-4-30M58 69c-6-4-13-4-18 1M58 69c2-6 8-10 15-9M58 69c6-1 12 2 14 8"/>
      <path class="ap-bottom-sheet__roof" d="M164 106c2-11 11-17 25-17h18c14 0 23 6 25 17Z"/>
      <path class="ap-bottom-sheet__hull" d="M150 105h96c-3 7-11 10-22 10h-52c-11 0-19-3-22-10Z"/>
    </svg>
    <div class="ap-bottom-sheet__copy">
      <p class="ap-bottom-sheet__kicker">Photo essay · 6 min read</p>
      <h3 class="ap-bottom-sheet__story-name">Monsoon on the Kerala backwaters</h3>
      <div class="ap-bottom-sheet__byline">
        <p class="ap-bottom-sheet__by">By Meera Nair</p>
        <button type="button" class="ap-btn" data-ap-open aria-haspopup="dialog">
          <svg class="ap-btn__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="18" cy="5.5" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="18.5" r="2.5"/><path d="m8.2 13.2 7.6 4.1M15.8 6.7l-7.6 4.1"/></svg>
          Share
        </button>
      </div>
    </div>
  </article>
  <p class="ap-bottom-sheet__status" role="status" data-ap-status></p>

  <dialog class="ap-bottom-sheet__sheet" id="bottom-sheet-dialog" aria-labelledby="bottom-sheet-name" data-snap="half">
    <div class="ap-bottom-sheet__grip" data-ap-grip>
      <span class="ap-bottom-sheet__handle" aria-hidden="true"></span>
    </div>
    <div class="ap-bottom-sheet__top" data-ap-grip>
      <h2 class="ap-bottom-sheet__name" id="bottom-sheet-name">Share</h2>
      <button type="button" class="ap-bottom-sheet__expand" data-ap-expand aria-expanded="false" aria-controls="bottom-sheet-dialog">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 15 6-6 6 6"/></svg>
        <span data-ap-expand-label>Expand</span>
      </button>
      <button type="button" class="ap-bottom-sheet__close" data-ap-close aria-label="Close">
        <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 6l12 12M18 6 6 18"/></svg>
      </button>
    </div>
    <div class="ap-bottom-sheet__scroll">
      <div class="ap-bottom-sheet__item">
        <svg class="ap-bottom-sheet__mini" viewBox="0 0 48 48" aria-hidden="true" focusable="false">
          <rect class="ap-bottom-sheet__sky" width="48" height="48"/>
          <path class="ap-bottom-sheet__cloud" d="M14 17a5 5 0 0 1 9.5-2 4.5 4.5 0 0 1 8 2.5 4 4 0 0 1-.5 8H15a4 4 0 0 1-1-8.5Z"/>
          <path class="ap-bottom-sheet__bank" d="M0 31c8-3 16-3 24-1s16 1 24-1v6H0Z"/>
          <rect class="ap-bottom-sheet__water" y="34" width="48" height="14"/>
          <path class="ap-bottom-sheet__roof" d="M17 37c.6-3.5 3.5-5.5 8-5.5h3c4.5 0 7.4 2 8 5.5Z"/>
          <path class="ap-bottom-sheet__hull" d="M13 37h28c-1 2.5-3.5 3.5-7 3.5H20c-3.5 0-6-1-7-3.5Z"/>
        </svg>
        <div>
          <p class="ap-bottom-sheet__item-name">Monsoon on the Kerala backwaters</p>
          <p class="ap-bottom-sheet__item-meta">Photo essay by Meera Nair</p>
        </div>
      </div>
      <ul class="ap-bottom-sheet__grid">
          <li><button type="button" class="ap-bottom-sheet__tile" data-ap-action="link" autofocus><span class="ap-bottom-sheet__disc"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg></span><span>Copy link</span></button></li>
          <li><button type="button" class="ap-bottom-sheet__tile" data-ap-action="email"><span class="ap-bottom-sheet__disc"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3.5 6.5 8.5 6.5 8.5-6.5"/></svg></span><span>Email</span></button></li>
          <li><button type="button" class="ap-bottom-sheet__tile" data-ap-action="message"><span class="ap-bottom-sheet__disc"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 5h16v11H9.5L4 20Z"/><path d="M8 9.5h8M8 12.5h5"/></svg></span><span>Message</span></button></li>
          <li><button type="button" class="ap-bottom-sheet__tile" data-ap-action="qr"><span class="ap-bottom-sheet__disc"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="3.5" width="6" height="6" rx="1"/><rect x="14.5" y="3.5" width="6" height="6" rx="1"/><rect x="3.5" y="14.5" width="6" height="6" rx="1"/><path d="M14.5 14.5h2v2h-2ZM18.5 18.5h2v2h-2ZM14.5 20.5h2M20.5 14.5v2"/></svg></span><span>QR code</span></button></li>
      </ul>
      <h3 class="ap-bottom-sheet__group" id="bottom-sheet-more">More options</h3>
      <ul class="ap-bottom-sheet__list" aria-labelledby="bottom-sheet-more">
          <li><button type="button" class="ap-bottom-sheet__row" data-ap-action="later"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.5h11v17l-5.5-4-5.5 4Z"/></svg><span>Save for later</span></button></li>
          <li><button type="button" class="ap-bottom-sheet__row" data-ap-action="pdf"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 4v11M7.5 10.5 12 15l4.5-4.5"/><path d="M5 20h14"/></svg><span>Download as PDF</span></button></li>
          <li><button type="button" class="ap-bottom-sheet__row" data-ap-action="collection"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3.5 7a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v8.5a2 2 0 0 1-2 2h-13a2 2 0 0 1-2-2Z"/><path d="M12 10.5v5M9.5 13h5"/></svg><span>Add to a collection</span></button></li>
          <li><button type="button" class="ap-bottom-sheet__row" data-ap-action="report"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5.5 21V4"/><path d="M5.5 4h11l-2 4 2 4h-11"/></svg><span>Report this story</span></button></li>
      </ul>
    </div>
  </dialog>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 2.1.1 Keyboard Level A

    Expand, Close and every share option are buttons, and Escape closes the sheet; nothing needs a swipe.

  • 2.4.3 Focus Order Level A

    Focus moves to the first option when the sheet opens and back to Share when it closes.

  • 2.5.1 Pointer Gestures Level A

    Swiping down to dismiss is an extra: the close button and Escape do the same with a single tap or key.

  • 2.5.7 Dragging Movements Level AA

    Dragging the handle up or down has single-pointer alternatives in Expand, Collapse and Close.

  • 2.5.8 Target Size (Minimum) Level AA

    Every option, Expand and Close are at least 44 pixels tall, well above the 24-pixel minimum.

  • 4.1.2 Name, Role, Value Level A

    The sheet is a named modal dialog, and Expand reports its state with aria-expanded as its label changes.

  • 4.1.3 Status Messages Level AA

    The result of an option is written to a status line that is announced after the sheet closes.

Usage

When to use it

Use it

  • A short list of actions on a phone, like sharing, sorting or picking an option for one item.
  • A quick task that should keep the page in view behind it.

Use something else

  • Wide screens, where a sheet stretched along the bottom is far from the pointer: a popover or a centred dialog fits better.
  • Long forms or content people read for a while: give it a page.
  • Navigation between sections of a site: use the mobile navigation pattern.

Common failures

How it usually goes wrong

  • Swipe as the only way out

    Anyone using a keyboard, a switch, voice control or a screen reader cannot swipe a sheet away. The close button and Escape always work.

  • A handle that pretends to be a control

    A focusable "Drag handle" that does nothing from the keyboard is a dead stop in the Tab order. Here the handle is hidden from assistive technology and Expand does its job.

  • Icons with no words

    A row of logos and glyphs leaves people guessing, and screen readers read nothing or a file name. Every option has a visible text label.

  • A sheet that is not modal

    Dimming the page without making it inert lets Tab and a screen reader slip behind the sheet. showModal() makes the rest of the page unreachable.

  • The page scrolls instead of the sheet

    Without touch-action on the handle, a drag scrolls or refreshes the page behind. The handle area claims the gesture, and the page is locked.

Notes

Building it

  • It is a modal dialog opened with showModal(); CSS pins it to the bottom edge and slides it up, and the browser handles focus, Escape and the inert page.
  • Expand changes its label and aria-expanded together, and aria-controls points at the sheet, so the text and the state agree.
  • The drag zone uses pointer events with setPointerCapture and touch-action: none; buttons inside it are left alone, and the list below keeps its normal scrolling.
  • When the system asks for reduced motion the sheet appears without sliding; a drag still follows the finger, because the person is moving it.

Sources: WAI-ARIA Authoring Practices: Dialog (Modal) · Understanding WCAG 2.2: Dragging Movements

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction