Inline Select

PreviousNext

A value picked in place — at rest it is just the current value as a pill, one click opens the menu of options. The pill magic-moves when its row reorders.

Sorted by status. Change one — the row travels, and the pill leads it.

  • Billing webhooks
  • Onboarding flow
  • Auth rewrite
  • Search index
  • Design tokens

Installation

pnpm dlx shadcn@latest add https://matos-ui.com/r/inline-select.json

Usage

import { InlineSelect } from '@/components/matos-ui/inline-select'
<InlineSelect
  label="Status"
  value={task.status}
  onValueChange={(s) => updateStatus(task.id, s)}
  options={[
    { value: 'backlog', label: 'Backlog', pill: 'bg-muted text-muted-foreground' },
    { value: 'active', label: 'Active', pill: 'bg-primary/12 text-foreground' },
    { value: 'done', label: 'Done', pill: 'bg-secondary text-secondary-foreground' },
  ]}
/>

No field chrome — at rest it is the current value as a pill, with a chevron that shows on hover and focus. A click opens the menu; choosing an option commits and closes.

pill is a token-based class per option (bg-primary/10 text-foreground), not a loose colour — the pill tracks the palette customiser like any other surface.

Magic-move on reorder

The component's second reason: in a table sorted by status, changing one row's status re-sorts the shelf. The pill carries layout (and an optional layoutId), so while the row travels to its new place on spring.moderate, the pill flies there on spring.fast — it arrives first, the row catches up.

{sorted.map((row) => (
  <motion.li key={row.id} layout transition={spring.moderate}>
    <span>{row.title}</span>
    <InlineSelect
      label={`Status of ${row.title}`}
      value={row.status}
      onValueChange={(s) => setStatus(row.id, s)}
      options={STATUSES}
      layoutId={`status-${row.id}`}
    />
  </motion.li>
))}

Two layout animations at once, at two tiers. layoutId is what lets the pill survive a virtualised list (the row unmounts and remounts in a new place); without virtualisation, layout on the pill is enough.

Surface

The popup climbs two rungs from wherever the control sits and keeps a constant shadow weight (§2.5.5) — a menu of options reads as a menu at any depth. prefers-reduced-motion drops the pill travel and the popup's zoom; the crossfade stays.

Reference

InlineSelect Props

PropTypeDefaultDescription
valuestring
onValueChange(value: string) => void
optionsArray<{ value, label, pill?, disabled? }>pill is that option's pill class.
labelstringAccessible name for the control.
layoutIdstringA stable per-row id for the pill's magic-move.
placeholderReactNode"—"Shown when value matches no option.
disabledbooleanfalse
open / defaultOpen / onOpenChangePopup state, controlled or not.
side"top" | "bottom""bottom"
align"start" | "center" | "end""start"