Forms

Cell Slider

A slider restyled as a settings-list row — the track is the visible cell, with its label pinned to one end, its value to the other, and a thin pill marking where the value falls.

Usage

Display brightness62

CellSlider is for a setting that lives in a list of settings: brightness, volume, a quality preset, anything you tune rather than pick. The whole row is the control — press anywhere along it and the value jumps there — so it presses like the rows above and below it instead of announcing itself as a slider dropped into a list.

It wraps the Slider from @blakeui/react and changes nothing about how it behaves. The value state, the drag, and the <input type="range"> carrying role="slider" with its aria-valuenow, aria-valuemin and aria-valuemax are all the underlying component's; this layer is layout and paint. Arrow keys step, Home and End jump to the ends, and the row rings when the thumb takes keyboard focus.

Use a plain Slider instead when the control is the subject of its own block rather than one row among several — a filter panel, a single form field with a caption. A cell only pays off in a stack.

Anatomy

Import the CellSlider component and access all parts using dot notation.

import {CellSlider} from "@blakeui/pro-react";

<CellSlider>
  <CellSlider.Track>
    <CellSlider.Fill />
    <CellSlider.Label />
    <CellSlider.Output />
    <CellSlider.Thumb />
  </CellSlider.Track>
</CellSlider>;

CellSlider.Track is the visible cell, and everything else sits inside it: the fill and the thumb are positioned along it, the label and the output are pinned to its two ends. That is the whole reason the row reads as a row — there is no outer flex layout placing a slider beside a label, just one box with things in it.

CellSlider.Label names the slider. It renders a <span> rather than a <label>, because a label element wrapping the drag surface is the wrong semantics for a row you press, but it is still React Aria's Label underneath — it finds the slider through context and becomes the thumb's accessible name with no id wiring on your part. A cell without one needs an aria-label on the root.

The component is always horizontal. orientation is not forwarded: a vertical cell is a different shape, not a variant of this one.

Controlled

Pass value and onChange to drive the row from your own state. onChange fires continuously during a drag; onChangeEnd fires once on release, which is the one to send to a server.

Export quality70

Disabled

Output volume55
Balance20

Integer Step

step quantises the drag and the arrow keys together, so a cell can only ever land on a value you would accept. Pair a fractional step with formatOptions — it shapes the visible number and the announced one at once, so the two cannot drift.

Grid columns4
Line height1.4

Secondary Group

Master82
Music64
Effects48
Dialogue25

Settings Group

Display

Tune the panel for the room you are in.

Brightness72
Keyboard backlight40
Contrast90

Variants

default45
secondary75

CSS Classes

The row part here is __track, not the __trigger the rest of the Cell family uses, because this row genuinely is the slider's track — the surface the drag is bound to — and naming it a trigger would misdescribe the element.

Base Classes

  • .cell-slider — The root. Declares every custom property below and unwinds the underlying slider's label/output/track grid to display: block, because a cell has none of those boxes — the label and the output live inside the track. Carries data-slot="cell-slider".
  • .cell-slider__track — The visible cell row, and the surface the drag is bound to. position: relative, full width, var(--cell-slider-height) tall, border: 1px solid var(--border), border-radius: var(--cell-slider-radius), background-color: var(--cell-slider-surface). Bordered-flat — a stroke and a fill, never elevation. overflow: hidden clips the fill to the rounded corners at every value. Takes cursor: ew-resize and user-select: none, since the row is the thing you drag.
  • .cell-slider__track--default — Sets --cell-slider-surface to --surface. The default.
  • .cell-slider__track--secondary — Sets --cell-slider-surface to --surface-secondary. One token apart from default, identical stroke — the same split the card family uses.

Element Classes

  • .cell-slider__fill — The tint from the start of the row to the current value. Absolutely positioned with inset-block: 0; its left and width are inline styles the underlying slider rewrites on every value change. The inline-start corners are rounded to calc(var(--cell-slider-radius) - 1px) so they sit inside the track's stroke; the inline-end edge stays square, which is what makes it read as a fill rather than a bar. pointer-events: none.
  • .cell-slider__label — The leading text, a <span>, pinned to var(--cell-slider-padding-inline) from the inline start and vertically centred. Truncates with an ellipsis at calc(55% - var(--cell-slider-padding-inline)).
  • .cell-slider__output — The trailing value, an <output>, pinned the same distance from the inline end. font-variant-numeric: tabular-nums and text-align: end, capped at calc(45% - var(--cell-slider-padding-inline) - var(--cell-slider-gap)). Those two caps are what keeps the pair from colliding at any width rather than at a breakpoint: the label can reach 55%, the output starts no earlier than 55% plus the gap, so the gap is preserved by construction.
  • .cell-slider__thumb — A transparent hit area, var(--cell-slider-thumb-width) wide and the full height of the row, holding the focusable <input type="range">. Nothing about it is painted; cursor: grab, and grabbing while [data-dragging="true"].
  • .cell-slider__thumb::after — The visible indicator, and the only part of the thumb you can see: a var(--cell-slider-indicator-width) × var(--cell-slider-indicator-height) pill, fully rounded, painted var(--cell-slider-indicator), no elevation.

Both .cell-slider__label and .cell-slider__output are pointer-events: none, so a press that lands on the text still reaches the track underneath and moves the value.

