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:
type | What it is | Where it stands |
|---|---|---|
modal (the default) | A question or a task that holds the page | In the centre, 20 squares wide |
sheet | A phone’s first choice for the same | From the foot of the screen, up to 32 squares wide |
drawer | A panel beside a list, a detail or settings | At the side end (the default) or start, the screen’s height, 16 squares wide |
popover | A small panel at a point | At anchor, kept inside the visible screen |
screen | Folded navigation, a document viewer | Over 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.
The frame
Section titled “The frame”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.
Design examples
Section titled “Design examples”An inline modeless dialog demonstrates its title, body and action alignment.
<section class="specimen">
<ds-overlay open non-modal><dialog aria-labelledby="proof-title"><h2 id="proof-title" data-ds-overlay="title">Approve this proof?</h2><p>The printer will use this version for the edition.</p><div data-ds-overlay="actions"><ds-button secondary data-ds-overlay-dismiss>Keep reviewing</ds-button><ds-button primary data-ds-overlay-dismiss>Approve</ds-button></div></dialog></ds-overlay>
</section>
<style>
.specimen { width: 100%; min-width: 0; --cell: calc(20px * var(--sf, 1)); }
.specimen .line { width: 100%; }
.specimen .grow { flex: 1; min-width: 0; }
.specimen .muted { color: var(--ds-text-color-muted); }
.specimen .mono { --ds-typeface-regular: var(--ds-typeface-mono); font-variant-numeric: tabular-nums; }
.specimen .value { width: calc(4 * var(--cell)); text-align: right; }
.specimen .mark { display: inline-flex; align-items: flex-end; width: var(--cell); height: var(--ds-control-height); flex: 0 0 var(--cell); }
.specimen .columns { display: flex; flex-wrap: wrap; gap: var(--cell); }
.specimen .columns > * { min-width: 0; }
.specimen fieldset { border: 0; padding: 0; margin: 0; }
.specimen legend { padding: 0; }
.specimen label { display: flex; align-items: flex-end; gap: calc(var(--cell) / 2); min-height: var(--cell); }
.specimen ds-panel { height: calc(10 * var(--cell)); }
</style> Sheets and drawers
Section titled “Sheets and drawers”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: nonehides it,--ds-overlay-handle-colorcolours 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.endis 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-enter380ms,--ds-overlay-exit260ms,--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.
The screen
Section titled “The screen”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.
Popovers
Section titled “Popovers”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.
Modeless overlays
Section titled “Modeless overlays”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.