# Model Picker

> Searchable popover for choosing an AI model, grouped by provider, with capability icons, context size, speed and cost hints.

- **Slug:** `model-picker` · **Category:** input · **Status:** beta
- **Import:** `import { ModelCapability, ModelPicker, ModelPickerModel } from "@kura-ui/react/model-picker";`
- **Docs:** https://kuro.abstergo.fyi/components/model-picker
- **Registry item:** https://kuro.abstergo.fyi/r/model-picker.json
- **Tags:** model, llm, select, combobox, picker, provider, dropdown

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/model-picker.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add model-picker
# npm package
npm install @kura-ui/react
```

## When to use

- Let the user switch the model in a chat composer toolbar or a chat header.
- Compare models at a glance: vision/tools/reasoning support, context window, speed and price tier.
- Any “choose one of N models/agents/providers” selector that benefits from search and grouping.

## When not to use

- Running actions or navigating — use command-palette.
- Inserting commands while typing a prompt — use slash-command-menu.
- Two or three static options without metadata — a segmented control or plain select is enough.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `models` | `readonly ModelPickerModel[]` | — | yes | Models: { id, name, provider?, description?, capabilities?, contextWindow?, speed?: 1\|2\|3, cost?: 1\|2\|3, badge?, disabled?, icon? }. Grouped by provider in first-seen order. |
| `value` | `string` | — | no | Controlled selected model id. |
| `defaultValue` | `string` | — | no | Initial id when uncontrolled. Defaults to the first enabled model. |
| `onValueChange` | `(id: string, model: ModelPickerModel) => void` | — | no | Called when the user picks a model. |
| `open` | `boolean` | — | no | Controlled open state. |
| `defaultOpen` | `boolean` | `false` | no | Initial open state. |
| `onOpenChange` | `(open: boolean) => void` | — | no | Open state changes. |
| `searchable` | `boolean` | — | no | Show the search field. Default: true when there are more than 6 models. |
| `placeholder` | `string` | `"Select model"` | no | Trigger text when nothing is selected. |
| `searchPlaceholder` | `string` | `"Search models…"` | no | Search field placeholder. |
| `emptyMessage` | `string` | `"No models match"` | no | Shown when the search has no results. |
| `showDetails` | `boolean` | `true` | no | Fixed-height footer describing the highlighted model (no layout shift). |
| `side` | `"top" \| "bottom"` | `"bottom"` | no | Preferred popover side; flips on collision. Use top in a bottom-docked composer. |
| `align` | `"start" \| "center" \| "end"` | `"start"` | no | Popover alignment relative to the trigger. |
| `size` | `"sm" \| "md"` | `"sm"` | no | Trigger height: sm 32px for toolbars, md 36px. |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **ModelPicker** (`data-slot="model-picker"`) — The trigger button (provider mark, model name, chevron). data-state = open | closed.
- **ModelPickerContent** (`data-slot="model-picker-content"`) — Popover surface; scales from the trigger (origin-aware).
- **ModelPickerSearch** (`data-slot="model-picker-search"`) — Search row; the input is the combobox that owns keyboard navigation.
- **ModelPickerGroup** (`data-slot="model-picker-group"`) — role=group per provider, labelled by its heading.
- **ModelPickerItem** (`data-slot="model-picker-item"`) — role=option row: name, badge, meta line, capability icons, check. data-active, data-disabled.
- **ModelPickerDetails** (`data-slot="model-picker-details"`) — Footer with the highlighted model's description.

## Accessibility

- Role: `combobox + listbox`
- The trigger is labelled “Model: <name>”. On open, focus moves to the search combobox (or the listbox when not searchable) and returns to the trigger on close.
- Highlighting uses aria-activedescendant, so focus never leaves the search field while navigating.
- Options expose aria-selected and aria-disabled; provider groups are role=group with their heading as label.
- Capability icons, speed and cost meters carry visually hidden text (“Vision”, “Fast”, “High cost”), so the meta line reads naturally.
- Reduced motion: The popover fades without scale or travel and the chevron flips without rotating; highlight changes are instant in all modes.

| Keys | Action |
| --- | --- |
| Enter / Space / ArrowDown | Open from the trigger |
| ArrowDown / ArrowUp | Move the highlight (wraps, skips disabled models) |
| Home / End | First / last model (when the search is empty) |
| Enter | Choose the highlighted model and close |
| Escape | Close and return focus to the trigger |

## Notes for agents

- Pass `side="top"` inside a composer docked to the bottom of the viewport.
- speed: 3 = fast, 1 = slow/thorough. cost: 1 = cheap, 3 = expensive. Omit either to hide it.
- `icon` takes a 16px provider logo; without it a monogram of the provider is shown.
- formatContextWindow(200000) → "200K" is exported for use elsewhere (e.g. a context-meter).

## Examples

### Three models (minimal)

```tsx
import { ModelPicker, type ModelPickerModel } from "@kura-ui/react/model-picker";

