A value set roughly between two bounds (range slider): a share of traffic, an alert threshold, weekly hours. The position on the track says as much as the figure.
import { Slider } from "@umriss-ui/core";
onChange reports a number already on the step and inside the bounds, by pointer and by key alike. Pair it with a NumberInput on the same state when the exact figure matters too.
Use a slider for a value set roughly between two bounds; step sets how finely it moves.
import { FormField, Slider } from "@umriss-ui/core";
export default function CanaryTraffic() {
return (
<FormField label="Canary traffic, %" hint="Share of requests the new release receives." style={{ maxWidth: 360 }}>
<Slider min={0} max={100} step={5} defaultValue={10} />
</FormField>
);
}
format writes the value for the readout and the screen reader; marks show reference values, with a word where one has a label.
import { FormField, Grid, Slider } from "@umriss-ui/core";
export default function UnitsAndMarks() {
return (
<Grid minItemWidth="260px" gap={6}>
<FormField label="Alert above error rate">
<Slider
min={0}
max={5}
step={0.1}
defaultValue={1.5}
format={(v) => `${v.toFixed(1)} %`}
marks={[0, { value: 1, label: "Objective" }, 5]}
/>
</FormField>
<FormField label="Log retention" hint="30 days, fixed by the data protection policy.">
<Slider min={7} max={90} defaultValue={30} disabled showValue={false} marks={[7, 30, 90]} />
</FormField>
</Grid>
);
}
Control it with value and onChange and put a NumberInput beside it on the same state for the exact figure.
import { useState } from "react";
import { FormField, NumberInput, Slider, Stack } from "@umriss-ui/core";
export default function SetRoughlyThenExactly() {
const [hours, setHours] = useState(32);
return (
<Stack gap={3} style={{ maxWidth: 420 }}>
<FormField label="Weekly capacity, Kofi Mensah">
<Slider
min={0}
max={48}
value={hours}
onChange={setHours}
showValue={false}
format={(v) => `${v} h`}
marks={[{ value: 0, label: "0" }, { value: 40, label: "Full time" }, { value: 48, label: "48" }]}
/>
</FormField>
<FormField label="Exact hours">
<NumberInput value={hours} onChange={(v) => setHours(v ?? 0)} min={0} max={48} decimals={0} suffix="h" />
</FormField>
</Stack>
);
}
Several sliders share one whole; show the sum they make in words, since no single slider can say it.
import { useState } from "react";
import { Badge, Card, CardBody, CardHeader, FormField, Slider, Stack, Text, useFormats } from "@umriss-ui/core";
const CHANNELS = [
{ id: "search", label: "Search ads", plan: 45 },
{ id: "events", label: "Events", plan: 30 },
{ id: "content", label: "Content", plan: 25 },
];
const BUDGET = 68_000;
export default function SplitABudget() {
const formats = useFormats();
const [shares, setShares] = useState<Record<string, number>>({ search: 45, events: 35, content: 25 });
const sum = CHANNELS.reduce((total, c) => total + (shares[c.id] ?? 0), 0);
return (
<Card style={{ maxWidth: 560 }}>
<CardHeader
eyebrow="CC-1200 Marketing · April"
title={`Budget ${formats.number(BUDGET, 0)} €`}
actions={<Badge tone={sum === 100 ? "success" : "warning"}>{sum === 100 ? "Adds up to 100 %" : `Sum ${sum} %`}</Badge>}
/>
<CardBody>
<Stack gap={5}>
{CHANNELS.map((c) => (
<FormField key={c.id} label={c.label}>
<Slider
step={5}
value={shares[c.id] ?? 0}
onChange={(v) => setShares((previous) => ({ ...previous, [c.id]: v }))}
format={(v) => `${v} % · ${formats.number((BUDGET * v) / 100, 0)} €`}
marks={[0, 50, 100, { value: c.plan, label: "Plan" }]}
/>
</FormField>
))}
<Text size="xs" tone="muted">
The plan was agreed in the March review.
</Text>
</Stack>
</CardBody>
</Card>
);
}
| Key | Action |
|---|---|
Arrow Right / Arrow Up |
Increases the value by one step. |
Arrow Left / Arrow Down |
Decreases the value by one step. |
Page Up / Page Down |
Moves by a tenth of the range. |
Home / End |
Moves to the lower or upper bound. |
SliderProps| Name | Type | Default | Description |
|---|---|---|---|
defaultValue | number | min | Uncontrolled: the value to start at. Shown in: Configurator, Slider, Units and marks |
value | number | — | Controlled: the value. Shown in: Set roughly, then exactly, Split a budget, Browse down and back up (Breadcrumb) and 2 more |
min | number | 0 | The smallest value. Shown in: Configurator, Slider, Units and marks and 4 more |
max | number | 100 | The largest value. Shown in: Configurator, Slider, Units and marks and 4 more |
step | number | 1 | The step the value moves by, by pointer or key. Shown in: Configurator, Slider, Units and marks and 3 more |
marks | readonly SliderMark[] | — | Values marked on the track; an object with a label writes the word
beneath its mark. A mark shows a value, it does not catch the thumb.Shown in: Units and marks, Set roughly, then exactly, Split a budget and 1 more |
format | (value: number) => string | the number in the formats' notation, with as many decimals as the step has | The value as text: for the readout and for the screen reader
(aria-valuetext).Shown in: Units and marks, Set roughly, then exactly, Split a budget and 3 more |
showValue | boolean | true | The value in mono beside the track. Shown in: Configurator, Units and marks, Set roughly, then exactly and 1 more |
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 |
| Name | Type | Default | Description |
|---|---|---|---|
onChange | (value: number) => void | — | Reports every change, by pointer or key, as a number on the step. Shown in: Set roughly, then exactly, Split a budget, Browse down and back up (Breadcrumb) and 2 more |
Also takes every attribute of <input> – without defaultValue, max, min, onChange, step, type, value.
SliderMarkA mark on the track: a value, or a value with the word beneath it.
type SliderMark = number | { value: number; label?: ReactNode };What umriss deliberately does not build, and why: ADR-0032.