Forms
Inline Select
A minimal select that blends into surrounding text — no stroke, no fill, sized by its own type scale — for a contextual choice that does not need a form field.
Usage
Mobile
Tablet
Desktop
Anatomy
Import the InlineSelect component and access all parts using dot notation.
import {InlineSelect} from "@blakeui/pro-react";
<InlineSelect aria-label="Date range">
<InlineSelect.Trigger>
<InlineSelect.Icon />
<InlineSelect.Value />
<InlineSelect.Indicator />
</InlineSelect.Trigger>
<InlineSelect.Popover>
<InlineSelect.List>
<InlineSelect.Section>
<InlineSelect.SectionHeader />
<InlineSelect.Item>
<InlineSelect.ItemLabel />
<InlineSelect.ItemDescription />
<InlineSelect.ItemIndicator />
</InlineSelect.Item>
</InlineSelect.Section>
</InlineSelect.List>
</InlineSelect.Popover>
</InlineSelect>;With Icon
Mobile
Tablet
Desktop
Sections
Mobile
Tablet
Desktop
With Descriptions
Mobile
Tablet
Desktop
Disabled
Mobile
Tablet
Desktop
Invalid
Mobile
Tablet
Desktop
Sizes
Mobile
Tablet
Desktop
In a Row of Text
Mobile
Tablet
Desktop
Send the activity digest to
every
, and never on weekends.
CSS Classes
Base Classes
.inline-select— The root hook, composed onto BlakeUI's.select. Turns the field's full-width label-and-description column into an inline-level box that shrink-wraps its trigger:display: inline-flex,width: auto,max-width: 100%,gap: 0. Declares both custom properties below.
Element Classes
.inline-select__trigger— The visible control, composed onto.select__trigger. Ghost: the field's stroke, fill andshadow-fieldall come off,min-heightdrops to0and vertical padding to0, so the control is exactly as tall as its own line box. Drops the free trigger's reserved trailing padding, because the chevrons here are laid out in flow rather than absolutely, and empties--tw-shadowso the focus ring — which re-declares the composedbox-shadowat a higher specificity — cannot bring the field's drop shadow back with it. Declares--inline-select-fill, which is the one thing a variant changes..inline-select__icon— The leading icon slot.inline-flex,flex-shrink: 0,var(--muted); sizes itssvgoff--inline-select-indicator-size..inline-select__value— The current selection, composed onto.select__value. Overrides the field'sflex: 1and fixed 14px withflex: 0 1 autoand inherited type — and inheritedcolor, which is what carries the trigger's hover, open and invalid ink through to the words. Capped at--inline-select-value-max-width. Hides any[data-slot="description"]inside it, the same way the free stylesheet hides the option's own check indicator there..inline-select__value-text— The animated text inside the value. A block, not an inline span, becausetransformdoes not apply to a non-replaced inline box. Owns the truncation:overflow: hidden,text-overflow: ellipsis,white-space: nowrap..inline-select__indicator— The stacked up/down chevrons, composed onto.select__indicator.position: staticat--inline-select-indicator-sizeinvar(--muted), so it sits at the end of the words instead of over a field box; follows the trigger's ink on hover and while open..inline-select__popover— The options panel, composed onto.select__popover. The one part of the component that is still a box, so it keeps--overlay-shadow. Sets only width and the option type scale —min-width: 11rem, capped at20rem— because the free rule pins the panel to the trigger's width, and a ghost trigger is as narrow as the word inside it.
Interactive States
- Hover —
:hoveror[data-hovered="true"]on.inline-select__trigger. The value picks up avar(--muted)underline and the chevrons come up from--mutedto--foreground. No fill appears: a wash under the words would put back the box the resting state just removed. The value's own colour does not move, which is deliberate — BlakeUI sets--link: var(--foreground)in both themes, and a hue here would fail in the dark, where--accentmeasures 2.08:1 against the page. - Pressed —
:activeor[data-pressed="true"]. The press-in snaps (transition-duration: 0s) and only the release eases back. - Open —
[aria-expanded="true"]on the trigger holds the hover treatment while the panel is up.[data-open="true"]lands on the indicator, which is where a replacement single chevron would take the free half-turn. - Focus visible —
:focus-visibleor[data-focus-visible="true"]. The shared focus ring and nothing else; the free rule's field focus fill and border are overridden away. - Disabled —
:disabledor[data-disabled="true"].status-disabledfrom the free stylesheet: reduced opacity,pointer-events: none.
Modifier Classes
.inline-select__trigger--default— No fill. The ghost, and the one the component exists for..inline-select__trigger--secondary—--surface-secondaryfill, still no stroke, for a control in a toolbar row rather than in prose..inline-select__trigger--tertiary—--surface-tertiaryfill, one step further..inline-select__trigger--sm—0.8125rem/1.125remtype. With no box to size, a size is a type scale: the control's height is its line box..inline-select__trigger--md—0.875rem/1.25remtype..inline-select--sm/.inline-select--md— Re-declare--inline-select-indicator-sizeat0.6875remand0.75rem, on the root so one utility class can override them..inline-select__popover--sm/.inline-select__popover--md— Option type scale, matched to the trigger. The panel is portaled out of the root, so it carries its own copy of the size modifier.
CSS Variables
--inline-select-value-max-width— How wide the value may get before it ellipsizes. Default12rem. This is what keeps a long option from stretching the line it sits in.--inline-select-indicator-size— Square size of the chevrons, and of the leading icon'ssvg. Default0.75rem;size="sm"re-declares it at0.6875rem.--inline-select-invalid-ink— The colour the value and chevrons take whenisInvalidis set. Defaults tocolor-mix(in oklab, var(--danger) 55%, var(--foreground) 45%)rather than raw--danger, which is a fill token: identical in both themes and meant to sit under--danger-foreground. Read as text it measures 1.89:1 against the dark page; the mix is theme-aware from one declaration and lands at 8.84:1 light, 5.01:1 dark.
Data Attributes
[data-slot="inline-select"]— The root, for styling from outside without depending on the BEM name.[data-slot="inline-select-icon"]— The leading icon slot.[data-slot="inline-select-value-text"]— The animated span inside the value.[data-slot="label"]/[data-slot="description"]— OnInlineSelect.ItemLabelandInlineSelect.ItemDescription. React Aria's own list-box item slots, kept rather than renamed, so the free stylesheet's.list-box-itemrules keep styling them.[aria-expanded="true"]— On the trigger while its list is open, which is what holds the hover treatment. React Aria's own attribute; no extra state is tracked for it.[data-invalid="true"]— On the root whenisInvalidis set. Turns the value and the chevrons--inline-select-invalid-inkand drops the free rule's 1px danger outline — an outline is the box this component spent its whole stylesheet removing.- 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
InlineSelect
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The control's parts — InlineSelect.Trigger and InlineSelect.Popover. Required. The root renders a <div>, and so does the visually-hidden native select React Aria puts inside it for form submission — so a sentence hosting one has to be a <div> rather than a <p>, which cannot contain either. |
aria-label | string | — | Names the control. Required — there is no visible label part, because an inline select takes its meaning from the row or sentence it sits in, and without this the trigger's only accessible name is the current value. Use aria-labelledby instead when the name is already on the page. |
variant | "default" | "secondary" | "tertiary" | "default" | Fill treatment. default paints nothing at all — that is the ghost this component exists for. secondary and tertiary add a soft --surface-secondary / --surface-tertiary fill for a control in a toolbar row rather than in prose. None of the three carries a stroke or a shadow. Read on the root and applied to the trigger as a modifier class, since the trigger is the box that paints. |
size | "sm" | "md" | "md" | Control scale. With no box to size, a size is a type scale — sm is 13px text with 11px chevrons, md is 14px with 12px — and the control's height is whatever its line box is. Lands on the trigger, on the root (where it re-declares --inline-select-indicator-size), and on the popover, which is portaled out of the root and would otherwise open a md list under a sm control. |
selectedKey | Key | null | — | Controlled selection. Pair with onSelectionChange. Mixing it with defaultSelectedKey makes the control read-only, which is a state no user can escape. |
defaultSelectedKey | Key | — | Uncontrolled starting selection. |
onSelectionChange | (key: Key | null) => void | — | Called with the newly selected key. |
placeholder | string | — | What the value shows with nothing selected. Rendered in --muted via [data-placeholder="true"]. |
isDisabled | boolean | false | Switches the whole control off — not focusable, does not open. |
disabledKeys | Iterable<Key> | — | Individual options that cannot be chosen, on a control that still opens. This is the prop for an option that is unavailable, rather than the control being locked. |
isInvalid | boolean | false | Marks the value invalid. Turns the value and the chevrons --inline-select-invalid-ink rather than drawing a danger box, since a ghost control has no stroke to turn. The signal is visual: React Aria puts [data-invalid] on the root, and the free Select does not reach the trigger button with aria-invalid, so pair this with your own error text rather than relying on it to announce. |
name | string | — | Names the hidden native select a form submits. The submitted value is the option's textValue. |
className | string | — | Class name for the root, appended after .inline-select. |
InlineSelect.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Control content — InlineSelect.Icon, InlineSelect.Value and InlineSelect.Indicator. All three are optional. |
className | string | — | Class name for the trigger, appended after the .inline-select__trigger classes. Renders the Select's trigger button, so it keeps the combobox semantics and the keyboard model, and accepts every prop that button takes. |
InlineSelect.Icon
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The icon element. Required. The slot sizes its svg off --inline-select-indicator-size, so do not pass a size class unless you mean to override that. |
className | string | — | Class name for the icon, appended after .inline-select__icon. Renders a <span> and is aria-hidden, because the icon restates the value rather than adding to it. |
InlineSelect.Value
| 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. Either way the result is wrapped in the animated span, so a custom value cross-fades on change like the default one. |
className | string | — | Class name for the value, appended after .inline-select__value. |
InlineSelect.Indicator
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | A replacement icon element, cloned with the indicator class applied. Omit for the stacked up/down chevrons, which read as "this word can change" where the field's single chevron-down reads as "this box opens downward". Supplying a directional icon brings back the free [data-open] half-turn, invisible on the symmetric default. |
className | string | — | Class name for the indicator, appended after .inline-select__indicator. |
InlineSelect.Popover
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The options, as an InlineSelect.List. Required. The panel genuinely floats, so it is the one part of this component that keeps --overlay-shadow; its enter and exit animation, and their reduced-motion behaviour, come from @blakeui/styles. |
placement | Placement | "bottom start" | Where the panel opens relative to the control. Defaults to the leading edge, under where the reader's eye already is. React Aria flips it when it would otherwise overflow. |
className | string | — | Class name for the popover, appended after .inline-select__popover. |
InlineSelect.List
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((item) => ReactNode) | — | The options, as InlineSelect.Item or InlineSelect.Section elements — or a render function when items is supplied. Required. |
items | Iterable<T> | — | Data to render, paired with a render function as children. |
className | string | — | Class name for the list, appended after .list-box. This is BlakeUI's ListBox, re-exported: React Aria builds a select's collection through context, so every collection part has to come from the same copy of @blakeui/react as the trigger, and taking them off InlineSelect makes that automatic. |
InlineSelect.Item
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | What the option renders. Required. The trigger's value renders the selected option's children, so an InlineSelect.ItemDescription inside them is hidden there. |
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. Without it, typing jumps nowhere and the submitted option carries an empty label. |
className | string | — | Class name for the option, appended after .list-box-item. |
InlineSelect.ItemIndicator
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | A replacement mark. Omit for the built-in check, which is the only selected-state affordance the option has. Hidden inside the trigger's value. |
className | string | — | Class name for the mark, appended after .list-box-item__indicator. |
InlineSelect.Section
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | An InlineSelect.SectionHeader followed by the group's InlineSelect.Item elements. Required. |
className | string | — | Class name for the group, appended after .list-box-section. |
InlineSelect.SectionHeader
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The group's title. Required — it is what names the section for a screen reader, not decoration that can be dropped. |
className | string | — | Class name for the title, appended after .header. |
InlineSelect.ItemLabel
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The option's title, and what names the option. Required alongside InlineSelect.ItemDescription and pointless without one: an option carrying a description but no label has no aria-labelledby, so its accessible name falls back to its whole subtree — and the description, already pointed at by aria-describedby, is read twice. |
className | string | — | Class name for the title. Renders a <span> carrying [data-slot="label"], which is what the list-box item styles it from. |
InlineSelect.ItemDescription
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The option's second line. Required. React Aria points the option's aria-describedby at it, so it is announced after the option's text rather than being lost. |
className | string | — | Class name for the description, appended after .description. |
Drop Zone
A file drop target with a browse affordance, built on React Aria's DropZone and FileTrigger — pointer drag, keyboard drag and the file picker all reach the same list.
Native Select
A form field built on a real <select> — the platform's own menu on a phone, the platform's keyboard model everywhere, and no JavaScript needed to make it accessible.