const models: ModelPickerModel[] = [
  {
    id: "claude-opus-5-5",
    name: "Claude Opus 5.5",
    provider: "Anthropic",
    description: "Most capable. Long-horizon agentic work, deep research and hard code.",
    capabilities: ["vision", "tools", "reasoning", "files"],
    contextWindow: 1_000_000,
    speed: 1,
    cost: 3,
    badge: "New",
  },
  {
    id: "claude-sonnet-5-5",
    name: "Claude Sonnet 5.5",
    provider: "Anthropic",
    description: "The everyday default. Strong reasoning at a fraction of the latency.",
    capabilities: ["vision", "tools", "reasoning", "files"],
    contextWindow: 1_000_000,
    speed: 2,
    cost: 2,
  },
  {
    id: "claude-haiku-4-5",
    name: "Claude Haiku 4.5",
    provider: "Anthropic",
    description: "Fastest. Great for classification, extraction and sub-agents.",
    capabilities: ["vision", "tools"],
    contextWindow: 200_000,
    speed: 3,
    cost: 1,
  },
  {
    id: "gpt-5",
    name: "GPT-5",
    provider: "OpenAI",
    description: "General-purpose flagship with adjustable reasoning effort.",
    capabilities: ["vision", "tools", "reasoning", "web"],
    contextWindow: 400_000,
    speed: 2,
    cost: 2,
  },
  {
    id: "gpt-5-mini",
    name: "GPT-5 mini",
    provider: "OpenAI",
    description: "Smaller and cheaper, tuned for well-defined tasks.",
    capabilities: ["vision", "tools"],
    contextWindow: 400_000,
    speed: 3,
    cost: 1,
  },
  {
    id: "gemini-2-5-pro",
    name: "Gemini 2.5 Pro",
    provider: "Google",
    description: "Native audio and video understanding with a very long context.",
    capabilities: ["vision", "audio", "tools", "reasoning"],
    contextWindow: 1_000_000,
    speed: 2,
    cost: 2,
  },
  {
    id: "llama-4-maverick",
    name: "Llama 4 Maverick",
    provider: "Open weights",
    description: "Self-hostable mixture-of-experts model.",
    capabilities: ["vision"],
    contextWindow: 1_000_000,
    speed: 3,
    cost: 1,
  },
  {
    id: "mistral-large",
    name: "Mistral Large",
    provider: "Open weights",
    description: "Unavailable on the free plan.",
    capabilities: ["tools"],
    contextWindow: 128_000,
    speed: 2,
    cost: 2,
    badge: "Pro",
    disabled: true,
  },
];

export function ModelPickerMinimal() {
  return <ModelPicker models={models.slice(0, 3)} />;
}
```

### Searchable, grouped, controlled (full)

Eight models across providers, a disabled tier, badges, capability icons and a controlled open state.

```tsx
"use client";

import * as React from "react";
import { ModelPicker, type ModelPickerModel } from "@kura-ui/react/model-picker";

const models: ModelPickerModel[] = [
  {
    id: "claude-opus-5-5",
    name: "Claude Opus 5.5",
    provider: "Anthropic",
    description: "Most capable. Long-horizon agentic work, deep research and hard code.",
    capabilities: ["vision", "tools", "reasoning", "files"],
    contextWindow: 1_000_000,
    speed: 1,
    cost: 3,
    badge: "New",
  },
  {
    id: "claude-sonnet-5-5",
    name: "Claude Sonnet 5.5",
    provider: "Anthropic",
    description: "The everyday default. Strong reasoning at a fraction of the latency.",
    capabilities: ["vision", "tools", "reasoning", "files"],
    contextWindow: 1_000_000,
    speed: 2,
    cost: 2,
  },
  {
    id: "claude-haiku-4-5",
    name: "Claude Haiku 4.5",
    provider: "Anthropic",
    description: "Fastest. Great for classification, extraction and sub-agents.",
    capabilities: ["vision", "tools"],
    contextWindow: 200_000,
    speed: 3,
    cost: 1,
  },
  {
    id: "gpt-5",
    name: "GPT-5",
    provider: "OpenAI",
    description: "General-purpose flagship with adjustable reasoning effort.",
    capabilities: ["vision", "tools", "reasoning", "web"],
    contextWindow: 400_000,
    speed: 2,
    cost: 2,
  },
  {
    id: "gpt-5-mini",
    name: "GPT-5 mini",
    provider: "OpenAI",
    description: "Smaller and cheaper, tuned for well-defined tasks.",
    capabilities: ["vision", "tools"],
    contextWindow: 400_000,
    speed: 3,
    cost: 1,
  },
  {
    id: "gemini-2-5-pro",
    name: "Gemini 2.5 Pro",
    provider: "Google",
    description: "Native audio and video understanding with a very long context.",
    capabilities: ["vision", "audio", "tools", "reasoning"],
    contextWindow: 1_000_000,
    speed: 2,
    cost: 2,
  },
  {
    id: "llama-4-maverick",
    name: "Llama 4 Maverick",
    provider: "Open weights",
    description: "Self-hostable mixture-of-experts model.",
    capabilities: ["vision"],
    contextWindow: 1_000_000,
    speed: 3,
    cost: 1,
  },
  {
    id: "mistral-large",
    name: "Mistral Large",
    provider: "Open weights",
    description: "Unavailable on the free plan.",
    capabilities: ["tools"],
    contextWindow: 128_000,
    speed: 2,
    cost: 2,
    badge: "Pro",
    disabled: true,
  },
];

