Forms

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 base Switch's own <label> element, so it also carries .switch and .switch--md; this class only sets display: block, width: 100% and min-width: 0, which is what turns the compact inline switch into a full-width row. It paints nothing. The pointer cursor, the data-hovered / data-focus-visible / data-selected / data-disabled attributes, and the disabled treatment are all the base component's, untouched.

Element Classes

  • .cell-switch__trigger — The visible row, rendered by Switch.Content so it stays inside the label. display: flex in a row, align-items: center, gap: 12px, border-radius: 16px, 16px / 10px padding, and a 44px minimum 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 is background-color and border-color over 150ms on var(--ease-out) — colour only, so there is no movement to remove under reduced motion and no opt-out to write.
  • .cell-switch__trigger--default1px solid var(--border) over var(--surface). Bordered-flat: a stroke and a fill, never elevation.
  • .cell-switch__trigger--secondary — the same stroke over var(--surface-secondary).
  • .cell-switch__label — The leading text. 0.875rem / 1.25rem, font-weight: 500, color: var(--foreground), min-width: 0 so long labels can shrink rather than push the track out of the row.
  • .cell-switch__control — The track, rendered by Switch.Control, so it also carries .switch__control and everything that comes with it. This class adds margin-inline-start: auto — not justify-content: space-between on 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% of var(--foreground) into var(--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 :root block of @blakeui/styles and 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 .switch element, 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--default goes to var(--surface-hover), and the secondary row to var(--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) at var(--ring-offset-width) offset on .cell-switch__trigger. An outline rather than a ring, so it follows the row's border-radius and cannot collide with the base component's box-shadow ring — which is set to none inside a cell so the two never both draw.
  • Disabled is the base component's status-disabled: opacity: var(--disabled-opacity) and pointer-events: none on 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/reactmargin 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.

PropTypeDefaultDescription
childrenReactNodeThe 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.
isSelectedbooleanControlled selection, from the underlying Switch. Pair it with onChange.
defaultSelectedbooleanfalseUncontrolled starting selection.
onChange(isSelected: boolean) => voidFires whenever the row is toggled, from anywhere in it.
isDisabledbooleanfalseDims 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.
classNamestringClass 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.

PropTypeDefaultDescription
childrenReactNodeThe row's content — a CellSwitch.Label, a CellSwitch.Control, and whatever else it needs. Required.
classNamestringClass 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.

PropTypeDefaultDescription
childrenReactNodeThe label text. Required.
classNamestringClass name for the label.

CellSwitch.Control

The switch track — Switch.Control — pushed to the end of the row. Also accepts every native <span> attribute.

PropTypeDefaultDescription
childrenReactNodeSwitch.ThumbTrack content. A Switch.Thumb is supplied when this is empty, which is the usual case — pass children only to put something inside the thumb.
classNamestringClass name for the track.

On this page