States

State is carried by data attributes the underlying slider already sets, not by extra classes.

  • [data-disabled="true"] on the root — dims the row, drops it out of the pointer path, and disables the input, which takes it out of the tab order.
  • [data-dragging="true"] on the thumb — grows the pill to scale: 1 1.35.
  • [data-focus-visible="true"] on the thumb — grows the pill the same way, and rings the row: outline: 2px solid var(--focus) at a 2px offset, applied to the track through :has(). The ring sits on the row rather than on the pill because at either limit the thumb centres on the track's own edge, so half of anything drawn there would fall inside the track's overflow: hidden. An outline is painted outside the border box and an element's overflow never clips its own outline, so this one survives at every value.
  • :hover on the track — raises --cell-slider-fill-strength from 28% to 36%. Colour only.

CSS Variables

All nine are declared on .cell-slider and inherited by every part, so overriding one on the root retunes the whole component.

  • --cell-slider-height — Row height. 44px.
  • --cell-slider-radius — Row corner radius. calc(var(--radius) * 2), so 16px at the default radius.
  • --cell-slider-padding-inline — How far the label and the output sit from their ends. 16px.
  • --cell-slider-gap — The minimum space held between the label and the output. 12px.
  • --cell-slider-font-size — Type size for both. 0.875rem.
  • --cell-slider-thumb-width — The transparent grab area. 28px.
  • --cell-slider-indicator-width / --cell-slider-indicator-height — The visible pill. 4px × 20px.
  • --cell-slider-fill-strength — How much accent the fill mixes in. 28% at rest, 36% on hover.
  • --cell-slider-surface / --cell-slider-fill / --cell-slider-indicator — The three paints. The surface comes from the variant. The fill is mixed from --accent at the strength above rather than taken from --accent-soft, which at its shipped strength measured 1.10:1 against the row in dark — here the tint is the value, not a backdrop. The indicator is --accent-soft-foreground, the ink token for content on an accent-soft surface; plain --accent measured 2.80:1 against the row in dark, under the 3:1 floor for non-text.

Motion

The value never animates. left and width on the fill and the thumb are inline styles React Aria rewrites on every pointer move, and easing them would drag the paint behind the finger — a drag is direct manipulation, and the only correct latency is none.

What does transition is the pill's press affordance, on the settled asymmetric timing: the grow on grab snaps (transition-duration: 0s), and only the release eases back over 120ms on var(--ease-out). A symmetric press lags under the finger.

The reduced-motion opt-out covers both halves — an explicit [data-reduce-motion="true"] on the component or any ancestor, and prefers-reduced-motion: reduce, the media half scoped so the two never double-apply. Under either, the pill still grows on grab; it just arrives in a single frame. The row's hover transition is colour-only and survives untouched, because there is no movement in it to remove.

API Reference

CellSlider

The root. Renders the underlying slider as a settings row and hands its parts the resolved variant. Also accepts every prop the @blakeui/react Slider takes except orientation, including minValue, maxValue, step, formatOptions, onChangeEnd, isDisabled and aria-label.

PropTypeDefaultDescription
childrenReactNodeA CellSlider.Track holding the fill, label, output and thumb. Required.
variant"default" | "secondary""default"Surface tone. The prop sits here, but the modifier class lands on the track — it is the only painted box in the component.
valuenumberThe current value, controlled. Pair with onChange.
defaultValuenumberThe starting value, uncontrolled.
onChange(value: number) => voidFires continuously while the value moves. Use onChangeEnd for the once-on-release call.
classNamestringClass name for the root.

CellSlider.Track

The visible cell row, and the surface the drag is bound to. Also accepts every native <div> attribute.

PropTypeDefaultDescription
childrenReactNodeThe row's contents — CellSlider.Fill, CellSlider.Label, CellSlider.Output and CellSlider.Thumb. Required.
classNamestringClass name for the track.

CellSlider.Fill

The accent tint from the start of the row to the current value. Always decorative — pointer-events: none, so it never intercepts a press.

PropTypeDefaultDescription
classNamestringClass name for the fill. Its left and width are written inline on every value change and cannot be overridden from CSS.

CellSlider.Label

The leading text, and the slider's accessible name. Renders a <span>. Also accepts every prop React Aria's Label takes except elementType.

PropTypeDefaultDescription
childrenReactNodeThe label text. Required — it is what names the slider. Leave the part out only if you put an aria-label on the root instead.
classNamestringClass name for the label.

CellSlider.Output

The trailing value, an <output> tied to the thumb. Also accepts every native <output> attribute.

PropTypeDefaultDescription
childrenReactNode | ((values) => ReactNode)the formatted valueLeft out, the output prints the same string the thumb announces as aria-valuetext, so the visible number and the spoken one cannot drift — change both at once with formatOptions on the root. Pass children only for something the formatter cannot say.
classNamestringClass name for the output.

CellSlider.Thumb

The transparent grab area whose ::after pill marks the value, and the element that holds the focusable <input type="range">. Also accepts every native <div> attribute.

PropTypeDefaultDescription
classNamestringClass name for the thumb. Its left and transform: translate(-50%, -50%) are written inline by React Aria — style the ::after pill rather than the box.
namestringName for the underlying input, so the value submits with a form.

On this page