Model picker

Free · MIT

Model dropdown grouped by provider with icon slots, speed and quality meters that grow in, context window and price per million tokens, a gliding highlight and full keyboard support with type-ahead.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/model-picker.json

The file lands in components/goodui/model-picker.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/model-picker works too.

Usage

import { ModelPicker, type ModelOption } from "@/components/goodui/model-picker"

const MODELS: ModelOption[] = [
  { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", provider: "Anthropic", speed: 4, quality: 5, context: 200_000, price: { input: 3, output: 15 }, badge: "Recommended" },
  { id: "gpt-5-mini", name: "GPT-5 mini", provider: "OpenAI", speed: 5, quality: 3, context: 400_000, price: { input: 0.25, output: 2 } },
]

<ModelPicker models={MODELS} value={model} onValueChange={setModel} />

When to use

Use it for
  • Letting people pick the model for a chat, an agent or a batch job, with enough information to choose (speed, quality, context, price).
  • Toolbars and composers: the ghost, small variant fits next to other controls.
  • Multi-provider products where rows should be grouped by provider.
Reach for something else
  • A generic single-select field without model metadata: use select.
  • Thousands of options with search: use combobox.
  • Comparing models in depth: a table; the picker shows one line of facts per model.
If you needUse
Field in a settings formvariant="outline" size="md"
Inside a composer or toolbarvariant="ghost" size="sm" showMeters={false}
Flat list, no provider headingsgroupByProvider={false}
Real logosproviderIcons={{ OpenAI: <Logo /> }} or icon per model
Retired model still listeddisabled on the model

Examples

Compact trigger aligned to the end

<ModelPicker models={MODELS} variant="ghost" size="sm" align="end" showMeters={false} />

German prices and labels

<ModelPicker models={MODELS} locale="de-DE" labels={{ chooseModel: "Modell wählen", speed: "Tempo", quality: "Qualität" }} />

API reference

PropTypeDefault
models*ModelOption[]—
value
Selected model id (controlled).
string—
defaultValuestring—
onValueChange(id: string, model: ModelOption) => void—
providerIcons
Icon per provider name, e.g. { OpenAI: <OpenAiLogo />, Anthropic: <AnthropicLogo /> }. Missing ones get a lettered tile.
Record<string, React.ReactNode>—
variant
Trigger look: a bordered field or a quiet ghost button for toolbars.
"outline" | "ghost""outline"
size"sm" | "md""md"
groupByProvider
Group rows under provider headings.
booleantrue
showMetersbooleantrue
showContextbooleantrue
showPricebooleantrue
align
Which trigger edge the menu aligns to.
"start" | "end""start"
locale
Locale for the context window and prices.
string"en-US"
placeholderstring—
disabledboolean—
labels
Text for built-in controls and screen readers; pass translations here.
Partial<ModelPickerLabels>—

Also accepts every <div> attribute.

Accessibility

Enter / Space / ArrowDown / ArrowUpOn the trigger: opens the list and focuses it.
ArrowDown / ArrowUpMoves the active row, skipping disabled models, and wraps.
Home / EndFirst or last enabled model.
LettersType-ahead: jumps to the first model whose name starts with what you typed (600 ms window).
Enter / SpaceSelects the active model and returns focus to the trigger.
EscapeCloses and returns focus to the trigger. Tab closes too.
  • The trigger has aria-haspopup="listbox", aria-expanded, aria-controls and an aria-label that includes the current model name.
  • The list is role="listbox" with aria-activedescendant; rows are role="option" with aria-selected and aria-disabled.
  • Meters are announced as text ("Speed 4 of 5"); the bars themselves are decorative. The selected row has a visually hidden "Selected".
  • Built-in strings go through the labels prop for translation.

Motion

  • The list springs open (stiffness 520, damping 34) from the trigger's edge and flips above it when fewer than 380px remain below.
  • Rows fade in staggered by 25 ms (up to 8 rows); the highlight glides between rows as a shared-layout spring; meter bars grow from the baseline with a 40 ms stagger when the list opens.
  • Reduced motion: the list fades, rows do not stagger, the highlight jumps and the bars appear instantly.

Notes for AI

Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.

  • Prices are USD per million tokens; context is in tokens and formatted compactly (200K, 1M).
  • onValueChange receives (id, model). Pass value to control it; otherwise it starts from defaultValue or the first model.
  • Provider icons: providerIcons keyed by the provider string, or icon on a model. Without either, a lettered tile colored from the provider name is used.
  • The list is rendered in place (absolute), not in a portal; a parent with overflow:hidden will clip it.

FAQ

Can I show only some facts?

Yes: showMeters, showContext and showPrice each hide their column.

How do I add a price unit other than per 1M?

Set labels.perMillion; it is the tooltip on the price. The numbers themselves are whatever you pass.

Related