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
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.
Disabled
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.
Secondary Group
Settings Group
Display
Tune the panel for the room you are in.
Variants
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 todisplay: block, because a cell has none of those boxes — the label and the output live inside the track. Carriesdata-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: hiddenclips the fill to the rounded corners at every value. Takescursor: ew-resizeanduser-select: none, since the row is the thing you drag..cell-slider__track--default— Sets--cell-slider-surfaceto--surface. The default..cell-slider__track--secondary— Sets--cell-slider-surfaceto--surface-secondary. One token apart fromdefault, 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 withinset-block: 0; itsleftandwidthare inline styles the underlying slider rewrites on every value change. The inline-start corners are rounded tocalc(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 tovar(--cell-slider-padding-inline)from the inline start and vertically centred. Truncates with an ellipsis atcalc(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-numsandtext-align: end, capped atcalc(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, andgrabbingwhile[data-dragging="true"]..cell-slider__thumb::after— The visible indicator, and the only part of the thumb you can see: avar(--cell-slider-indicator-width)×var(--cell-slider-indicator-height)pill, fully rounded, paintedvar(--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 toscale: 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'soverflow: 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.:hoveron the track — raises--cell-slider-fill-strengthfrom 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), so16pxat 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--accentat 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--accentmeasured 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | A 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. |
value | number | — | The current value, controlled. Pair with onChange. |
defaultValue | number | — | The starting value, uncontrolled. |
onChange | (value: number) => void | — | Fires continuously while the value moves. Use onChangeEnd for the once-on-release call. |
className | string | — | Class name for the root. |
CellSlider.Track
The visible cell row, and the surface the drag is bound to. Also accepts every native <div>
attribute.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The row's contents — CellSlider.Fill, CellSlider.Label, CellSlider.Output and CellSlider.Thumb. Required. |
className | string | — | Class 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.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Class 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The label text. Required — it is what names the slider. Leave the part out only if you put an aria-label on the root instead. |
className | string | — | Class name for the label. |
CellSlider.Output
The trailing value, an <output> tied to the thumb. Also accepts every native <output> attribute.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((values) => ReactNode) | the formatted value | Left 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. |
className | string | — | Class 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.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Class 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. |
name | string | — | Name for the underlying input, so the value submits with a form. |
Cell Select
A select dressed as a settings row — a leading label, the current value at the trailing edge, and a list that opens under the value rather than under the label.
Cell Switch
A settings-row toggle built on the Switch — the whole row is the hit target, not just the control, and cells stack into a settings panel.