Select
A choice set inside a sentence: the chosen word is yours, italic over a hairline. It can be reset from the last word's letters, or wear the other choices above it as ruby.
Report an issue with SelectExample
underline
work from
compose
Send it
ruby
Reply this week
underline, rest
underline, pointed at
underline, disabled
compose, rest
compose, pointed at
compose, disabled
ruby, rest
ruby, pointed at
ruby, disabled
Install
Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.
npx shadcn@latest add https:// thedirectors.agency/ ui/ r/select.jsonUsage
import { Select } from "@/components/ui/select"export default function Example() {return (<div className="grid gap-12"><div className="grid gap-6"><span className="ot-label">underline</span><Select label="Sort by" defaultValue="newest"><option>newest</option><option>oldest</option><option>name</option></Select><p className="ot-mp text-[color:var(--ot-graphite)]"><Select label="Show" aria-label="Which projects"><option>all</option><option>identity</option><option>web</option><option>motion</option></Select>{" "}work from{" "}<Select aria-label="Year"><option>2026</option><option>2025</option><option>2024</option></Select></p></div><div className="grid gap-6"><span className="ot-label">compose</span><Select label="Sort by" variant="compose" defaultValue="newest"><option>newest</option><option>oldest</option><option>latest</option><option>name</option></Select><p className="ot-mp text-[color:var(--ot-graphite)]">Send it{" "}<Select aria-label="When to send" variant="compose" defaultValue="tonight"><option>now</option><option>tonight</option><option>tomorrow</option><option>on Monday</option></Select></p></div><div className="grid gap-6"><span className="ot-label">ruby</span><Select label="Set in" variant="ruby" defaultValue="Bodoni"><option>Bodoni</option><option>Garamond</option><option>Caslon</option></Select><p className="ot-mp text-[color:var(--ot-graphite)]">Reply{" "}<Select aria-label="How to reply" variant="ruby" defaultValue="by email"><option>by email</option><option>by phone</option><option>in person</option></Select>{" "}this week</p></div></div>)}Contract
- Underneath: native
<select>. - Anatomy:
<p class="ot-select">Sort by <span class="ot-select-box"><select>…</select></span></p>. - Yours: the chosen option, italic over a hairline. A small ↓ follows it.
- Where
appearance: base-selectis supported, the open list is restyled: the current option has an accent dot beside it, and the hovered option is highlighted. The list drops in 6px and its options arrive in turn (sibling-index()×--ot-arpeggio, where supported). - Keyboard: native select.
- Variants go on
data-variant: underline (the default), compose, ruby. Both drawn variants keep the native select for focus, keyboard and list; its own words are made transparent and a copy is set in the same place for script to move. The hairline moves to the box and stretches between the old word's width and the new one's. - Compose, after the compositor distributing type back to the case: choosing a new word resets it letter by letter. Letters the two words share slide across to their new places; the rest of the old word falls away in pencil and the new letters drop in, spiccato, as sorts set into the stick. "newest" to "oldest" keeps its e, s and t.
- Ruby, after Japanese ruby and the posters' small notes hung by a large italic: the other choices ride small above the word, ours (roman, pencil) over yours (italic, ink), so every option is in view without opening anything. Clicking one sets it: the two words cross, the picked one down onto the line and the old one up into the ruby, while the others glide aside. The ruby is
aria-hidden; the select is still the control. Pointing a ruby word draws a hairline under it. - Drawn variants: reduced motion swaps words at once. Right to left the word keeps its own direction (
dir="auto") and the ruby hangs from the right. Forced colours drop the copy and show the select's own text.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
variant | "underline" | "compose" | "ruby" | "underline" | How the chosen word changes. underline: the word, italic over a hairline, simply changes. compose: the new word is set from the letters of the last, as a compositor resets a line; the letters both share slide to their new places, the others drop out and new ones drop in, and the hairline stretches to fit. ruby: the other choices are set small in our roman above the word, as ruby rides over Japanese; point at one to pick it, and it comes down onto the line as the old word goes up into its place. Ruby suits two to four choices. Either way the select underneath keeps the keyboard, the list and forms. |
label | ReactNode | The words before the choice, such as Sort by. They label the select, and clicking them opens it. | |
children | <option>[] | The choices. A real select underneath, so keyboard, forms and autofill are the browser's own. | |
value / defaultValue / onChange | native | Every native select prop passes through. | |
rootClassName | string | Classes for the outer sentence and label. className goes to the native select. |
Craft
Where its move comes from
| Component | Reference | Move |
|---|---|---|
| Compose select | The compositor distributing type | The word resets letter by letter; shared letters stay in the stick |
| Ruby select | Japanese ruby; the small notes hung by a big italic | The other choices ride small above the word |
How it moves
| Component | Articulation | What moves |
|---|---|---|
| Select | Arpeggio | The options arrive in turn |
| Compose select | Composing | Shared letters slide to their places; the others fall out in pencil and drop in with spiccato; the hairline stretches |
| Ruby select | Interlinear | The picked word comes down onto the line as the old one goes up into the ruby; the rest glide aside |