Unit Switch
betaThe unit beside a value, like £, min, or km, that you can change in place. Click it for a menu of units. On a change the unit crossfades to the new one, and a note can say what changed.
Preview
currency
Ticket price6.00
stepper
size
custom
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/unit-switch
Use it for
- A value whose unit people may want to change: a fee in pounds or dollars, a length in minutes or hours, a distance in km or miles.
- Pass a set name as units: "currency", "duration", "distance", or "weight". Or pass your own list of ids and options.
- A currency's id is its code, like "GBP", so you can pass it straight to Intl.NumberFormat.
- Put it in a NumberStepper's suffix for a count with a unit. Beside plain text, put it before or after the value, as its place says.
- Give announce to say what changed, like { text: "Pounds changed to dollars", key: 3 }. Change the key to say it again.
- matchUnit reads a typed value, like "2 hours", and returns the unit in it. Use it to switch the unit when someone types one.
Avoid it for
- A unit that never changes. Write it as text, or use NumberStepper's suffix.
- Picking anything that is not a unit. Use a select or a menu.
Props
- units
A built-in set, or your own list. A string in a list is a built-in id, like "mins", or a plain unit.
"currency" | "duration" | "distance" | "weight" | Array<string | UnitOption> · default "currency"
- value
The unit's id. Use it with onValueChange.
string
- defaultValue
The unit at first render. Defaults to the first one.
string
- onValueChange
Called with the new unit's id when someone picks one.
(id: string) => void
- announce
Shows a note above the unit for a moment, and says it to screen readers.
{ text: string; key: number | string } | null
- size
Text size and padding, to match the field it sits in.
"sm" | "md" | "lg" · default "md"
- disabled
Blocks the menu.
boolean · default false
- aria-label
Names the button. Defaults to the unit's name, like "Pounds". A unit that shows letters puts them first, like "min, Minutes".
string
- title
The tooltip. Defaults to "Change currency", or "Change unit".
string
- className
Classes for the button. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| units | "currency" | "duration" | "distance" | "weight" | Array<string | UnitOption> | "currency" | A built-in set, or your own list. A string in a list is a built-in id, like "mins", or a plain unit. |
| value | string | - | The unit's id. Use it with onValueChange. |
| defaultValue | string | - | The unit at first render. Defaults to the first one. |
| onValueChange | (id: string) => void | - | Called with the new unit's id when someone picks one. |
| announce | { text: string; key: number | string } | null | - | Shows a note above the unit for a moment, and says it to screen readers. |
| size | "sm" | "md" | "lg" | "md" | Text size and padding, to match the field it sits in. |
| disabled | boolean | false | Blocks the menu. |
| aria-label | string | - | Names the button. Defaults to the unit's name, like "Pounds". A unit that shows letters puts them first, like "min, Minutes". |
| title | string | - | The tooltip. Defaults to "Change currency", or "Change unit". |
| className | string | - | Classes for the button. Later classes win. |
Accessibility
- The unit is a menu button: Enter, Space, or the down arrow opens the menu, and focus starts on the current unit.
- With a mouse it opens on the press. Let go on a unit to pick it, or click to leave the menu open.
- The button's name is the unit's name, like "Pounds", and its tooltip, "Change currency", describes it.
- A unit that shows letters puts them first in its name, like "min, Minutes", so people who speak commands can say what they see.
- Each unit is a radio item named by its name, so screen readers hear which one is checked.
- Picking a unit closes the menu and puts focus back on the button.
- The note is said once through a polite live region. The visible copy is hidden from screen readers, so it is not read twice.
- The label uses the shared MotionSwap fade and layout tokens. Motion off changes it immediately; reduced motion keeps only the fade.
Examples
Currency beside a price
<p className="flex items-center gap-1">
<UnitSwitch units="currency" defaultValue="GBP" />
<span>6.00</span>
</p>Duration in a stepper
<NumberStepper
aria-label="Meeting length"
defaultValue={90}
suffix={<UnitSwitch units="duration" defaultValue="mins" />}
/>Switch it from typed text
const unit = matchUnit("2 hours", "duration"); // { id: "hours", ... }