A modal window that slides in from the left or right edge of the screen (also called a side sheet or side panel). Reach for it for the detail of something picked from a list, while the list stays in sight behind it.
import { Drawer, ModalHeader, ModalBody, ModalFooter } from "@umriss-ui/core";
Everything the modal promises holds: the focus stays inside, Escape and a click beside it ask to close, and the focus returns to the button that opened it. Header, body and footer are the modal's parts.
Its width is the token --u-drawer-width: set it once for the application, or in one drawer's style where it needs more room.
A Drawer is a Modal at the right edge, with the same header, body and footer and the same hold on focus.
import { useState } from "react";
import { Button, Drawer, ModalBody, ModalFooter, ModalHeader, Text } from "@umriss-ui/core";
export default function ADetailFromTheEdge() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Show the shipment</Button>
<Drawer open={open} onClose={() => setOpen(false)}>
<ModalHeader title="Shipment SH-1042" description="Harlow Bakery · out for delivery on tour T-01" />
<ModalBody>
<Text>Two parcels, 14 kg in all, to be delivered between 09:00 and 11:00.</Text>
</ModalBody>
<ModalFooter>
<Button onClick={() => setOpen(false)}>Close</Button>
</ModalFooter>
</Drawer>
</>
);
}
side set to left puts it at the other edge; one drawer that needs more room sets --u-drawer-width in its own style.
import { useState } from "react";
import type { CSSProperties } from "react";
import { Button, Checkbox, Drawer, ModalBody, ModalFooter, ModalHeader, Stack, Text } from "@umriss-ui/core";
const TEAMS = ["Payments", "Identity", "Discovery", "Messaging", "Integrations", "Insights"];
export default function LeftAndWider() {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Filter the alerts</Button>
<Drawer open={open} onClose={() => setOpen(false)} side="left" style={{ "--u-drawer-width": "480px" } as CSSProperties}>
<ModalHeader title="Filter" description="Which alerts the list shows." />
<ModalBody>
<Stack gap={3}>
<Text size="sm" weight="medium">
Owning team
</Text>
{TEAMS.map((team) => (
<Checkbox key={team} label={team} defaultChecked={team === "Payments" || team === "Identity"} />
))}
<Text size="sm" weight="medium">
Priority
</Text>
<Checkbox label="High – pages the on-call engineer at any hour" defaultChecked />
<Checkbox label="Medium – pages during working hours, otherwise waits" defaultChecked />
<Checkbox label="Low – collected into the morning's summary" />
</Stack>
</ModalBody>
<ModalFooter>
<Button onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={() => setOpen(false)}>
Apply
</Button>
</ModalFooter>
</Drawer>
</>
);
}
The list stays in sight behind the backdrop; which service is open is your state, and the drawer only shows it.
import { useState } from "react";
import { Badge, Button, Drawer, Grid, Meter, ModalBody, ModalFooter, ModalHeader, Stack, Stat, Switch, Text } from "@umriss-ui/core";
import type { LimitSet } from "@umriss-ui/core";
/* Data from the operations world, written out here so the example runs on its own. */
/** A small LCG - the same numbers on every computer. */
function random(seed: number): () => number {
let s = seed >>> 0;
return () => {
s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
return s / 4294967296;
};
}
const at = (day: number, hours: number, minutes = 0) => new Date(2026, 2, day, hours, minutes).getTime();
const MINUTE = 60_000;
/** Tuesday, 17 March 2026, 10:30 - the moment the screens are read at. */
const NOW = at(17, 10, 30);
interface Service {
id: string;
name: string;
team: string;
/** 1 is customer-facing and pages at night; 3 waits for the morning. */
tier: 1 | 2 | 3;
/** The latency objective: the 95th percentile stays below this, in ms. */
latencySlo: number;
/** The availability promised for a month, in per cent. */
availabilityTarget: number;
}
const SERVICES: readonly Service[] = [
{ id: "checkout", name: "Checkout", team: "Payments", tier: 1, latencySlo: 300, availabilityTarget: 99.95 },
{ id: "billing", name: "Billing", team: "Payments", tier: 1, latencySlo: 400, availabilityTarget: 99.9 },
{ id: "sign-in", name: "Sign-in", team: "Identity", tier: 1, latencySlo: 200, availabilityTarget: 99.95 },
{ id: "search", name: "Search", team: "Discovery", tier: 1, latencySlo: 250, availabilityTarget: 99.9 },
{ id: "images", name: "Image service", team: "Discovery", tier: 2, latencySlo: 500, availabilityTarget: 99.5 },
{ id: "notifications", name: "Notifications", team: "Messaging", tier: 2, latencySlo: 800, availabilityTarget: 99.5 },
{ id: "webhooks", name: "Webhooks", team: "Integrations", tier: 2, latencySlo: 1000, availabilityTarget: 99.5 },
{ id: "reports", name: "Reporting", team: "Insights", tier: 3, latencySlo: 2000, availabilityTarget: 99 },
];
interface MetricPoint {
t: number;
/** Median and 95th percentile latency, in ms. */
p50: number;
p95: number;
/** Failed requests, in per cent. */
errorRate: number;
/** Requests per minute. */
requests: number;
}
/** Requests per minute at the busiest hour, by tier. */
const VOLUME = { 1: 900, 2: 400, 3: 120 } as const;
/** A service's latency and error rate every five minutes of today, from
midnight up to now. Busy by day, quiet at night; Checkout's 95th percentile
climbs well past its objective from 09:40 to 10:10, and its errors with it. */
function metrics(serviceId: string): MetricPoint[] {
const index = SERVICES.findIndex((one) => one.id === serviceId);
const service = SERVICES[index];
if (service === undefined) throw new Error(`No service "${serviceId}".`);
const r = random(1000 + index * 37);
const points: MetricPoint[] = [];
let drift = 0;
for (let t = at(17, 0); t <= NOW; t += 5 * MINUTE) {
const hour = (t - at(17, 0)) / (60 * MINUTE);
const load = 0.35 + 0.65 * Math.max(0, Math.sin((Math.PI * (hour - 5)) / 16));
drift = 0.8 * drift + (r() - 0.5) * 0.08;
const base = service.latencySlo * (0.45 + 0.2 * load + drift);
const surge = serviceId === "checkout" && t >= at(17, 9, 40) && t < at(17, 10, 10) ? 1.9 : 1;
const p95 = Math.round(base * surge * (1 + r() * 0.08));
points.push({
t,
p50: Math.round(p95 * (0.42 + r() * 0.06)),
p95,
errorRate: Math.round((0.05 + r() * 0.12 + (surge > 1 ? 2.4 + r() : 0)) * 100) / 100,
requests: Math.round(VOLUME[service.tier] * load * (1 + (r() - 0.5) * 0.1)),
});
}
return points;
}
/** The month so far: 1 March 00:00 to now, in minutes. */
const MONTH_MINUTES = (NOW - at(1, 0)) / MINUTE;
/** Minutes each service was down this month. With `MONTH_MINUTES` and the
service's target, what an availability is worked out from. */
const DOWNTIME_MINUTES: Readonly<Record<string, number>> = {
checkout: 34,
billing: 18,
"sign-in": 41,
search: 85,
images: 120,
notifications: 12,
webhooks: 210,
reports: 95,
};
/** The latency objective as a rule: warn at 80 %, alarm at the objective itself. */
function latencyLimits(service: Service): LimitSet {
return {
limits: [
{ value: service.latencySlo * 0.8, side: "upper", severity: "warning" },
{ value: service.latencySlo, side: "upper", severity: "alarm" },
],
};
}
/** How much of the month's allowed downtime is used up. */
function budgetUsed(service: Service): number {
const allowed = MONTH_MINUTES * (1 - service.availabilityTarget / 100);
return (DOWNTIME_MINUTES[service.id] ?? 0) / allowed;
}
export default function BesideAServiceList() {
const [openId, setOpenId] = useState<string | null>(null);
const service = SERVICES.find((one) => one.id === openId);
const points = service ? metrics(service.id).slice(-24) : [];
const used = service ? budgetUsed(service) : 0;
return (
<>
<Grid minItemWidth="150px" gap={3}>
{SERVICES.map((one) => (
<Button key={one.id} onClick={() => setOpenId(one.id)}>
{one.name}
</Button>
))}
</Grid>
<Drawer open={service !== undefined} onClose={() => setOpenId(null)}>
{service && (
<>
<ModalHeader title={service.name} description={`${service.team} · tier ${service.tier}`} />
<ModalBody>
<Stack gap={5}>
<Stat
label="p95 latency, last two hours"
value={points.at(-1)?.p95}
unit="ms"
decimals={0}
limits={latencyLimits(service)}
history={points.map((point) => point.p95)}
/>
<Stack gap={1}>
<Text size="xs" tone="muted">
Downtime budget used this month
</Text>
<Meter
value={used}
tone={used >= 1 ? "danger" : used >= 0.75 ? "warning" : "neutral"}
showLabel
label={`Downtime budget of ${service.name}`}
/>
<Text size="xs" tone="muted">
{DOWNTIME_MINUTES[service.id]} min down, target {service.availabilityTarget} %
</Text>
</Stack>
<Stack gap={1} align="flex-start">
<Text size="xs" tone="muted">
Alerts
</Text>
<Badge tone={service.tier === 1 ? "accent" : "neutral"}>{service.tier === 1 ? "Pages at night" : "Waits for the morning"}</Badge>
</Stack>
<Switch label="Page the on-call engineer" defaultChecked={service.tier === 1} />
</Stack>
</ModalBody>
<ModalFooter>
<Button onClick={() => setOpenId(null)}>Close</Button>
<Button variant="primary" onClick={() => setOpenId(null)}>
Open the runbook
</Button>
</ModalFooter>
</>
)}
</Drawer>
</>
);
}
| Key | Action |
|---|---|
Tab / Shift+Tab |
Moves between the controls inside; focus does not leave the window. |
Escape |
Asks to close: onClose is called, and the window goes when you close it. |
DrawerProps| Name | Type | Default | Description |
|---|---|---|---|
side | "right" | "left" | "right" | The edge the drawer stands at and enters from. Shown in: From the left, and wider |
openrequired | boolean | — | Whether the window stands. Controlled: the opening is the caller's
decision, and only the caller knows whether something may be open. from ModalPropsShown in: A detail from the edge, From the left, and wider, Open a service's detail from its list and 10 more |
closeOnBackdrop | boolean | true | Closes on a click on the backdrop. from ModalPropsShown in: A window that must be answered (Modal) |
ref | Ref<HTMLDialogElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
| Name | Type | Default | Description |
|---|---|---|---|
onCloserequired | () => void | — | Reports every wish to close – Escape, cross, backdrop click. from ModalPropsShown in: A detail from the edge, From the left, and wider, Open a service's detail from its list and 10 more |
Also takes every attribute of <dialog> – without onClose.
What umriss deliberately does not build, and why: ADR-0032.