Overlay

An overlay is anything that stands over the page. Design system 1 draws them all with one component, ds-overlay, and a type that says which it is:

typeWhat it isWhere it stands
modal (the default)A question or a task that holds the pageIn the centre, 20 squares wide
sheetA phone’s first choice for the sameFrom the foot of the screen, up to 32 squares wide
drawerA panel beside a list, a detail or settingsAt the side end (the default) or start, the screen’s height, 16 squares wide
popoverA small panel at a pointAt anchor, kept inside the visible screen
screenFolded navigation, a document viewerOver the whole screen

ds-overlay owns the behaviour and the look around a light-DOM <dialog>, so labels, descriptions and form controls stay in one tree. A modal, a sheet, a drawer and a screen open with showModal(): the browser supplies the top layer and the inert page behind. A popover, and any type with non-modal, opens with show() and leaves the page live.

<ds-overlay open>
<dialog aria-labelledby="example-title">
<h2 id="example-title" data-ds-overlay="title">Example</h2>
<button type="button" data-ds-overlay-dismiss>Close</button>
</dialog>
</ds-overlay>

For a single question, use confirm(), prompt() and alert() from the feedback module: they answer with a promise and are drawn as modals on the same native frame. Never the browser’s own.

Set the host’s open property to open or close it. closeOnEscape and closeOnBackdrop default to true. The cancelable ds-close-request event has a detail.reason of escape, backdrop, outside, button, native or swipe. Prevent its default action when the application must decide whether closing is allowed. Nested overlays retain the background scroll lock until the last one closes. The lock fixes the body at its current offset, then restores its original inline styles and scroll position, including when an underlying overlay closes first.

Select overlay in the source snapshot to include the optional React adapter:

import { Overlay, OverlayContent, OverlayHeader, OverlayTitle, OverlayDescription }
from "./design/react/overlay";
<Overlay open={open} onOpenChange={setOpen}>
<OverlayContent type="sheet" closeLabel="Close">
<OverlayHeader>
<OverlayTitle>Example</OverlayTitle>
<OverlayDescription>Content and actions belong to the app.</OverlayDescription>
</OverlayHeader>
</OverlayContent>
</Overlay>

The adapter links the title and description, portals the content into the body, and provides the shared close button. It does not import Lit during server rendering; register the selected components in the browser before mounting it. Use OverlayFooter for actions. showClose={false} leaves the close out when the content has its own way back (a sheet’s swipe, its own buttons); the header then gives the title its whole row. A controlled root stays open when its onOpenChange handler declines a request. The optional second argument is the close reason, so products can tell Escape from a close button while DS1 remains responsible for the keyboard. A sheet and a drawer stay on the page while they slide out, then leave.

A modal is 20 squares wide, with a square of padding, square corners and no shadow. Set --ds-overlay-width for a different whole-square width. Colours, type and scale follow the shared --ds-* tokens. The title stands on its square’s lower sixteen, as every row’s words do; the header keeps its last square for the close button only when there is one.

flush removes the padding and gap when the content already provides its own. Set --ds-overlay-height for a tall, bounded frame. Content layouts belong to the application; the frame supplies its paper, type, border, backdrop and behaviour. rotated turns a document frame 90 degrees and swaps its viewport bounds. Applications decide when to offer rotation.

Menus inside a modal must stay inside its native dialog or use the browser’s own top layer; body-level layers are outside the modal’s interactive area. Aa mounts its floating toolbar, menus, find controls and link field inside the closest native dialog. Sync Aa before placing an older editor copy in an overlay.

Review a proof

An inline modeless dialog demonstrates its title, body and action alignment.

Phones take a sheet first; a panel beside a list takes a drawer. The modal behaviour is the same in each: the focus trap, Escape, the backdrop and the scroll lock.

