Forms

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.

Usage

CellSelect wraps BlakeUI's Select and changes its shape, not its behaviour. Selection state, the hidden native select a form submits, the combobox semantics and the whole keyboard model stay the Select's; what this adds is the cell-row layout — label leading, value trailing, chevron in flow — and a popover that defaults to bottom end so the list drops under the value the reader was just looking at.

Give every option an id and a textValue. BlakeUI's list-box item composes an indicator around your children, so React Aria cannot infer the item's text from them: without textValue the popover's typeahead has nothing to search and the option a form submits carries an empty label. With it, typing m jumps to "Match system" and the native option reads "Match system".

Anatomy

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

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

<CellSelect>
  <CellSelect.Trigger>
    <CellSelect.Label />
    <CellSelect.Value />
    <CellSelect.Indicator />
  </CellSelect.Trigger>
  <CellSelect.Popover>
    <CellSelect.List>
      <CellSelect.Item />
    </CellSelect.List>
  </CellSelect.Popover>
</CellSelect>;

CellSelect.Trigger is the visible cell row and the part the variant modifier lands on. The three parts inside it are all optional — a row with no Label needs an aria-label on the root instead, because the label is where the trigger's accessible name comes from.

CellSelect.List and CellSelect.Item are BlakeUI's ListBox and ListBox.Item, re-exported unchanged. React Aria builds a select's collection through context, so the list and the trigger have to come from the same copy of @blakeui/react; taking both off CellSelect makes that automatic. Importing ListBox yourself is equivalent whenever your install resolves one copy, which a normal application does.

Controlled

Stored value: light

Pass selectedKey and onSelectionChange to own the value; pass defaultSelectedKey and let the component own it. Mixing the two makes the row read-only, which is a state no user can escape.

Custom Value

CellSelect.Value takes a render function — React Aria's own, forwarded untouched — so the trailing column can be anything. selectedItem is the object from your items array, so a status dot, a swatch or a formatted number all come from data you already have. isPlaceholder is what to branch on for the empty state.

Disabled

isDisabled on the root switches the whole row off: it drops out of the tab order and does not open. disabledKeys leaves the row live and switches individual options off, which is the right prop when a plan is unavailable rather than the setting being locked.

Text Size

BlakeUI ships a single typeface, so the font picker this section is usually built around would be picking between host-OS stacks rather than anything the design system owns. Text size is the same idea on an axis we do own: every option is set at the size it names, and so is the value, so the row previews the setting instead of describing it.

Settings Group

Cells stack into a settings list. The grouping is composition rather than a component: a bordered-flat Card owns the single stroke, divide-y draws the rules between rows, and each trigger drops its own border and radius so the panel reads as one surface. Put the dividers on the Card's children, not on the triggers — every trigger is the only child of its own root, so a last: rule there would match all four.

Variants

Both variants are bordered-flat — 1px solid var(--border) and no shadow — and differ only in fill: default is --surface, secondary is --default. The stroke is the same token on both, because --separator sits at 92% lightness against the secondary fill's 91.8% and would be a border in name only. The popover is the exception to the flatness: it genuinely floats, so it keeps --overlay-shadow.

Accessibility

The trigger is a real <button> with aria-haspopup="listbox" and aria-expanded, and its accessible name is composed from CellSelect.Label plus the current value — measured as "<value> <label>", which is exactly what the free Select produces from a real <Label>. The order is React Aria's, and this component matches it rather than forking the library's convention.

Keyboard: Enter opens the list with focus on the selected option, / move between options, typing jumps by first letter, Enter selects and closes, and Esc closes without changing the value. Focus returns to the trigger either way.

The popover's entrance and exit come from @blakeui/styles, including their reduced-motion behaviour. What this component adds is the chevron's flip, which under [data-reduce-motion="true"] or prefers-reduced-motion: reduce still points up when the list is open — only the animation between the two positions is dropped.

CSS Classes

Base Classes

  • .cell-select — The root hook, composed onto BlakeUI's .select. Turns the field's label-and-description column into a single full-width block: display: block, width: 100%, gap: 0.

Element Classes

  • .cell-select__trigger — The visible cell row, composed onto .select__trigger. A 44px-minimum flex row with justify-content: space-between, border-radius: calc(var(--radius) * 2), a 1px stroke and box-shadow: none. Drops the free trigger's reserved trailing padding, because the chevron here is laid out in flow rather than absolutely.
  • .cell-select__label — The leading text. flex-shrink: 0, medium weight, var(--foreground).
  • .cell-select__value — The trailing column, composed onto .select__value. Overrides the field's flex: 1 with flex: 0 1 auto plus margin-inline-start: auto, and truncates with an ellipsis rather than wrapping the row onto two lines.
  • .cell-select__indicator — The chevron, composed onto .select__indicator. position: static at 1rem square in var(--muted), so it sits at the end of the row instead of over it.
  • .cell-select__popover — The options panel, composed onto .select__popover. Sets only the width — min-width: 11rem, capped at 20rem — because the free rule pins the panel to the trigger's width, which for a full-width settings row is the whole row.

