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.
pnpm dlx shadcn@latest add https://matos-ui.com/r/inline-select.jsonimport { 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.
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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | — |
onValueChange | (value: string) => void | — | — |
options | Array<{ value, label, pill?, disabled? }> | — | pill is that option's pill class. |
label | string | — | Accessible name for the control. |
layoutId | string | — | A stable per-row id for the pill's magic-move. |
placeholder | ReactNode | "—" | Shown when value matches no option. |
disabled | boolean | false | — |
open / defaultOpen / onOpenChange | — | — | Popup state, controlled or not. |
side | "top" | "bottom" | "bottom" | — |
align | "start" | "center" | "end" | "start" | — |
Install Matos UI
Choose a package manager and copy one command for every component.