# Model Picker

> A React AI model picker for chat apps: a quiet trigger that opens a short grouped list of models, each with one line on what it is for.

- Type: React component (AI Kit)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.1
- Install: `npx wingo-ui@latest add model-picker`
- Docs: https://wingo-ui.com/components/model-picker

## Interaction

Click or tap the trigger ("Northwind Standard" with a chevron) to open the list; the chevron turns over on a quick spring. On desktop it is a popover that opens upward from a composer, grouped by model group with sticky section labels; each row shows the model's icon, its name with New / Recommended badges and one line of description; showCapabilities, showSpeed, showCost and showContext add a line of capability icons, speed and cost meters and the context window, whose icons and meters name themselves on hover. Click a row to pick it: the check draws, the list closes and onValueChange gets the id and the model. Locked models stay in the list, dimmed with a lock and the reason as their description, and cannot be picked. Above 8 models (or with search="always") a search row sits on top and takes focus: it matches names, makers, groups, capability names and keywords, ignores case and diacritics and bolds the matched letters. Keyboard: Tab reaches the trigger; Enter, Space or ArrowDown open it on the chosen model, ArrowUp and ArrowDown move, Home and End jump to the ends, typing letters jumps by name, Enter picks, Escape or Tab close and focus stays on the trigger. While closed, letters pick the next model by name like a native select. With shortcut ("mod+.") the list toggles from anywhere on the page, the tooltip shows the keys, and a model picked after the shortcut sends focus back where you were typing. On a phone the list is a bottom sheet titled "Model" with 64px rows, two-line descriptions and no tooltips; a tap picks and closes it, a drag down or a tap outside dismisses it. With reduced motion the chevron flips without the spring and the list fades instead of scaling.

## Installation

```bash
npx wingo-ui@latest add model-picker
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/model-picker` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/model-picker.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { Feather, Gem, Sparkles } from "lucide-react"
import { ModelPicker, type AiModel } from "@/components/ui/model-picker"

const MODELS: AiModel[] = [
  { id: "swift", name: "Northwind Swift", group: "Northwind AI", description: "Short answers, almost instant", icon: <Feather />, capabilities: ["tools", "files"], contextWindow: 128_000, speed: 3, cost: 1 },
  { id: "standard", name: "Northwind Standard", group: "Northwind AI", description: "Balanced, for everyday work", icon: <Sparkles />, capabilities: ["vision", "tools", "files", "web"], contextWindow: 200_000, speed: 2, cost: 2, recommended: true },
  { id: "pro", name: "Northwind Pro", group: "Northwind AI", description: "Deep reasoning for analysis and reports", icon: <Gem />, capabilities: ["reasoning", "tools", "web", "files", "code", "vision"], contextWindow: 200_000, speed: 1, cost: 3, isNew: true },
]

