IX Surfaces

Star on GitHubFeedback
IX SurfacesNative HTMLot-drawer

Drawer

A sheet from below, lifted by the fermata's arc. Drag the arc down to put it away.

Report an issue with Drawer

Example

Filter the work

Kinds of work

Notes on the Halden brief

Drag the arc up for more of them, or press the arrow keys on it.

  1. Keep the flag. The blue is older than the company and every islander knows it.
  2. Lose the anchor. It says harbour; the line is about the crossing.
  3. The timetable is the real product. Set it large enough to read from the quay.
  4. One typeface, two weights. The ferries' names in the italic.
  5. Paint the hulls last. The livery follows the type, not the other way round.

Sort the work

Kinds of work
Open

Filter the work

thirds, at two thirds

Notes

Keep the flag. Lose the anchor.

solid, at rest

Sort the work

By year

solid, pulled halfway

Sort the work

By year

solid, set solid

Sort the work

By year

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base, dialog, and fraction along.

npx shadcn@latest add https://thedirectors.agency/ui/r/drawer.json

Usage

This example also uses Button and Checkbox.

npx shadcn@latest add https://thedirectors.agency/ui/r/button.json https://thedirectors.agency/ui/r/checkbox.json
drawer-example.tsx
import type { CSSProperties, ReactNode } from "react"import { Button } from "@/components/ui/button"import { Checkbox, CheckboxGroup } from "@/components/ui/checkbox"import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"import { Fraction } from "@/components/ui/fraction"const notes = ["Keep the flag. The blue is older than the company and every islander knows it.","Lose the anchor. It says harbour; the line is about the crossing.","The timetable is the real product. Set it large enough to read from the quay.","One typeface, two weights. The ferries' names in the italic.","Paint the hulls last. The livery follows the type, not the other way round.",]function Filters() {return (<CheckboxGroup legend="Kinds of work"><Checkbox>Identity</Checkbox><Checkbox>Web</Checkbox><Checkbox>Motion</Checkbox></CheckboxGroup>)}function Notes() {return (<ol className="grid gap-4" style={{ maxWidth: "36rem" }}>{notes.map((n) => (<li key={n}>{n}</li>))}</ol>)}export default function Example() {return (<div className="flex flex-wrap items-center gap-10"><Drawer><DrawerTrigger asChild><Button variant="bracket">Filter the work</Button></DrawerTrigger><DrawerContent><DrawerTitle>Filter the work</DrawerTitle><Filters /><DrawerClose asChild><Button variant="statement" data-autofocus>Show 7 projects</Button></DrawerClose></DrawerContent></Drawer><Drawer><DrawerTrigger asChild><Button variant="bracket">Read the notes</Button></DrawerTrigger><DrawerContent variant="thirds" handleLabel="Height of the notes"><DrawerTitle>Notes on the Halden brief</DrawerTitle><DrawerDescription>Drag the arc up for more of them, or press the arrow keys on it.</DrawerDescription><Notes /><DrawerClose asChild><Button variant="bracket">Close</Button></DrawerClose></DrawerContent></Drawer><Drawer><DrawerTrigger asChild><Button variant="bracket">Sort the work</Button></DrawerTrigger><DrawerContent variant="solid"><DrawerTitle>Sort the work</DrawerTitle><Filters /><DrawerClose asChild><Button variant="statement" data-autofocus>Sort by year</Button></DrawerClose></DrawerContent></Drawer></div>)}/** A drawer drawn still, in the page, for the states row. */function Still({ variant, style, share, children }: { variant?: string; style?: CSSProperties; share?: number; children: ReactNode }) {return (<dialogopenclassName="ot-drawer"data-variant={variant}style={{ position: "relative", inset: "auto", translate: "none", width: "min(22rem, 100%)", maxHeight: "none", height: "auto", boxShadow: "none", transition: "none", ...style }}><span className="ot-drawer-handle" aria-hidden="true" style={{ width: "fit-content" }}><svg viewBox="0 0 40 22"><path d="M3 20 A17 17 0 0 1 37 20" /><circle cx="20" cy="17" r="2.6" /></svg></span>{share && (<span className="ot-drawer-share" aria-hidden="true"><Fraction count={share} total={3} /></span>)}<div className="ot-drawer-body">{children}</div></dialog>)}