export function ModelPickerFull() {
  const [model, setModel] = React.useState("claude-sonnet-5-5");
  const [open, setOpen] = React.useState(false);
  const current = models.find((m) => m.id === model);
  return (
    <div className="flex flex-col items-center gap-3">
      <ModelPicker
        models={models}
        value={model}
        onValueChange={(id) => setModel(id)}
        open={open}
        onOpenChange={setOpen}
        searchable
        size="md"
        side="bottom"
        align="center"
        placeholder="Choose a model"
        searchPlaceholder="Search by name, provider or capability…"
        emptyMessage="No model has that capability"
        showDetails
      />
      <p className="text-kura-fg-subtle text-xs" aria-live="polite">
        Using <span className="font-kura-mono text-kura-fg-muted">{current?.id}</span>
      </p>
    </div>
  );
}
```

### In a composer toolbar (composition)

Opens upward from the composer's toolbar.

```tsx
"use client";

import * as React from "react";
import {
  Composer,
  ComposerActions,
  ComposerAttachButton,
  ComposerInput,
  ComposerSubmit,
  ComposerToolbar,
  ComposerTools,
} from "@kura-ui/react/composer";
import { ModelPicker, type ModelPickerModel } from "@kura-ui/react/model-picker";

const models: ModelPickerModel[] = [
  {
    id: "claude-opus-5-5",
    name: "Claude Opus 5.5",
    provider: "Anthropic",
    description: "Most capable. Long-horizon agentic work, deep research and hard code.",
    capabilities: ["vision", "tools", "reasoning", "files"],
    contextWindow: 1_000_000,
    speed: 1,
    cost: 3,
    badge: "New",
  },
  {
    id: "claude-sonnet-5-5",
    name: "Claude Sonnet 5.5",
    provider: "Anthropic",
    description: "The everyday default. Strong reasoning at a fraction of the latency.",
    capabilities: ["vision", "tools", "reasoning", "files"],
    contextWindow: 1_000_000,
    speed: 2,
    cost: 2,
  },
  {
    id: "claude-haiku-4-5",
    name: "Claude Haiku 4.5",
    provider: "Anthropic",
    description: "Fastest. Great for classification, extraction and sub-agents.",
    capabilities: ["vision", "tools"],
    contextWindow: 200_000,
    speed: 3,
    cost: 1,
  },
  {
    id: "gpt-5",
    name: "GPT-5",
    provider: "OpenAI",
    description: "General-purpose flagship with adjustable reasoning effort.",
    capabilities: ["vision", "tools", "reasoning", "web"],
    contextWindow: 400_000,
    speed: 2,
    cost: 2,
  },
  {
    id: "gpt-5-mini",
    name: "GPT-5 mini",
    provider: "OpenAI",
    description: "Smaller and cheaper, tuned for well-defined tasks.",
    capabilities: ["vision", "tools"],
    contextWindow: 400_000,
    speed: 3,
    cost: 1,
  },
  {
    id: "gemini-2-5-pro",
    name: "Gemini 2.5 Pro",
    provider: "Google",
    description: "Native audio and video understanding with a very long context.",
    capabilities: ["vision", "audio", "tools", "reasoning"],
    contextWindow: 1_000_000,
    speed: 2,
    cost: 2,
  },
  {
    id: "llama-4-maverick",
    name: "Llama 4 Maverick",
    provider: "Open weights",
    description: "Self-hostable mixture-of-experts model.",
    capabilities: ["vision"],
    contextWindow: 1_000_000,
    speed: 3,
    cost: 1,
  },
  {
    id: "mistral-large",
    name: "Mistral Large",
    provider: "Open weights",
    description: "Unavailable on the free plan.",
    capabilities: ["tools"],
    contextWindow: 128_000,
    speed: 2,
    cost: 2,
    badge: "Pro",
    disabled: true,
  },
];

export function ModelPickerInComposer() {
  const [model, setModel] = React.useState("claude-opus-5-5");
  return (
    <div className="w-full max-w-xl">
      <Composer onSubmit={() => {}}>
        <ComposerInput placeholder="Plan a migration from REST to GraphQL…" />
        <ComposerToolbar>
          <ComposerTools>
            <ComposerAttachButton />
            <ModelPicker models={models} value={model} onValueChange={setModel} side="top" />
          </ComposerTools>
          <ComposerActions>
            <ComposerSubmit />
          </ComposerActions>
        </ComposerToolbar>
      </Composer>
    </div>
  );
}
```

## Dependencies

- npm: `lucide-react`
- Kura registry items: `kura-theme`, `popover`, `use-controllable-state`, `utils`
- Related: `composer`, `command-palette`, `slash-command-menu`
