On or off, taking effect at once (toggle): paging at night, autoscaling, a feature for one service. A screen reader announces it as a switch, on or off.
import { Switch } from "@umriss-ui/core";
Use a switch for a setting that takes effect the moment it flips; Space and a click on the label toggle it.
import { Switch } from "@umriss-ui/core";
export default function OnAndOff() {
return <Switch label="Page me at night" defaultChecked />;
}
Both sizes, off and on, each also disabled; an error on the surrounding FormField marks the switch invalid.
import { FormField, Grid, Stack, Switch, Text } from "@umriss-ui/core";
export default function StatesAndSizes() {
return (
<Grid minItemWidth="220px" gap={5}>
<Stack gap={3}>
<Text size="xs" tone="muted">
Medium
</Text>
<Switch label="Status page" />
<Switch label="Error budget alerts" defaultChecked />
<Switch label="Public API" disabled />
<Switch label="Audit log" disabled defaultChecked />
</Stack>
<Stack gap={3}>
<Text size="xs" tone="muted">
Small
</Text>
<Switch size="sm" label="Status page" />
<Switch size="sm" label="Error budget alerts" defaultChecked />
<Switch size="sm" label="Public API" disabled />
<Switch size="sm" label="Audit log" disabled defaultChecked />
</Stack>
<Stack gap={3}>
<Text size="xs" tone="muted">
Invalid, and a long label
</Text>
<FormField label="Paging" error="A tier 1 service has to page its on-call engineer.">
<Switch label="Page on-call" />
</FormField>
<Switch label="Post every deployment of this service to the team channel, including the automatic rollbacks" defaultChecked />
</Stack>
</Grid>
);
}
A FormField with an error marks the switch invalid by itself. Without one, set invalid and tie your message to the switch with aria-describedby.
import { useId, useState } from "react";
import { Stack, Switch, Text } from "@umriss-ui/core";
export default function WithAnError() {
const [paging, setPaging] = useState(false);
const messageId = useId();
const error = paging ? undefined : "A tier 1 service has to page its on-call engineer.";
return (
<Stack gap={1}>
<Switch
label="Page on-call, Checkout (tier 1)"
checked={paging}
onChange={(event) => setPaging(event.target.checked)}
invalid={error !== undefined}
aria-describedby={error ? messageId : undefined}
/>
{error && (
<Text id={messageId} size="xs" style={{ color: "var(--u-color-danger-text)" }}>
{error}
</Text>
)}
</Stack>
);
}
Control it with checked and onChange when other fields depend on it; here autoscaling takes the instance count away.
import { useState } from "react";
import { FormField, NumberInput, Stack, Switch } from "@umriss-ui/core";
export default function LetTheSwitchDecide() {
const [autoscaling, setAutoscaling] = useState(true);
const [instances, setInstances] = useState<number | null>(6);
return (
<Stack gap={4} style={{ maxWidth: 320 }}>
<Switch label="Autoscaling" checked={autoscaling} onChange={(event) => setAutoscaling(event.target.checked)} />
<FormField label="Instances" hint={autoscaling ? "Set by autoscaling, between 2 and 24." : "Fixed by hand."}>
<NumberInput value={instances} onChange={setInstances} min={1} max={24} decimals={0} disabled={autoscaling} />
</FormField>
</Stack>
);
}
Give each switch its sentence through aria-describedby, and say why when one is disabled for now.
import { useId, useState } from "react";
import { Badge, Card, CardBody, CardHeader, Divider, Stack, Switch, Text } from "@umriss-ui/core";
interface Setting {
id: string;
label: string;
description: string;
lockedDuringIncident?: boolean;
}
const SETTINGS: Setting[] = [
{ id: "deploys", label: "Deployments", description: "Lets the pipeline release to production." },
{ id: "canary", label: "Canary releases", description: "Sends a new release to 5 % of traffic for 15 minutes first." },
{ id: "flags", label: "Feature flag changes", description: "Only while no incident is open on this service.", lockedDuringIncident: true },
{ id: "page", label: "Page on-call", description: "Pages the primary when the latency objective is breached." },
{ id: "digest", label: "Weekly digest", description: "Mails the team the week's latency and error budget." },
];
export default function SettingsOfAService() {
const [on, setOn] = useState<Record<string, boolean>>({ deploys: true, canary: true, page: true });
const incidentOpen = true;
const base = useId();
const count = SETTINGS.filter((s) => on[s.id]).length;
return (
<Card style={{ maxWidth: 520 }}>
<CardHeader eyebrow="Checkout · Payments" title="Service settings" actions={<Badge pill>{count} on</Badge>} />
<CardBody>
<Stack gap={3}>
{SETTINGS.map((s, i) => {
const locked = s.lockedDuringIncident === true && incidentOpen;
return (
<Stack key={s.id} gap={3}>
{i > 0 && <Divider />}
<Stack gap={1}>
<Switch
label={s.label}
checked={on[s.id] === true}
disabled={locked}
aria-describedby={`${base}-${s.id}`}
onChange={(event) => setOn((previous) => ({ ...previous, [s.id]: event.target.checked }))}
/>
<Text id={`${base}-${s.id}`} size="xs" tone="muted" style={{ paddingLeft: 36 }}>
{locked ? "Locked while INC-1048 is open. " : ""}
{s.description}
</Text>
</Stack>
</Stack>
);
})}
</Stack>
</CardBody>
</Card>
);
}
| Key | Action |
|---|---|
Space |
Turns the focused switch on or off. |
SwitchProps| Name | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Label to the right of the switch. Shown in: Configurator, On and off, States and sizes and 13 more |
size | "sm" | "md" | the size of a ControlSizeProvider around it, else "md" | sm for a toolbar and dense forms, md otherwise.Shown in: Configurator, States and sizes |
invalid | boolean | — | Marks the switch as invalid. FormField sets it itself as soon as it
carries an error - by hand only necessary without FormField.Shown in: Configurator, With an error |
ref | Ref<HTMLInputElement> | — | 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 |
Also takes every attribute of <input> – without role, size, type.
What umriss deliberately does not build, and why: ADR-0032.