Modifier Classes

  • .cell-select__trigger--default--surface fill on a --border stroke. The default.
  • .cell-select__trigger--secondary--default fill on the same --border stroke.

Data Attributes

  • [data-slot="cell-select"] — The root, for styling from outside without depending on the BEM name.
  • [data-slot="cell-select-label"] — The label span, which also carries the id the trigger's aria-labelledby points at.
  • [data-own-label="true"] — On the root when an aria-label or aria-labelledby was supplied, so the label part is not the source of the accessible name.
  • The trigger, value and indicator keep the underlying Select's slots — [data-slot="select-trigger"], [data-slot="select-value"] and [data-slot="select-default-indicator"] (or [data-slot="select-indicator"] for a custom icon). Those are not renamed, because the free stylesheet sizes the built-in chevron off its slot.

API Reference

CellSelect

The root. Wraps BlakeUI Select and accepts every Select prop except fullWidth and variant, whose meanings this component replaces.

PropTypeDefaultDescription
childrenReactNodeThe cell's parts — CellSelect.Trigger and CellSelect.Popover. Required.
variant"default" | "secondary""default"Row treatment. Read on the root and applied to the trigger as a modifier class, since the trigger is the box that paints.
selectedKeyKey | nullControlled selection. Pair with onSelectionChange.
defaultSelectedKeyKeyUncontrolled starting selection.
onSelectionChange(key: Key | null) => voidCalled with the newly selected key.
isDisabledbooleanfalseSwitches the whole row off — not focusable, does not open.
disabledKeysIterable<Key>Individual options that cannot be chosen, on a row that still opens.
aria-labelstringNames the row outright, in place of CellSelect.Label. Required when the row has no visible label.
classNamestringClass name for the root, appended after .cell-select.

CellSelect.Trigger

The visible cell row. Renders the Select's trigger button, so it keeps the combobox semantics and the keyboard model, and accepts every prop that button takes.

PropTypeDefaultDescription
childrenReactNodeRow content — CellSelect.Label, CellSelect.Value and CellSelect.Indicator. All three are optional.
classNamestringClass name for the row, appended after the .cell-select__trigger classes. This is where a grouped panel flattens the row.

CellSelect.Label

The leading text, and the source of the trigger's accessible name. Renders a <span> and accepts every native <span> attribute except id, which the root owns.

PropTypeDefaultDescription
childrenReactNodeThe label text. Required.
classNamestringClass name for the label, appended after .cell-select__label.

CellSelect.Value

The current selection, in the trailing column. Renders the Select's value element and accepts every prop it takes.

PropTypeDefaultDescription
childrenReactNode | ((values) => ReactNode)Omit for the selected item's text. Pass a function to draw the value yourself — it receives selectedItem, selectedText and isPlaceholder.
classNamestringClass name for the value, appended after .cell-select__value.

CellSelect.Indicator

The chevron. Renders the Select's indicator, which supplies its own icon when given no children and clones a single element child with the indicator class applied.

PropTypeDefaultDescription
childrenReactNodeA replacement icon element. Omit for the built-in chevron.
classNamestringClass name for the indicator, appended after .cell-select__indicator.

CellSelect.Popover

The floating options panel. Renders the Select's popover and accepts every prop it takes, including offset and crossOffset.

PropTypeDefaultDescription
childrenReactNodeThe options, as a CellSelect.List of CellSelect.Item. Give every item an id and a textValue. Required.
placementPlacement"bottom end"Where the panel opens relative to the row. Defaults to the trailing edge, under the value. React Aria flips it when it would otherwise overflow.
classNamestringClass name for the popover, appended after .cell-select__popover.

CellSelect.List

BlakeUI's ListBox, re-exported so the options and the trigger come from one copy of the library. Every ListBox prop applies; the two below are the ones a cell uses.

PropTypeDefaultDescription
childrenReactNodeThe options, as CellSelect.Item elements. Required.
classNamestringClass name for the list, appended after .list-box.

CellSelect.Item

BlakeUI's ListBox.Item, re-exported. One option.

PropTypeDefaultDescription
childrenReactNodeWhat the option renders. Required.
idKeyThe option's key — what selectedKey and disabledKeys refer to. Required.
textValuestringThe option's text, for typeahead and for the value a form submits. Required — it cannot be inferred from children, because the item composes an indicator around them.
classNamestringClass name for the option, appended after .list-box-item.

On this page