Range
A multi-thumb range input for selecting a span of numeric values between a minimum and maximum. It supports tick marks, value labels, and configurable stepping.
import { Range } from "@tenstorrent/vesper/range";
export default function RangeDemo() {
return (
<Range
thumbAriaLabels={["Price (min)", "Price (max)"]}
defaultValues={[25, 75]}
/>
);
}Options
| Prop | Type | Description | Default |
|---|---|---|---|
thumbAriaLabels | string[] | Accessible aria-label attributes for each thumb, in the same order as the values. Required. | — |
values | number[] | The values of each thumb (controlled mode). The number of thumbs matches the length of this array. | — |
defaultValues | number[] | The initial thumb values (uncontrolled mode). | [min, max] |
onValuesChange | (values: number[]) => void | Callback fired as thumb values change during interaction. Receives the full array of current values. | — |
onValuesCommit | (values: number[]) => void | Callback fired when a thumb interaction is completed, eg. on pointer up. Receives the final array of values. | — |
min | number | The minimum allowed value. | 0 |
max | number | The maximum allowed value. | 100 |
step | number | The stepping interval between selectable values. | 1 |
minStepsBetweenThumbs | number | The minimum number of steps required between thumbs, preventing them from overlapping. | 1 |
showTicks | boolean | When true, renders tick marks along the track at each step interval. | false |
showValueLabels | boolean | When true, displays a value label above each thumb. | false |
valueLabels | string[] | Custom display labels for each thumb. When unset, each label falls back to that thumb's current value. | — |
disabled | boolean | When true, prevents interaction. | false |
name | string | The form field name submitted with form data. Every thumb submits its value under this name. | — |
form | string | Associates the range with a <form> element by its id. | — |
All other props are forwarded to the wrapping <div> element.
If you only need to select a single value, reach for the Slider component instead: it renders the same track, ticks, and labels, but exposes a single-value API.
Examples
Uncontrolled vs controlled
Render a Range in an uncontrolled fashion to let it keep track of its own values. Pass defaultValues if the thumbs should start somewhere other than min and max:
import { Range } from "@tenstorrent/vesper/range";
export default function UncontrolledRange() {
return (
<Range
defaultValues={[20, 80]}
thumbAriaLabels={["Price (min)", "Price (max)"]}
/>
);
}If you need to control the values, use the values and onValuesChange props. onValuesChange fires continuously as a thumb is dragged, and always receives the full array of values:
Selected values: 25, 75
import { useState } from "react";
import { Button } from "@tenstorrent/vesper/button";
import { Range } from "@tenstorrent/vesper/range";
import { Typography } from "@tenstorrent/vesper/typography";
export default function ControlledRange() {
const [price, setPrice] = useState([25, 75]);
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--vesper-spacing-4)",
}}
>
<Range
aria-label="Price"
values={price}
onValuesChange={setPrice}
thumbAriaLabels={["Price (min)", "Price (max)"]}
/>
<Typography variant="copy-sm">
Selected values: {price.join(", ")}
</Typography>
<Button size="sm" onClick={() => setPrice([0, 100])}>
Reset
</Button>
</div>
);
}Use onValuesCommit when you only care about the final values of an interaction, eg. to avoid firing a request on every pointer move:
import { Range } from "@tenstorrent/vesper/range";
export default function PriceRange() {
return (
<Range
defaultValues={[20, 80]}
onValuesCommit={([min, max]) => fetchResults({ min, max })}
thumbAriaLabels={["Price (min)", "Price (max)"]}
/>
);
}Bounds and stepping
min, max, and step describe the values a thumb can take, defaulting to 0, 100, and 1. A step of 50 on a range of 0-1000 restricts thumbs to multiples of 50:
import { Range } from "@tenstorrent/vesper/range";
export default function LargeStepRange() {
return (
<Range
min={0}
max={1000}
step={50}
defaultValues={[200, 800]}
showValueLabels
thumbAriaLabels={["Budget (min)", "Budget (max)"]}
/>
);
}The step also determines how far a thumb moves per arrow key press when it is focused.
Keeping thumbs apart
By default, thumbs must stay at least one step apart. Raise minStepsBetweenThumbs when a range needs a wider minimum span. The range below keeps its thumbs at least 20 apart:
import { Range } from "@tenstorrent/vesper/range";
export default function CustomStepBetweenThumbsRange() {
return (
<Range
minStepsBetweenThumbs={20}
defaultValues={[30, 70]}
showValueLabels
thumbAriaLabels={["Temperature (min)", "Temperature (max)"]}
/>
);
}The value is measured in steps, not in units, so a minStepsBetweenThumbs of 2 on a range with a step of 5 keeps thumbs at least 10 apart.
Showing ticks
Pass showTicks to mark each step along the track. Ticks are rendered between min and max, so a range of 0-10 with a step of 1 renders nine of them:
import { Range } from "@tenstorrent/vesper/range";
export default function RangeWithTicks() {
return (
<Range
min={0}
max={10}
step={1}
showTicks
defaultValues={[1, 4]}
thumbAriaLabels={["Rating (min)", "Rating (max)"]}
/>
);
}step on a wide range produces a dense, noisy track. Ticks work best when the range has a handful of stops, eg. a range of 0-100 with a step of 10.Value labels
Pass showValueLabels to render each thumb's current value above it as you drag:
import { Range } from "@tenstorrent/vesper/range";
export default function RangeShowingValueLabels() {
return (
<Range
showValueLabels
defaultValues={[30, 70]}
thumbAriaLabels={["Price (min)", "Price (max)"]}
/>
);
}Use valueLabels to display something other than the raw value, such as a formatted or unit-suffixed one. Labels are matched to thumbs by index, and are only rendered when showValueLabels is also passed:
import { useState } from "react";
import { Range } from "@tenstorrent/vesper/range";
export default function CustomThumbLabelsRange() {
const [price, setPrice] = useState([30, 70]);
return (
<Range
aria-label="Price"
showValueLabels
values={price}
onValuesChange={setPrice}
valueLabels={price.map((value) => `$${value}`)}
thumbAriaLabels={["Price (min)", "Price (max)"]}
/>
);
}thumbAriaLabels too.More than two thumbs
A Range renders one thumb per entry in values or defaultValues, so it isn't limited to two. Give every thumb an entry in thumbAriaLabels, and in valueLabels when you are customising labels:
import { Range } from "@tenstorrent/vesper/range";
export default function ThreeThumbsRange() {
return (
<Range
showValueLabels
defaultValues={[20, 50, 80]}
valueLabels={["Low", "Mid", "High"]}
thumbAriaLabels={["Low threshold", "Mid threshold", "High threshold"]}
/>
);
}Labels and accessible names
thumbAriaLabels names the individual thumbs. It is required, and each thumb needs its own entry. A thumb without a label is announced with no indication of which end of the range it controls.
import { Range } from "@tenstorrent/vesper/range";
export default function RangeDemo() {
return (
<Range
defaultValues={[25, 75]}
thumbAriaLabels={["Price (min)", "Price (max)"]}
/>
);
}Keyboard interaction
Each thumb is a native <input type="range">, so a Range is fully keyboard operable once a thumb is focused:
| Key | Behaviour |
|---|---|
ArrowRight / ArrowUp | Increases the focused thumb by step. |
ArrowLeft / ArrowDown | Decreases the focused thumb by step. |
Shift + any arrow key | Moves the focused thumb by 10 units instead of step. |
PageUp / PageDown | Increases / decreases the focused thumb by 10 units. |
Home | Moves the focused thumb as far towards min as it is allowed to go. |
End | Moves the focused thumb as far towards max as it is allowed to go. |
Disabling the range
Pass disabled to prevent all interaction. A disabled Range still displays its current values, but its thumbs are skipped in the tab order and excluded from form data:
import { Range } from "@tenstorrent/vesper/range";
export default function DisabledRange() {
return (
<Range
disabled
defaultValues={[25, 75]}
thumbAriaLabels={["Price (min)", "Price (max)"]}
/>
);
}Usage in forms
Each thumb renders a native <input type="range">, so passing a name includes every thumb's value in form submissions under that name.
Since every thumb submits under the same name, the form below submits price=20&price=80. Read both values with FormData.getAll:
const data = new FormData(form);
data.getAll("price"); // ["20", "80"]import { useState } from "react";
import { Button } from "@tenstorrent/vesper/button";
import { Range } from "@tenstorrent/vesper/range";
import { Typography } from "@tenstorrent/vesper/typography";
export default function FormRangeDemo() {
const [submitted, setSubmitted] = useState<string[] | null>(null);
return (
<form
style={{
display: "flex",
flexDirection: "column",
gap: "var(--vesper-spacing-4)",
}}
onSubmit={(event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
setSubmitted(data.getAll("price").map(String));
}}
>
<Range
name="price"
aria-label="Price"
defaultValues={[20, 80]}
thumbAriaLabels={["Price (min)", "Price (max)"]}
/>
<Button size="sm" type="submit">
Submit
</Button>
{submitted !== null && (
<Typography variant="copy-sm">
Submitted values: {submitted.join(", ")}
</Typography>
)}
</form>
);
}