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

PropTypeDescriptionDefault
thumbAriaLabelsstring[]Accessible aria-label attributes for each thumb, in the same order as the values. Required.—
valuesnumber[]The values of each thumb (controlled mode). The number of thumbs matches the length of this array.—
defaultValuesnumber[]The initial thumb values (uncontrolled mode).[min, max]
onValuesChange(values: number[]) => voidCallback fired as thumb values change during interaction. Receives the full array of current values.—
onValuesCommit(values: number[]) => voidCallback fired when a thumb interaction is completed, eg. on pointer up. Receives the final array of values.—
minnumberThe minimum allowed value.0
maxnumberThe maximum allowed value.100
stepnumberThe stepping interval between selectable values.1
minStepsBetweenThumbsnumberThe minimum number of steps required between thumbs, preventing them from overlapping.1
showTicksbooleanWhen true, renders tick marks along the track at each step interval.false
showValueLabelsbooleanWhen true, displays a value label above each thumb.false
valueLabelsstring[]Custom display labels for each thumb. When unset, each label falls back to that thumb's current value.—
disabledbooleanWhen true, prevents interaction.false
namestringThe form field name submitted with form data. Every thumb submits its value under this name.—
formstringAssociates 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)"]}
    />
  );
}
Ticks are drawn per step, so a fine 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)"]}
    />
  );
}
Value labels are presentational, and are not announced by assistive technology. Screen readers announce the underlying numeric value of a thumb instead. If a label carries meaning that the number alone doesn't, such as a currency or a unit, include it in the 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:

KeyBehaviour
ArrowRight / ArrowUpIncreases the focused thumb by step.
ArrowLeft / ArrowDownDecreases the focused thumb by step.
Shift + any arrow keyMoves the focused thumb by 10 units instead of step.
PageUp / PageDownIncreases / decreases the focused thumb by 10 units.
HomeMoves the focused thumb as far towards min as it is allowed to go.
EndMoves 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>
  );
}