BadgeSelect
A single-value pick-to-change control styled as a Badge — a tinted lozenge whose tone follows the selected option, opening a single-select list to change the value.
@garn/ui/badge-selectDefault
A status picker — the trigger tone follows the selected value.
Tone × appearance
Every tone rendered against every appearance. Hover a cell for the data attributes it emits; click the copy icon for that combination's JSX.
| soft | outline | framed | |
|---|---|---|---|
| neutral | |||
| brand | |||
| info | |||
| success | |||
| warning | |||
| danger | |||
| discovery |
Shapes
sharp · rounded · pill.
Sizes
sm · md.
Leading
Custom leading icons (priority) mirrored into the trigger.
Count
Per-option `count` tallies (tone-matched Counters), mirrored into the trigger.
Assignee
Avatars as leading content — an assignee picker.
Grouped
Sectioned options (BadgeSelectGroup / Label / Separator).
Loading
A pending change — a Spinner in the trigger, disabled until done.
Options Prop
Data-driven options via the `options` prop.
Placeholder Disabled
A placeholder (unset), a disabled control, and a disabled option.
Data Rich
A status cell per row in a data-rich table — the core use case.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add badge-select- Registry
badge-select- Source
- badge-select.tsx
- Deps
class-variance-authority@radix-ui/react-selectlucide-react- Registry deps
utilsuse-controllable-stateselectcounterspinner
Import
import { BadgeSelect } from "@garn/ui/badge-select";Props
Generated from the component's TypeScript types.
appearance- Description
- No description.
- Type
"framed" | "outline" | "soft"- Default
soft
shape- Description
- No description.
- Type
"pill" | "rounded" | "sharp"- Default
rounded
size- Description
- No description.
- Type
"md" | "sm"- Default
md
tone- Description
- No description.
- Type
"brand" | "danger" | "discovery" | "info" | "neutral" | "success" | "warning"- Default
neutral
"aria-label"- Description
- No description.
- Type
string
"aria-labelledby"- Description
- No description.
- Type
string
children- Description
- BadgeSelectItem children.
- Type
React.ReactNode
className- Description
- Applied to the trigger (the visible root).
- Type
string
contentClassName- Description
- Applied to the popup content.
- Type
string
id- Description
- No description.
- Type
string
loading- Description
- Pending state: a spinner replaces the trigger leading and the control is disabled.
- Type
boolean
options- Description
- Data-driven options — an alternative to compound children (one or the other).
- Type
BadgeSelectOption[]
placeholder- Description
- Shown when nothing is selected.
- Type
React.ReactNode
ref- Description
- No description.
- Type
React.Ref<HTMLButtonElement>
Plus 14 inherited native <button> attributes.
BadgeSelectItem
children- Description
- No description.
- Type
React.ReactNode
count- Description
- A trailing count, rendered as a tone-matched Counter. Row only.
- Type
number
leading- Description
- Leading content — icon / avatar / badge / dot. Mirrors into the trigger when selected.
- Type
React.ReactNode
tone- Description
- Tone the trigger mirrors when this option is selected; also colours the row dot.
- Type
BadgeSelectTone
trailing- Description
- Trailing content (Counter / Badge / Kbd …). Shown in the row only, not the trigger.
- Type
React.ReactNode
valueRequired- Description
- Stable value for this option (required).
- Type
string
Plus 282 inherited native HTML attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="badge-select-trigger"][data-slot="badge-select-trigger-leading"][data-slot="badge-select-trigger-label"][data-slot="badge-select-trigger-count"]| Property | Token | Tier |
|---|---|---|
fill | --garn-badge-brand-bgbrandable | semantic |
Private instance vars (never externalize): --t-soft-bg
Overview
BadgeSelect is a single value you pick that reads as a badge — a status / priority / category / assignee cell you edit in place on a table, board, or detail view. The trigger is a tinted lozenge whose tone follows the chosen value; opening it reveals BadgeSelectItems (each with a tone dot), optionally organized with Group/Label/Separator. It's the editable cousin of Badge: use it when the badge's value is set by the user, Select when the control is a normal field, and Badge when the value is read-only.
Guidelines
Use it where a badge needs to become editable in place. It shines as an inline-edit cell — a status or priority you click to change without leaving the row. If the field belongs in a form with a label, a normal Select reads better.
Let the chosen tone mirror its meaning. Give each option a tone that matches its semantics (done = success, blocked = danger) so the resting lozenge communicates state at a glance, and keep that mapping consistent across the app.
Keep the option set short and named. It's a pick-one from a small, known set; for a long or searchable list use a Combobox. Ensure the trigger has an accessible name and that selection is fully keyboard-operable.
Best practices
- Give the trigger an accessible name (aria-label / aria-labelledby, or a <label>).
- Set a
toneon each option so the trigger colour follows the selected value. - Use the badge family tone vocabulary so a status reads the same across Badge / Counter / Tag / BadgeSelect.
- Use
countfor a per-option tally (a tone-matched Counter), shown in the row and mirrored to the trigger. - Use
namefor form submission, andloadingfor a pending async change.
- Don't use it for multi-select or removable chips — that's Tag / TagGroup.
- Don't rely on the placeholder as the accessible name.
- Don't reach for raw colors; tone resolves through the --garn-badge-* tokens (edit dark mode in the token layer).
- Don't use it for a long list that needs search — compose Select / Command instead.
Content guidelines
- Keep option labels short — they become the trigger lozenge.
- Prefer a leading dot/icon (or tone) to distinguish options over color alone.
Accessibility
- Role
combobox- Focus
- 2px ring in --garn-ring on the trigger; focus and the listbox roving are managed by Radix Select.
Enforceable contracts
Give the trigger an accessible name (aria-label / aria-labelledby, or a <label htmlFor> matching the trigger id). The placeholder doesn't count.
when the BadgeSelect trigger has no `aria-label`, `aria-labelledby`, or `id` (for an external <label htmlFor>)
- • The trigger is a combobox (Radix Select.Trigger) that conveys the current value in its collapsed state — listbox, not menu, semantics — so it suits a control whose job is to display a value.
- • Opening exposes a role=listbox of role=option items; the selected option carries aria-selected and a trailing check. Single-select only.
- • Keyboard (inherited from Select): Enter / Space / ArrowUp / ArrowDown and typeahead open and move; Enter / Space pick; Esc closes; Home / End jump.
- • Give the trigger an accessible name via `aria-label`, `aria-labelledby`, or a <label htmlFor> matching the trigger `id` — the placeholder is not a name. The selected value is the trigger's text content.
- • Setting `name` submits the value via a hidden native <select> (form integration, inherited from Select).
- Status
- Stable
- Version
v0.3.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.