49 / Agent UI
Model Picker
A model chooser that has room for what each one is good at, and full keyboard control.
Answering with Sonnet
"use client"
import * as React from "react"
import {
ModelPicker,
type Model,
} from "mischief-ui/model-picker"
const models: Model[] = [
{
id: "opus",
name: "Opus",
description: "The deepest reasoning, for work that is worth the wait.",
badges: ["reasoning", "vision"],
},
{
id: "sonnet",
name: "Sonnet",
description: "The everyday balance of speed and judgement.",
badges: ["balanced", "vision"],
},
{
id: "haiku",
name: "Haiku",
description: "Quick answers where latency matters more than depth.",
badges: ["fast"],
},
{
id: "legacy",
name: "Legacy",
description: "Retired. Kept here so old links still resolve.",
disabled: true,
},
]
export function ModelPickerDemo() {
const [value, setValue] = React.useState("sonnet")
return (
<div className="grid w-full max-w-sm gap-3">
<ModelPicker models={models} value={value} onValueChange={setValue} />
<p className="text-muted-foreground text-xs" role="status">
Answering with {models.find((model) => model.id === value)?.name}
</p>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/model-pickerimport { ModelPicker } from "mischief-ui/model-picker"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { Check, ChevronsUpDown } from "lucide-react" import { cn } from "@/lib/utils" export type Model = { id: string name: string description?: string /** Short capability tags, such as "vision" or "fast". */ badges?: readonly string[] disabled?: booleanUsage
export function Chooser() {
return (
<ModelPicker
models={models}
defaultValue="opus"
onValueChange={setModel}
/>
)
}Describing the models
The reason to use a listbox rather than a select element is the room it gives you: a sentence about what each model is for, and a few tags for what it can do. Write the description for someone deciding, not for someone who already knows -- speed, depth, and cost are what people are actually choosing between.
const models = [
{
id: "opus",
name: "Opus",
description: "The deepest reasoning, for work worth the wait.",
badges: ["reasoning", "vision"],
},
{
id: "haiku",
name: "Haiku",
description: "Quick answers where latency matters more than depth.",
badges: ["fast"],
},
{ id: "legacy", name: "Legacy", disabled: true },
]Keep a retired model in the list with disabled rather than removing it, so a stored preference still resolves to a name instead of falling back to the placeholder. Disabled models are skipped by the arrow keys, not merely dimmed.
API
modelsModel[]Each with an id, a name, and optional description, badges, and disabled.value, defaultValuestring, stringControlled and uncontrolled selection.onValueChange(id: string) => voidCalled with the chosen model's id.labelstringNames the control. Defaults to "Model".placeholderstringShown until something is chosen.disabledbooleanDisables the trigger.Model
idstringWhat onValueChange reports and value matches.namestringShown on the trigger and in the list.descriptionstringA line beneath the name.badgesstring[]Short capability tags, such as vision or fast.disabledbooleanListed but unchoosable, and skipped by the keyboard.Accessibility
The trigger declares that it opens a listbox and whether it is open, and names the current model. The list takes focus and drives selection through aria-activedescendant, so the active option is announced without focus leaving the list. Arrow keys move, Home and End jump, Enter and Space choose, Escape closes, and focus returns to the trigger either way. Disabled models are skipped by the keyboard rather than merely dimmed. Pointer events outside close it.