<ds-overlay type="sheet">
<dialog aria-labelledby="trade-title">
<h2 id="trade-title" data-ds-overlay="title">Buy</h2>
</dialog>
</ds-overlay>
<ds-overlay type="drawer" side="start">…</ds-overlay>
  • A sheet is as wide as the screen up to 32 squares (--ds-overlay-sheet-width) and centred beyond that, as high as its content up to two squares below the top (--ds-overlay-sheet-gap), then it scrolls. It has no line at its foot; its line at the top is drawn inside its first square (--ds-overlay-line), so its rows stay on whole squares. A handle two squares wide and 4px high stands in the middle of that square, clear of the title, and stays there while the content scrolls; --ds-overlay-handle: none hides it, --ds-overlay-handle-color colours it.
  • A drawer is the screen’s height and 16 squares wide (--ds-overlay-width), leaving at least two squares of the page beside it, with one line on the side facing the page. end is the right of a left-to-right page. It has no handle.
  • Each keeps its square of padding inside the safe area of a phone’s notch and home indicator (env(safe-area-inset-*)); give the page <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">.
  • Each slides in from its edge and back out to it, in transform alone, while the backdrop fades (--ds-overlay-enter 380ms, --ds-overlay-exit 260ms, --ds-overlay-easing). With reduced motion it fades where it stands.

Swiping it away. On a touch screen a finger that drags a sheet down, or a drawer towards its edge, moves it one to one after ten pixels, and the backdrop fades as it goes. Dragged the other way it gives a little and pulls back. On release DS1 projects where the throw would come to rest and dismisses when that is past half its size and the finger is not moving back; otherwise it settles home. Content scrolled down scrolls back up before the sheet moves, a strip that scrolls sideways keeps its touches, and so do a slider, a range input and anything marked data-ds-overlay-no-swipe. Dismissing sends ds-close-request with the reason swipe: prevent it (unsaved changes, say) and the sheet settles home. A mouse or trackpad closes it with Escape or the close button.

A drawer beside a list is often modeless (non-modal), so a press on another row swaps what it shows.

type="screen" covers the whole screen on the page’s paper (--ds-overlay-paper), with a square of padding (none with flush). It arrives by fading in and settling half a square. ds-header and ds-page drawer fold their navigation into one on a narrow screen, through the shared overlay behavior. Focus stays inside, Escape closes it, and the page keeps its scroll position.

A popover stands at a point: set element.anchor = { x, y } in viewport coordinates before opening. DS1 bounds it to the visible viewport with half a square of clearance and updates after content, window and mobile keyboard resizing. element.place() is available after a synchronous content change. Products need no resize listeners or clamping code. Keep the accessible label on the native dialog. A menu that opens from a button is a Select, and one that opens where the pointer is, a Context: use a popover for a panel that is neither.

Set non-modal on ds-overlay to use native show() without a top layer or inert background. Use data-ds-overlay-inline on its native dialog for a panel in the document flow. In React, use OverlayContent nonModal; portal={false} keeps the panel inside its product layout or containing modal. closeOnOutside, blockOutside and holdScroll configure the same behaviour. A native ref points to the light-DOM dialog. If the product removes its launcher while open, set returnFocus={() => launcherRef.current} to resolve the replacement after the closed state renders. DS1 still decides whether focus should return; a modeless overlay does not steal it from an outside control. Set the mode before opening. The shared stylesheet also installs into a containing shadow root, so Lit products use the same frame without copying its CSS.

Modeless overlays can opt into close-on-outside and block-outside: the outside click closes the overlay and does not activate the control behind it. A gesture started inside stays inside. Escape closes only the top one. Set hold-scroll to hold the page without fixing or moving its body. Nested modal and modeless overlays release only their own scroll hold.

await element.show() waits for native opening and the first render of its controls. element.close() closes and releases its hold immediately; camera and media cleanup still belongs to the product’s close handler.

Use element.show({ immediate: true }) only when a rendered control must take focus inside the opening gesture, such as a phone’s search keyboard. The native frame opens synchronously; focus that control before awaiting the returned promise. Nested shared menus and inputs render synchronously too. When a new input is added to an already open overlay, input.renderNow() exposes its native field before gesture-time focus. Modeless Escape is handled after a nested editor can cancel it, and keyboard scrolling inside the overlay remains available while the page behind is held.

Configure a floating overlay with --ds-overlay-inset, --ds-overlay-margin, --ds-overlay-width, --ds-overlay-max-width, --ds-overlay-max-height and --ds-overlay-layer. Transparent media frames may set --ds-overlay-paper and --ds-overlay-border. Use --ds-overlay-overflow: hidden when a bounded layout provides its own scrolling regions.