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

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

Sections

With Descriptions

Disabled

Invalid

Sizes

In a Row of Text

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 and shadow-field all come off, min-height drops to 0 and vertical padding to 0, 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-shadow so the focus ring — which re-declares the composed box-shadow at 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 its svg off --inline-select-indicator-size.
  • .inline-select__value — The current selection, composed onto .select__value. Overrides the field's flex: 1 and fixed 14px with flex: 0 1 auto and inherited type — and inherited color, 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, because transform does 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: static at --inline-select-indicator-size in var(--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 at 20rem — 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:hover or [data-hovered="true"] on .inline-select__trigger. The value picks up a var(--muted) underline and the chevrons come up from --muted to --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 --accent measures 2.08:1 against the page.
  • Pressed:active or [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-visible or [data-focus-visible="true"]. The shared focus ring and nothing else; the free rule's field focus fill and border are overridden away.
  • Disabled:disabled or [data-disabled="true"]. status-disabled from 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-secondary fill, still no stroke, for a control in a toolbar row rather than in prose.
  • .inline-select__trigger--tertiary--surface-tertiary fill, one step further.
  • .inline-select__trigger--sm0.8125rem/1.125rem type. With no box to size, a size is a type scale: the control's height is its line box.
  • .inline-select__trigger--md0.875rem/1.25rem type.
  • .inline-select--sm / .inline-select--md — Re-declare --inline-select-indicator-size at 0.6875rem and 0.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. Default 12rem. 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's svg. Default 0.75rem; size="sm" re-declares it at 0.6875rem.
  • --inline-select-invalid-ink — The colour the value and chevrons take when isInvalid is set. Defaults to color-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"] — On InlineSelect.ItemLabel and InlineSelect.ItemDescription. React Aria's own list-box item slots, kept rather than renamed, so the free stylesheet's .list-box-item rules 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 when isInvalid is set. Turns the value and the chevrons --inline-select-invalid-ink and 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

PropTypeDefaultDescription
childrenReactNodeThe 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-labelstringNames 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.
selectedKeyKey | nullControlled selection. Pair with onSelectionChange. Mixing it with defaultSelectedKey makes the control read-only, which is a state no user can escape.
defaultSelectedKeyKeyUncontrolled starting selection.
onSelectionChange(key: Key | null) => voidCalled with the newly selected key.
placeholderstringWhat the value shows with nothing selected. Rendered in --muted via [data-placeholder="true"].
isDisabledbooleanfalseSwitches the whole control off — not focusable, does not open.
disabledKeysIterable<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.
isInvalidbooleanfalseMarks 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.
namestringNames the hidden native select a form submits. The submitted value is the option's textValue.
classNamestringClass name for the root, appended after .inline-select.

InlineSelect.Trigger

PropTypeDefaultDescription
childrenReactNodeControl content — InlineSelect.Icon, InlineSelect.Value and InlineSelect.Indicator. All three are optional.
classNamestringClass 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

PropTypeDefaultDescription
childrenReactNodeThe 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.
classNamestringClass 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

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. Either way the result is wrapped in the animated span, so a custom value cross-fades on change like the default one.
classNamestringClass name for the value, appended after .inline-select__value.

InlineSelect.Indicator

PropTypeDefaultDescription
childrenReactNodeA 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.
classNamestringClass name for the indicator, appended after .inline-select__indicator.

InlineSelect.Popover

PropTypeDefaultDescription
childrenReactNodeThe 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.
placementPlacement"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.
classNamestringClass name for the popover, appended after .inline-select__popover.

InlineSelect.List

PropTypeDefaultDescription
childrenReactNode | ((item) => ReactNode)The options, as InlineSelect.Item or InlineSelect.Section elements — or a render function when items is supplied. Required.
itemsIterable<T>Data to render, paired with a render function as children.
classNamestringClass 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

PropTypeDefaultDescription
childrenReactNodeWhat the option renders. Required. The trigger's value renders the selected option's children, so an InlineSelect.ItemDescription inside them is hidden there.
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. Without it, typing jumps nowhere and the submitted option carries an empty label.
classNamestringClass name for the option, appended after .list-box-item.

InlineSelect.ItemIndicator

PropTypeDefaultDescription
childrenReactNodeA replacement mark. Omit for the built-in check, which is the only selected-state affordance the option has. Hidden inside the trigger's value.
classNamestringClass name for the mark, appended after .list-box-item__indicator.

InlineSelect.Section

PropTypeDefaultDescription
childrenReactNodeAn InlineSelect.SectionHeader followed by the group's InlineSelect.Item elements. Required.
classNamestringClass name for the group, appended after .list-box-section.

InlineSelect.SectionHeader

PropTypeDefaultDescription
childrenReactNodeThe group's title. Required — it is what names the section for a screen reader, not decoration that can be dropped.
classNamestringClass name for the title, appended after .header.

InlineSelect.ItemLabel

PropTypeDefaultDescription
childrenReactNodeThe 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.
classNamestringClass name for the title. Renders a <span> carrying [data-slot="label"], which is what the list-box item styles it from.

InlineSelect.ItemDescription

PropTypeDefaultDescription
childrenReactNodeThe 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.
classNamestringClass name for the description, appended after .description.

On this page