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.
Usage
CellSwitch is the row you build a settings screen out of: a label on the left, a switch on the
right, and a border around both. It wraps the Switch
from @blakeui/react rather than reimplementing one, so selection, form posting, disabled and
read-only states, and the accessible name are all that component's, unchanged.
The whole row toggles — the label text, the gap in the middle, the padding at the far edge. That is
the point of the component, and it is not a click handler: the underlying Switch renders a real
<label> around a hidden <input role="switch">, and this component only lays that label out as a
full-width row. Nothing is intercepting the click, which is why the target never falls out of sync
with the control.
The API is composable rather than a single component with props, the same exception the Legend and the Timeline take. A settings row carries a label, and often an icon, a chip, a second line of copy, or a shortcut hint; props cannot express that arrangement, and children can.
Anatomy
Import the CellSwitch component and access all parts using dot notation.
import {CellSwitch} from "@blakeui/pro-react";
<CellSwitch>
<CellSwitch.Trigger>
<CellSwitch.Label />
<CellSwitch.Control />
</CellSwitch.Trigger>
</CellSwitch>;CellSwitch.Control renders the default thumb when you leave it empty, so a plain cell needs nothing
inside it. Everything else in the row is yours: put an icon before the label, a chip beside it, or a
second line of copy under it, and it keeps its own semantics.
Controlled
Previews follow your notification setting.
Disabled
Feature Announcement
A row with an icon, a chip and supporting copy under the label. Because the label wraps the input,
the accessible name would otherwise be the row's entire text — the aria-label on the root is what
keeps it to the feature's name.
Secondary Group
Settings Group
Sync and delivery
Variants
CSS Classes
Base Classes
.cell-switch— The root. It is the baseSwitch's own<label>element, so it also carries.switchand.switch--md; this class only setsdisplay: block,width: 100%andmin-width: 0, which is what turns the compact inline switch into a full-width row. It paints nothing. The pointer cursor, thedata-hovered/data-focus-visible/data-selected/data-disabledattributes, and the disabled treatment are all the base component's, untouched.
Element Classes
.cell-switch__trigger— The visible row, rendered bySwitch.Contentso it stays inside the label.display: flexin a row,align-items: center,gap: 12px,border-radius: 16px,16px/10pxpadding, and a44pxminimum height — the Cell family's row height, since a settings row is a touch target rather than a text field. The floor rather than the padding sets it, so a row that grows a second line of copy still lays itself out. Overrides.switch__content's column stack. Its transition isbackground-colorandborder-colorover 150ms onvar(--ease-out)— colour only, so there is no movement to remove under reduced motion and no opt-out to write..cell-switch__trigger--default—1px solid var(--border)overvar(--surface). Bordered-flat: a stroke and a fill, never elevation..cell-switch__trigger--secondary— the same stroke overvar(--surface-secondary)..cell-switch__label— The leading text.0.875rem/1.25rem,font-weight: 500,color: var(--foreground),min-width: 0so long labels can shrink rather than push the track out of the row..cell-switch__control— The track, rendered bySwitch.Control, so it also carries.switch__controland everything that comes with it. This class addsmargin-inline-start: auto— notjustify-content: space-betweenon the row, so the track still sits at the end when the row grows a third child..cell-switch__control--default— Restates the base track fill,var(--default), together with the hover and pressed values derived from it..cell-switch__control--secondary— Retunes the off-state track for the tinted row, mixing 12% ofvar(--foreground)intovar(--surface-secondary). Mixing the row's fill with the ink on it is what makes the step a darkening in light and a lightening in dark without either value being written per theme. It is deliberately not--surface-secondary-foreground, which reads as the better-matched pair but is declared only in the:rootblock of@blakeui/stylesand never restated for dark — so it is substituted once at the document root and inherited as that literal colour, and under a theme scoped to a subtree it would still hold the light ink. The base component derives its hover and pressed fills at the.switchelement, so both are restated here too — they were computed upstream and do not follow a descendant's override on their own.
Hover, focus and disabled
- Hover is on the row, because the row is the hit target:
.cell-switch:hover .cell-switch__trigger--defaultgoes tovar(--surface-hover), and the secondary row tovar(--surface-tertiary). Both selectors are paired with a[data-hovered="true"]half so the treatment holds for a plain pointer as well as for React Aria's own state. - Focus is a single indicator, and it surrounds the row rather than the 40px track:
outline: 2px solid var(--focus)atvar(--ring-offset-width)offset on.cell-switch__trigger. Anoutlinerather than a ring, so it follows the row'sborder-radiusand cannot collide with the base component's box-shadow ring — which is set tononeinside a cell so the two never both draw. - Disabled is the base component's
status-disabled:opacity: var(--disabled-opacity)andpointer-events: noneon the root. Since the root is the row, the entire cell leaves the hit path — the label and the padding stop responding along with the control.
Motion
The thumb's travel comes from @blakeui/react — margin 300ms var(--ease-out-fluid) plus
background-color 200ms var(--ease-out) — and so does its reduced-motion opt-out, which ships both
required halves: an explicit [data-reduce-motion="true"] selector on the element or any ancestor,
and an @media (prefers-reduced-motion: reduce) rule scoped
:not(:is([data-reduce-motion="true"], [data-reduce-motion="true"] *)) so the two never
double-apply. Under either the travel is dropped to transition-property: none. None of this is
reimplemented here; a change to it is a version bump of the free package, not an edit to this
component.
API Reference
CellSwitch
The root. Renders the Switch from @blakeui/react — a <label> wrapping a hidden
<input role="switch"> — and accepts every one of its props, including isSelected,
defaultSelected, onChange, isDisabled, isReadOnly, size, name and value, plus the usual
aria-label / aria-labelledby.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The cell's parts — normally a single CellSwitch.Trigger. Required. |
variant | "default" | "secondary" | "default" | Surface treatment of the row. Painted by CellSwitch.Trigger, which is what carries the modifier — the root itself paints nothing. |
isSelected | boolean | — | Controlled selection, from the underlying Switch. Pair it with onChange. |
defaultSelected | boolean | false | Uncontrolled starting selection. |
onChange | (isSelected: boolean) => void | — | Fires whenever the row is toggled, from anywhere in it. |
isDisabled | boolean | false | Dims the cell and sets pointer-events: none on the root — so the whole row leaves the hit path, not just the control. |
size | "sm" | "md" | "lg" | "md" | Track and thumb size, passed through to the underlying Switch. It does not change the row's padding or type scale. |
className | string | — | Class name for the root label. |
CellSwitch.Trigger
The visible cell row, and the part that carries the variant modifier. Renders Switch.Content, so it
sits inside the root label — which is what keeps the whole row clickable. It is not a control in its
own right: no role, no tabindex, no handler. Also accepts every native <div> attribute.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The row's content — a CellSwitch.Label, a CellSwitch.Control, and whatever else it needs. Required. |
className | string | — | Class name for the row. This is where a stacked panel drops the per-row stroke with rounded-none border-0. |
CellSwitch.Label
The leading text, as a <span> — deliberately not a second <label>, since the root already is one.
Because the input is wrapped, this span's text becomes the switch's accessible name. Also accepts
every native <span> attribute.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The label text. Required. |
className | string | — | Class name for the label. |
CellSwitch.Control
The switch track — Switch.Control — pushed to the end of the row. Also accepts every native
<span> attribute.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | Switch.Thumb | Track content. A Switch.Thumb is supplied when this is empty, which is the usual case — pass children only to put something inside the thumb. |
className | string | — | Class name for the track. |
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.
Checkbox Button Group
A multi-select group whose options are cards, built on a real checkbox group with keyboard selection and a per-item indicator.