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 withjustify-content: space-between,border-radius: calc(var(--radius) * 2), a1pxstroke andbox-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'sflex: 1withflex: 0 1 autoplusmargin-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: staticat1remsquare invar(--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 at20rem— 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—--surfacefill on a--borderstroke. The default..cell-select__trigger--secondary—--defaultfill on the same--borderstroke.
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 theidthe trigger'saria-labelledbypoints at.[data-own-label="true"]— On the root when anaria-labeloraria-labelledbywas 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The 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. |
selectedKey | Key | null | — | Controlled selection. Pair with onSelectionChange. |
defaultSelectedKey | Key | — | Uncontrolled starting selection. |
onSelectionChange | (key: Key | null) => void | — | Called with the newly selected key. |
isDisabled | boolean | false | Switches the whole row off — not focusable, does not open. |
disabledKeys | Iterable<Key> | — | Individual options that cannot be chosen, on a row that still opens. |
aria-label | string | — | Names the row outright, in place of CellSelect.Label. Required when the row has no visible label. |
className | string | — | Class 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Row content — CellSelect.Label, CellSelect.Value and CellSelect.Indicator. All three are optional. |
className | string | — | Class 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The label text. Required. |
className | string | — | Class 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((values) => ReactNode) | — | Omit for the selected item's text. Pass a function to draw the value yourself — it receives selectedItem, selectedText and isPlaceholder. |
className | string | — | Class 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | A replacement icon element. Omit for the built-in chevron. |
className | string | — | Class 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The options, as a CellSelect.List of CellSelect.Item. Give every item an id and a textValue. Required. |
placement | Placement | "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. |
className | string | — | Class 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.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The options, as CellSelect.Item elements. Required. |
className | string | — | Class name for the list, appended after .list-box. |
CellSelect.Item
BlakeUI's ListBox.Item, re-exported. One option.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | What the option renders. Required. |
id | Key | — | The option's key — what selectedKey and disabledKeys refer to. Required. |
textValue | string | — | The 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. |
className | string | — | Class name for the option, appended after .list-box-item. |