Contract

  • Underneath: native <dialog>, through the dialog item.
  • Anatomy: <dialog class="ot-sheet" closedby="any"> with .ot-sheet-spine (the title again, aria-hidden). <dialog class="ot-drawer" closedby="any"> with <button class="ot-drawer-handle">. Both share the dialog backdrop.
  • A sheet is a page slid in from the end edge, its title running up the spine like a book's (ot-spine), in its own column: the sheet's start padding, on the left, or the right in right-to-left. A drawer rises from below and lands with spiccato; its handle is the fermata's arc. Dragging the handle down sets --pull and data-pulling, and past a threshold it closes.
  • Keyboard: Escape closes; the handle closes on Enter. Smooth scrolling pauses while open.
  • Sheet variants go on data-variant (SheetContent variant): spine (the default), shelf, rag, fold.
  • Shelf, after books standing on a shelf: a sheet opened from inside a shelf sheet stands in front of it, one spine column narrower, so the spine behind stays in view, set back to pencil, and is the way back: pointing at it (the bare backdrop) or Escape puts the front sheet away and focus returns to the control that opened it. Only the front sheet blurs the page. Below 40rem the column is space-7 and the spine mf.
  • Rag, after "Healthy habits" cut from its paper and "Less is more." set flush right: the words stand flush to the window's edge and rag toward the page, and the paper is cut to that rag, a step for every line, with a hairline along the cut. The script reads the line boxes (after the fonts), draws <svg class="ot-sheet-rag" aria-hidden="true"> behind them and sets data-laid; until then it is plain paper with its edge. The box hangs from the top only as tall as its words, and a click on its bare part is a click outside. Start and end sides only. It re-cuts on resize and when <html> changes its face. Forced colours: Canvas paper, CanvasText cut.
  • Fold, after the index entry that opens to its page and "Less is more." set between its hairlines: the row that opened it unfolds into the page. Before it opens (and again before it closes) the sheet writes the opener's box to --fold-t/r/b/l; the page is clipped to that band and opens out to the window, the row's two hairlines (ink, ::before and ::after) riding the clip's edges to the top and the foot, while the row's name (its rows-title, or the trigger's own words) grows into the title, set at --ot-ff (--ot-f below 40rem), by a transform measured from one to the other so their first lines meet. The rest arrives once there is room. A reversed row opens from its ink (data-from-ink: the page starts ink and washes to paper). Put away, it folds back into the row and the title flies back to the name. The whole window, no spine; side is ignored. The trigger remembers itself (SheetTrigger), so open it from <Row asChild><SheetTrigger> with one Sheet round the Rows; opened by state alone, a line across the middle parts. Reduced motion: it opens at once. Forced colours: the lines in CanvasText.
  • Panels (SheetPanels, .ot-sheet-panels holding nav.ot-sheet-spine.ot-sheet-trail and section.ot-sheet-panel pages): pages inside one sheet, the chapters of the book on its spine, and the scale contrast of "It has to be design.". The spine becomes the trail, read up from the foot (right to left as well): the names you came through small (mp) in the pencil, the one you are in large (f, 200) in ink, aria-current="page". Stepping in (SheetPanelLink to), the name you left shrinks into the trail and the new one writes up the spine; the new page slides in from the sheet's own edge while the old one leaves the other way, the two crossing in one grid cell (hidden and inert on the pages you are not in, the exit kept by display allow-discrete). The small names are buttons and the way back. Focus goes to the new page's heading, and going back returns it to the link that stepped in; the sheet scrolls to the top. Each opening starts at the root, whose title names the dialog. Below 40rem the trail is mf over p. It belongs to the sheet, not the drawer: a sheet is already a page with a spine to carry the path, where a drawer is a tray whose height the thirds decide.
  • Drawer variants go on data-variant (DrawerContent variant): arc (the default), thirds, solid.
  • Thirds, after SPECTRA's "1/3": the drawer stops at a third, two thirds or the whole height, and the share stands at the top end corner as a Fraction (aria-hidden), thin and large. The handle is then role="slider" (vertical, 1 to 3, "2 of 3"): drag it to the nearest third, or lower than half a third to put it away; a tap or Enter raises it a third, round to one; the arrow keys, Home and End step it. Each opening starts at a third.
  • Solid, after the compositor's leads: pulling the arc first takes the silence out between the drawer's parts (--solid from 0 to 1 closes the gaps and the padding), as type is set solid when the leads come out; only once it is set solid does the drawer itself move, and letting go past that closes it. Its give is the height it loses, measured on pointerdown. Keyboard as arc.

Props

Props of Drawer
PropTypeDefaultWhat it does
DrawerDialog propsThe same open, defaultOpen, onOpenChange and alert as Dialog.
DrawerContent variant"arc" | "thirds" | "solid""arc"How the drawer answers the hand. arc: drag the arc down past a short pull, or tap it, and it goes. thirds: it opens at a third of the window and stops at a third, two thirds or the whole height; the arc follows your drag both ways and lets go on the nearest third (below half of one, it goes), a tap raises it a third, and the share stands in the far corner as a Fraction, yours in italic. The arc is then a vertical slider: the arrow keys step it, Home and End go to a third and the whole; Escape closes. solid: pulling the arc first takes out the silence between the drawer's parts, so it closes up and sinks under your hand, as a compositor sets type solid; let go once it is solid and it goes, short of that the silence springs back.
DrawerContent handleLabelstring"Put this away" (thirds: "Height of the drawer")The accessible name of the arc. Tapping it, or pressing Enter on it, closes the drawer; in thirds it raises it a third.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
DrawerThe fermata signThe handle is the arc
Thirds drawerSPECTRA's "1/3"It stops at thirds; the share is the fraction in the corner
Solid drawerThe compositor's leads (set solid)The pull takes out the silence first, then the drawer

How it moves

How it moves
ComponentArticulationWhat moves
DrawerSpiccatoIt lands with a rebound; it follows your drag down
Thirds drawerSpiccatoIt follows the hand both ways and lands on a third; the fraction's wheels turn
Solid drawerLegatoThe gaps close under the hand, then the drawer follows it down