Model Picker React component for Tailwind CSS v4
v1.0.1Updated in 1.6.0
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.
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.
Model Picker is a Pro React component in the React AI components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add model-picker, together with Select, Badge, Tooltip, Spinner, Field and 9 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add model-pickerModel Picker is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import ModelPicker from @/components/ui/model-picker.
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
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).
valuestringThe 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.
defaultValuedefault""stringThe 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) => voidCalled with the id and the whole model after a pick or a typeahead jump. Never called for the automatic default.
defaultOpendefaultfalsebooleanStarts with the list (or the sheet) open. Pass open and onOpenChange to control it.
openbooleanWhether the list is open, controlled. It never opens while disabled.
onOpenChange(open: boolean) => voidCalled when the list opens or closes.
disableddefaultfalsebooleanDims the trigger; it never opens and the shortcut does nothing. A disabled Field around it does the same.
loadingdefaultfalsebooleanModels are on their way: a spinner replaces the chevron and the list shows a loading row until models arrive.
variantdefault"ghost""ghost" | "soft" | "outline"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.
sizedefault"sm""sm" | "md" | "lg"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.
radiusdefault"full""none" | "sm" | "md" | "lg" | "full"The trigger's corners, scaled per size like the button; full is a pill.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"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.
colordefault""stringAny CSS color for the trigger; replaces the tone. Leave empty to use the tone.
showIcondefaulttruebooleanThe chosen model's icon before its name in the trigger (when the model has one).
showProviderdefaultfalsebooleanThe provider before the name in the trigger: "Northwind AI · Standard" (a name that repeats the maker is shortened).
showCapabilitiesdefaultfalsebooleanThe row of capability icons in each option (vision, tools, reasoning, web, files, code, image, voice), each with a tooltip.
showSpeeddefaultfalsebooleanThe 3-step speed meter in each option.
showCostdefaultfalsebooleanThe 3-step cost meter in each option.
showContextdefaultfalsebooleanThe context window in each option ("200K context"), formatted for the locale.
showDescriptiondefaulttruebooleanOne line of description in the menu, two in the phone sheet. A locked model shows its reason either way.
fullWidthdefaultfalsebooleanStretches the trigger to its container with the chevron at the end, the usual choice as a form field.
contentWidthdefault320numberThe list's width on desktop in px (never narrower than the trigger, never wider than the screen).
maxHeightdefault560numberThe list's maximum height on desktop in px, capped by the room on screen; longer lists scroll with sticky group labels.
sidedefault"top""top" | "right" | "bottom" | "left"Where the list opens. Composers sit low on the screen, so it opens upward; it flips when there is no room.
aligndefault"start""start" | "center" | "end"How the list lines up with the trigger.
containerHTMLElement | nullWhere the list or the sheet is portaled (document.body by default).
groupBydefault"group""group" | "provider" | "none"List sections: by model.group, by model.provider, or one flat list. Models without one go last, under a separator with no visible label.
searchdefault"auto""auto" | "always" | "never"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.
responsivedefaulttruebooleanA bottom sheet with 64px rows and two-line descriptions below 768px instead of the popover.
shortcutdefault""stringA 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.
namedefault""stringRenders a hidden native select with this name, so FormData and form reset work.
localedefault""stringThe 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.
labelsPartial<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).
classNamesPartial<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.
classNamestringExtra classes for the trigger button, merged last. Other props (aria-*, data-*, onClick, onBlur, id, form, ref) go there too.
Parts
ModelPickerOption
componentThe 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
componentThe 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
componentThe 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
functionModels 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
constantThe 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.
Dependencies
Files
- components/ui/model-picker.tsx
FAQ
Is the React Model Picker component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Model Picker?
Run npx wingo-ui@latest add model-picker. It also adds the Wingo UI items it builds on (Select, Badge, Tooltip, Spinner, Field and 9 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/model-picker.
How does Model Picker behave on phones?
A bottom sheet with 64px rows and two-line descriptions below 768px instead of the popover. The responsive prop controls this and is on by default.
See also
Used in
More React AI components
Articles using this component
- React Prompt Input: Enter, IME, Files, Stop and Voice
Building UI with AI agents10 min read
- React AI Chat UI: Streaming, Auto-Scroll and Mobile
Building UI with AI agents10 min read