export function ChatModel() {
  const [model, setModel] = useState("standard")
  return (
    <ModelPicker
      models={MODELS}
      value={model}
      onValueChange={(id) => setModel(id)}
      shortcut="mod+."
      labels={{ placeholder: "Pick a model", sheetTitle: "Switch model" }}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `models` | `AiModel[]` |  | The models: { id, name, provider?, description?, icon?, group?, capabilities?, contextWindow?, speed?: 1 \| 2 \| 3, cost?: 1 \| 2 \| 3, pricing?, badge?: { label, tone? }, recommended?, isNew?, disabled?, disabledReason?, keywords? } from lib/ai-chat. A repeated id keeps its first model (with a warning while developing). |
| `value?` | `string` |  | The chosen model's id, controlled. "" shows the placeholder, and so does an id that is not among the models; the picker never replaces it on its own. |
| `defaultValue?` | `string` | `""` | The starting model while uncontrolled. "" is automatic: the first recommended model, else the first enabled one (it follows models that arrive later). |
| `onValueChange?` | `(id: string, model: AiModel) => void` |  | Called with the id and the whole model after a pick or a typeahead jump. Never called for the automatic default. |
| `defaultOpen?` | `boolean` | `false` | Starts with the list (or the sheet) open. Pass open and onOpenChange to control it. |
| `open?` | `boolean` |  | Whether the list is open, controlled. It never opens while disabled. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the list opens or closes. |
| `disabled?` | `boolean` | `false` | Dims the trigger; it never opens and the shortcut does nothing. A disabled Field around it does the same. |
| `loading?` | `boolean` | `false` | Models are on their way: a spinner replaces the chevron and the list shows a loading row until models arrive. |
| `variant?` | `"ghost" \| "soft" \| "outline"` | `"ghost"` | ghost for composers and toolbars (a tint on hover and while open); soft and outline copy the field box, so the picker lines up with inputs and selects in a form. |
| `size?` | `"sm" \| "md" \| "lg"` | `"sm"` | Trigger height 32 / 40 / 48px with matching text and icons. md grows to 44px on touch, sm keeps its look with a 44px hit area. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"full"` | The trigger's corners, scaled per size like the button; full is a pill. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Paints the trigger's text, tint and border in a tone. neutral keeps the field look; brand is for the one place a premium model should stand out. |
| `color?` | `string` | `""` | Any CSS color for the trigger; replaces the tone. Leave empty to use the tone. |
| `showIcon?` | `boolean` | `true` | The chosen model's icon before its name in the trigger (when the model has one). |
| `showProvider?` | `boolean` | `false` | The provider before the name in the trigger: "Northwind AI · Standard" (a name that repeats the maker is shortened). |
| `showCapabilities?` | `boolean` | `false` | The row of capability icons in each option (vision, tools, reasoning, web, files, code, image, voice), each with a tooltip. |
| `showSpeed?` | `boolean` | `false` | The 3-step speed meter in each option. |
| `showCost?` | `boolean` | `false` | The 3-step cost meter in each option. |
| `showContext?` | `boolean` | `false` | The context window in each option ("200K context"), formatted for the locale. |
| `showDescription?` | `boolean` | `true` | One line of description in the menu, two in the phone sheet. A locked model shows its reason either way. |
| `fullWidth?` | `boolean` | `false` | Stretches the trigger to its container with the chevron at the end, the usual choice as a form field. |
| `contentWidth?` | `number` | `320` | The list's width on desktop in px (never narrower than the trigger, never wider than the screen). |
| `maxHeight?` | `number` | `560` | The list's maximum height on desktop in px, capped by the room on screen; longer lists scroll with sticky group labels. |
| `side?` | `"top" \| "right" \| "bottom" \| "left"` | `"top"` | Where the list opens. Composers sit low on the screen, so it opens upward; it flips when there is no room. |
| `align?` | `"start" \| "center" \| "end"` | `"start"` | How the list lines up with the trigger. |
| `container?` | `HTMLElement \| null` |  | Where the list or the sheet is portaled (document.body by default). |
| `groupBy?` | `"group" \| "provider" \| "none"` | `"group"` | List sections: by model.group, by model.provider, or one flat list. Models without one go last, under a separator with no visible label. |
| `search?` | `"auto" \| "always" \| "never"` | `"auto"` | The search row on top of the list: auto shows it above 8 models. It matches names, providers, groups, capability names and keywords, diacritics ignored. |
| `responsive?` | `boolean` | `true` | A bottom sheet with 64px rows and two-line descriptions below 768px instead of the popover. |
| `shortcut?` | `string` | `""` | A key combo in mod+k notation that toggles the list from anywhere ("mod+." is Cmd+. on a Mac, Ctrl+. elsewhere). Shown in the trigger's tooltip and set as aria-keyshortcuts. Empty binds nothing. |
| `name?` | `string` | `""` | Renders a hidden native select with this name, so FormData and form reset work. |
| `locale?` | `string` | `""` | The locale of the context window numbers ("200K" in en-US, "200 K" and "1 mil." in ro-RO). Empty uses the UIProvider's, then the browser's. |
| `labels?` | `Partial<ModelPickerLabels>` |  | Every string, English by default (DEFAULT_MODEL_PICKER_LABELS): placeholder "Choose a model", trigger "Model" (the accessible name is "Model: Northwind Standard"), tooltip "Switch model", search "Search models", empty "No models found", loading, sheetTitle "Model", recommended, new, context (formatted) => "200K context", speed ["Slower", "Fast", "Fastest"], cost ["Low cost", "Medium cost", "High cost"], unavailable, ungrouped, results (count) => string, clearSearch, capabilities (DEFAULT_AI_CAPABILITY_LABELS). |
| `classNames?` | `Partial<Record<"trigger" \| "icon" \| "provider" \| "name" \| "chevron" \| "content" \| "search" \| "group" \| "option" \| "badges" \| "description" \| "meta" \| "capabilities", string>>` |  | Extra classes for the inner parts: the trigger and its pieces, the list surface (content), its search row and group labels, and the parts of each option. |
| `className?` | `string` |  | Extra classes for the trigger button, merged last. Other props (aria-*, data-*, onClick, onBlur, id, form, ref) go there too. |

## Parts

### ModelPickerOption (component)

The rich row on its own, for other lists (a command menu, a settings table): icon tile or provider initials, name and badges, description, capabilities, meters and context, a lock for locked models. Props: { model, density?: "menu" | "sheet", ranges?, showCapabilities?, showSpeed?, showCost?, showContext?, showDescription?, tooltips?, locale?, labels?, classNames?, ...div }.

### ModelCapabilities (component)

The capability icons as a list; each name is in a tooltip on fine pointers and always there for screen readers. Props: { capabilities: AiCapability[], labels?, tooltips?, size?: "sm" | "md", classNames?, ...ul }.

### ModelMeter (component)

The 3-step speed or cost indicator: a Zap or Coins glyph and three bars, role="img" with the level's name. Props: { value: 1 | 2 | 3, kind?: "speed" | "cost", label?, icon?, tooltip?, classNames?, ...span }.

### toModelOptions (function)

Models to select / listbox options, grouped, with provider, group and capability names as search keywords. Pair it with ModelPickerOption in a Listbox or Combobox renderOption. Signature: (models: AiModel[], { groupBy?, showDescription?, labels? }) => SelectItems.

### DEFAULT_MODEL_PICKER_LABELS (constant)

The English strings, to spread and override. Type: ModelPickerLabels.

## Examples

- **As a form field**: variant="outline" size="md" fullWidth inside a Field: same height, border and radius as the select under it.
- **Flat list with search**: groupBy="none" search="always" over 14 models; names, makers and capabilities all match.
- **Detailed rows**: showCapabilities, showSpeed, showCost and showContext add a line of model details under each row.
- **Keyboard shortcut**: shortcut="mod+." toggles the list from anywhere and shows in the trigger's tooltip.
- **Brand**: tone="brand" on the trigger and a model.badge in the brand tone.
- **Sizes and variants**: ghost for composers, soft and outline as fields; 32, 40 and 48px.
- **States**: Loading, disabled, a value that is not among the models, and a plan where every model is locked.

## See also

- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Combobox](https://wingo-ui.com/components/combobox) ([Markdown](https://wingo-ui.com/components/combobox.md))
- [Prompt Input](https://wingo-ui.com/components/prompt-input) ([Markdown](https://wingo-ui.com/components/prompt-input.md))
- [AI Chat](https://wingo-ui.com/components/ai-chat) ([Markdown](https://wingo-ui.com/components/ai-chat.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))

---

Canonical: https://wingo-ui.com/components/model-picker
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
