Model Picker

A select for the model a message goes to, searched by name, filtered by provider and starred.

'use client';

import { ClaudeMark, DeepSeekMark, GeminiMark, MetaMark, MistralMark, OpenAIMark, XAIMark } from '@/components/model-logos';
import { ModelPicker, type ModelPickerItem, type ModelPickerProvider } from '@/components/panelui/model-picker';

const PROVIDERS: ModelPickerProvider[] = [
  { value: 'openai', label: 'OpenAI', icon: <OpenAIMark /> },
  { value: 'anthropic', label: 'Anthropic', icon: <ClaudeMark /> },
  { value: 'google', label: 'Google', icon: <GeminiMark />, status: 'new' },
  { value: 'xai', label: 'xAI', icon: <XAIMark /> },
  { value: 'mistral', label: 'Mistral', icon: <MistralMark /> },
  { value: 'deepseek', label: 'DeepSeek', icon: <DeepSeekMark />, status: 'limited' },
  { value: 'meta', label: 'Meta', icon: <MetaMark /> },
];

const MODELS: ModelPickerItem[] = [
  { value: 'gpt-5.5', label: 'GPT-5.5', provider: 'openai', badge: 'New' },
  { value: 'gpt-5.4-thinking', label: 'GPT-5.4 Thinking', provider: 'openai' },
  { value: 'claude-opus-5.5', label: 'Claude Opus 5.5', provider: 'anthropic' },
  { value: 'claude-sonnet-5.5', label: 'Claude Sonnet 5.5', provider: 'anthropic' },
  { value: 'claude-haiku-5.5', label: 'Claude Haiku 5.5', provider: 'anthropic' },
  { value: 'gemini-3.1-pro', label: 'Gemini 3.1 Pro', provider: 'google' },
  { value: 'gemini-3-flash', label: 'Gemini 3 Flash', provider: 'google' },
  { value: 'grok-4', label: 'Grok 4', provider: 'xai' },
  { value: 'mistral-large-3', label: 'Mistral Large 3', provider: 'mistral' },
  { value: 'deepseek-v4', label: 'DeepSeek V4', provider: 'deepseek', disabled: true, note: 'Rate-limited until 14:00' },
  { value: 'llama-4-maverick', label: 'Llama 4 Maverick', provider: 'meta' },
];

export default function ModelPickerOverview() {
  return (
    <ModelPicker
      items={MODELS}
      providers={PROVIDERS}
      defaultValue="claude-sonnet-5.5"
      defaultFavorites={['gpt-5.5', 'claude-opus-5.5', 'claude-sonnet-5.5', 'gemini-3.1-pro']}
      hotkey="alt+m"
    >
      <ModelPicker.Trigger appearance="outline" />
      <ModelPicker.Content />
    </ModelPicker>
  );
}

Use it to choose the model a message goes to, in a composer's footer or a chat's header, when there are more models than a plain select can show well. For three or four models with no need to search, Prompt's own Prompt.Model is enough.

It opens with the search field focused. Typing searches every model by its name, its provider, its description and its tags, whatever filter is on. The arrow keys move through the list, Enter chooses, and ⌥1 to ⌥9 choose the numbered rows. hotkey opens it from anywhere on the page; opened that way it appears at once instead of growing.

Starred models come first: they are the rail's first filter, and the first group of the list layout. Star a model with the star on its row, or with ⌥S on the highlighted one.

The component draws no logos. Give each provider an icon, and each model shows its provider's unless it has its own. The marks on this page are the providers' own, and an app needs the right to show them.

Installation

npx shadcn@latest add https://panelui.dev/r/web/model-picker.json

The source is copied into components/panelui/, where you can edit it.

Usage

import { ModelPicker } from "@/components/panelui/model-picker"
const providers = [
  { value: 'anthropic', label: 'Anthropic', icon: <ClaudeMark /> },
  { value: 'openai', label: 'OpenAI', icon: <OpenAIMark /> },
]

const models = [
  { value: 'claude-sonnet-5.5', label: 'Claude Sonnet 5.5', provider: 'anthropic' },
  { value: 'gpt-5.5', label: 'GPT-5.5', provider: 'openai' },
]

<ModelPicker items={models} providers={providers} defaultValue="claude-sonnet-5.5" hotkey="alt+m">
  <ModelPicker.Trigger />
  <ModelPicker.Content />
</ModelPicker>

Composition

<ModelPicker>
  <ModelPicker.Trigger />
  <ModelPicker.Content />
</ModelPicker>
  • ModelPicker.Trigger — The button that opens the list, showing the chosen model's icon and name. appearance is ghost, with no box until hovered, for a composer, or outline, for a toolbar. With a hotkey on the root, its tooltip shows the keys.
  • ModelPicker.Content — The list, laid out as the root's variant says. side and align place it against the trigger, and placeholder and emptyText change what the search field and an empty list say.

Examples

Rail

The default layout: the providers in a column of icons beside the list, starred models first. A provider's status marks its icon: new with a dot, limited with a clock. A model with disabled stays in the list, greyed out, with its note saying why.

'use client';

import { ClaudeMark, DeepSeekMark, GeminiMark, OpenAIMark } from '@/components/model-logos';
import { ModelPicker, type ModelPickerItem, type ModelPickerProvider } from '@/components/panelui/model-picker';

const PROVIDERS: ModelPickerProvider[] = [
  { value: 'anthropic', label: 'Anthropic', icon: <ClaudeMark /> },
  { value: 'openai', label: 'OpenAI', icon: <OpenAIMark /> },
  { value: 'google', label: 'Google', icon: <GeminiMark /> },
  { value: 'deepseek', label: 'DeepSeek', icon: <DeepSeekMark />, status: 'limited' },
];

const MODELS: ModelPickerItem[] = [
  { value: 'claude-opus-5.5', label: 'Claude Opus 5.5', provider: 'anthropic' },
  { value: 'claude-sonnet-5.5', label: 'Claude Sonnet 5.5', provider: 'anthropic' },
  { value: 'claude-haiku-5.5', label: 'Claude Haiku 5.5', provider: 'anthropic', note: 'Fastest, for short replies' },
  { value: 'gpt-5.5', label: 'GPT-5.5', provider: 'openai' },
  { value: 'gpt-5.4-pro', label: 'GPT-5.4 Pro', provider: 'openai', note: 'Pro plan only' },
  { value: 'gemini-3.1-pro', label: 'Gemini 3.1 Pro', provider: 'google' },
  { value: 'deepseek-v4', label: 'DeepSeek V4', provider: 'deepseek', disabled: true, note: 'Rate-limited until 14:00' },
];

export default function ModelPickerRail() {
  return (
    <ModelPicker items={MODELS} providers={PROVIDERS} defaultValue="gpt-5.5" defaultFavorites={['claude-sonnet-5.5', 'gpt-5.5']}>
      <ModelPicker.Trigger />
      <ModelPicker.Content />
    </ModelPicker>
  );
}

List

One list under provider headings, with starred models in a group of their own at the top and each model's contextWindow at the end of its row.

'use client';

import { ClaudeMark, GeminiMark, MistralMark, OpenAIMark } from '@/components/model-logos';
import { ModelPicker, type ModelPickerItem, type ModelPickerProvider } from '@/components/panelui/model-picker';

const PROVIDERS: ModelPickerProvider[] = [
  { value: 'openai', label: 'OpenAI', icon: <OpenAIMark /> },
  { value: 'anthropic', label: 'Anthropic', icon: <ClaudeMark /> },
  { value: 'google', label: 'Google', icon: <GeminiMark /> },
  { value: 'mistral', label: 'Mistral', icon: <MistralMark /> },
];

const MODELS: ModelPickerItem[] = [
  { value: 'gpt-5.5', label: 'GPT-5.5', provider: 'openai', contextWindow: 400_000 },
  { value: 'gpt-5.4-thinking', label: 'GPT-5.4 Thinking', provider: 'openai', contextWindow: 400_000 },
  { value: 'claude-opus-5.5', label: 'Claude Opus 5.5', provider: 'anthropic', contextWindow: 1_000_000 },
  { value: 'claude-sonnet-5.5', label: 'Claude Sonnet 5.5', provider: 'anthropic', contextWindow: 1_000_000 },
  { value: 'gemini-3.1-pro', label: 'Gemini 3.1 Pro', provider: 'google', contextWindow: 1_000_000 },
  { value: 'gemini-3-flash', label: 'Gemini 3 Flash', provider: 'google', contextWindow: 1_000_000 },
  { value: 'mistral-large-3', label: 'Mistral Large 3', provider: 'mistral', contextWindow: 256_000 },
];

export default function ModelPickerList() {
  return (
    <ModelPicker variant="list" items={MODELS} providers={PROVIDERS} defaultValue="claude-opus-5.5" defaultFavorites={['claude-opus-5.5']}>
      <ModelPicker.Trigger appearance="outline" />
      <ModelPicker.Content />
    </ModelPicker>
  );
}

Cards

Each model as a card with its description and tags, under a row of provider filters. The arrow keys move across the grid as well as down it.

'use client';

import { ClaudeMark, GeminiMark, MetaMark, OpenAIMark, XAIMark } from '@/components/model-logos';
import { ModelPicker, type ModelPickerItem, type ModelPickerProvider } from '@/components/panelui/model-picker';

const PROVIDERS: ModelPickerProvider[] = [
  { value: 'anthropic', label: 'Anthropic', icon: <ClaudeMark /> },
  { value: 'openai', label: 'OpenAI', icon: <OpenAIMark /> },
  { value: 'google', label: 'Google', icon: <GeminiMark /> },
  { value: 'xai', label: 'xAI', icon: <XAIMark /> },
  { value: 'meta', label: 'Meta', icon: <MetaMark /> },
];

const MODELS: ModelPickerItem[] = [
  { value: 'claude-opus-5.5', label: 'Claude Opus 5.5', provider: 'anthropic', description: 'Long agent runs and careful refactors across a whole repository.', tags: ['Code', 'Agents'] },
  { value: 'claude-haiku-5.5', label: 'Claude Haiku 5.5', provider: 'anthropic', description: 'Quick answers, triage and autocomplete.', tags: ['Fast'] },
  { value: 'gpt-5.5', label: 'GPT-5.5', provider: 'openai', badge: 'New', description: 'Research that reads widely and writes a report with sources.', tags: ['Research', 'Tools'] },
  { value: 'gpt-5.4-thinking', label: 'GPT-5.4 Thinking', provider: 'openai', description: 'Thinks before answering, for maths and planning.', tags: ['Reasoning'] },
  { value: 'gemini-3.1-pro', label: 'Gemini 3.1 Pro', provider: 'google', description: 'Video, audio and very long documents in one prompt.', tags: ['Vision', 'Long context'] },
  { value: 'grok-4', label: 'Grok 4', provider: 'xai', description: 'Answers about what is happening right now.', tags: ['Live search'] },
  { value: 'llama-4-maverick', label: 'Llama 4 Maverick', provider: 'meta', description: 'Open weights, for running on your own servers.', tags: ['Open', 'Self-host'] },
];

export default function ModelPickerCards() {
  return (
    <ModelPicker variant="cards" items={MODELS} providers={PROVIDERS} defaultValue="gemini-3.1-pro">
      <ModelPicker.Trigger appearance="outline" />
      <ModelPicker.Content />
    </ModelPicker>
  );
}

Compare

Speed, intelligence and price side by side, from each model's speed, intelligence (1 to 5) and price (1 to 4). Use it where the choice is a trade-off the reader should see.

'use client';

import { ClaudeMark, DeepSeekMark, GeminiMark, MistralMark, OpenAIMark } from '@/components/model-logos';
import { ModelPicker, type ModelPickerItem, type ModelPickerProvider } from '@/components/panelui/model-picker';

const PROVIDERS: ModelPickerProvider[] = [
  { value: 'anthropic', label: 'Anthropic', icon: <ClaudeMark /> },
  { value: 'openai', label: 'OpenAI', icon: <OpenAIMark /> },
  { value: 'google', label: 'Google', icon: <GeminiMark /> },
  { value: 'mistral', label: 'Mistral', icon: <MistralMark /> },
  { value: 'deepseek', label: 'DeepSeek', icon: <DeepSeekMark /> },
];

/** Speed and intelligence from 1 to 5, price from 1 to 4. */
const MODELS: ModelPickerItem[] = [
  { value: 'claude-opus-5.5', label: 'Claude Opus 5.5', provider: 'anthropic', speed: 2, intelligence: 5, price: 4 },
  { value: 'claude-sonnet-5.5', label: 'Claude Sonnet 5.5', provider: 'anthropic', speed: 4, intelligence: 4, price: 3 },
  { value: 'claude-haiku-5.5', label: 'Claude Haiku 5.5', provider: 'anthropic', speed: 5, intelligence: 3, price: 1 },
  { value: 'gpt-5.5', label: 'GPT-5.5', provider: 'openai', speed: 3, intelligence: 5, price: 4 },
  { value: 'gemini-3-flash', label: 'Gemini 3 Flash', provider: 'google', speed: 5, intelligence: 3, price: 1 },
  { value: 'mistral-large-3', label: 'Mistral Large 3', provider: 'mistral', speed: 4, intelligence: 3, price: 2 },
  { value: 'deepseek-v4', label: 'DeepSeek V4', provider: 'deepseek', speed: 3, intelligence: 4, price: 1 },
];

export default function ModelPickerCompare() {
  return (
    <ModelPicker variant="compare" items={MODELS} providers={PROVIDERS} defaultValue="claude-sonnet-5.5">
      <ModelPicker.Trigger appearance="outline" />
      <ModelPicker.Content />
    </ModelPicker>
  );
}

Detail

A pane beside the list describing the highlighted model: its description, context window, meters and tags. Moving through the list with the arrow keys updates it. On a narrow screen the pane is left out.

'use client';

import { ClaudeMark, GeminiMark, OpenAIMark, XAIMark } from '@/components/model-logos';
import { ModelPicker, type ModelPickerItem, type ModelPickerProvider } from '@/components/panelui/model-picker';

const PROVIDERS: ModelPickerProvider[] = [
  { value: 'anthropic', label: 'Anthropic', icon: <ClaudeMark /> },
  { value: 'openai', label: 'OpenAI', icon: <OpenAIMark /> },
  { value: 'google', label: 'Google', icon: <GeminiMark /> },
  { value: 'xai', label: 'xAI', icon: <XAIMark /> },
];

const MODELS: ModelPickerItem[] = [
  {
    value: 'claude-opus-5.5',
    label: 'Claude Opus 5.5',
    provider: 'anthropic',
    description: 'For work that takes hours: migrations, audits and long agent runs.',
    contextWindow: 1_000_000,
    speed: 2,
    intelligence: 5,
    price: 4,
    tags: ['Code', 'Agents', 'Long context'],
  },
  {
    value: 'claude-sonnet-5.5',
    label: 'Claude Sonnet 5.5',
    provider: 'anthropic',
    description: 'The everyday choice: quick enough to chat with, strong enough to ship code.',
    contextWindow: 1_000_000,
    speed: 4,
    intelligence: 4,
    price: 3,
    tags: ['Code', 'Writing'],
  },
  {
    value: 'gpt-5.5',
    label: 'GPT-5.5',
    provider: 'openai',
    description: 'Uses tools and the web on its own to finish a task end to end.',
    contextWindow: 400_000,
    speed: 3,
    intelligence: 5,
    price: 4,
    tags: ['Tools', 'Research'],
  },
  {
    value: 'gemini-3.1-pro',
    label: 'Gemini 3.1 Pro',
    provider: 'google',
    description: 'Reads video, audio, images and very long documents in one prompt.',
    contextWindow: 1_000_000,
    speed: 3,
    intelligence: 5,
    price: 3,
    tags: ['Vision', 'Audio'],
  },
  {
    value: 'grok-4',
    label: 'Grok 4',
    provider: 'xai',
    description: 'Looks things up as it answers, for questions about the news.',
    contextWindow: 256_000,
    speed: 3,
    intelligence: 4,
    price: 3,
    tags: ['Live search'],
  },
];

export default function ModelPickerDetail() {
  return (
    <ModelPicker variant="detail" items={MODELS} providers={PROVIDERS} defaultValue="claude-sonnet-5.5">
      <ModelPicker.Trigger appearance="outline" />
      <ModelPicker.Content />
    </ModelPicker>
  );
}

Controlled

value with onValueChange, and favorites with onFavoritesChange, hold the choice and the stars outside the picker, to keep them in a user's settings.

'use client';

import { ClaudeMark, GeminiMark, OpenAIMark } from '@/components/model-logos';
import { ModelPicker, type ModelPickerItem, type ModelPickerProvider } from '@/components/panelui/model-picker';
import { useState } from 'react';

const PROVIDERS: ModelPickerProvider[] = [
  { value: 'anthropic', label: 'Anthropic', icon: <ClaudeMark /> },
  { value: 'openai', label: 'OpenAI', icon: <OpenAIMark /> },
  { value: 'google', label: 'Google', icon: <GeminiMark /> },
];

const MODELS: ModelPickerItem[] = [
  { value: 'claude-sonnet-5.5', label: 'Claude Sonnet 5.5', provider: 'anthropic' },
  { value: 'claude-haiku-5.5', label: 'Claude Haiku 5.5', provider: 'anthropic' },
  { value: 'gpt-5.5', label: 'GPT-5.5', provider: 'openai' },
  { value: 'gemini-3-flash', label: 'Gemini 3 Flash', provider: 'google' },
];

export default function ModelPickerControlled() {
  const [model, setModel] = useState('claude-haiku-5.5');
  const [starred, setStarred] = useState<string[]>(['claude-haiku-5.5', 'gemini-3-flash']);
  const chosen = MODELS.find((item) => item.value === model);

  return (
    <div className="flex flex-col items-center gap-3">
      <ModelPicker
        variant="list"
        items={MODELS}
        providers={PROVIDERS}
        value={model}
        onValueChange={setModel}
        favorites={starred}
        onFavoritesChange={setStarred}
      >
        <ModelPicker.Trigger appearance="outline" />
        <ModelPicker.Content align="center" />
      </ModelPicker>
      <p className="text-sm text-muted-foreground">
        Replies come from {chosen?.label}. {starred.length} starred.
      </p>
    </div>
  );
}

In a prompt

The ghost trigger in a Prompt's footer, opening upwards with side="top" since the prompt sits at the bottom of the page.

'use client';

import { ClaudeMark, GeminiMark, OpenAIMark } from '@/components/model-logos';
import { ModelPicker, type ModelPickerItem, type ModelPickerProvider } from '@/components/panelui/model-picker';
import { Prompt } from '@/components/panelui/prompt';

const PROVIDERS: ModelPickerProvider[] = [
  { value: 'anthropic', label: 'Anthropic', icon: <ClaudeMark /> },
  { value: 'openai', label: 'OpenAI', icon: <OpenAIMark /> },
  { value: 'google', label: 'Google', icon: <GeminiMark /> },
];

const MODELS: ModelPickerItem[] = [
  { value: 'claude-opus-5.5', label: 'Claude Opus 5.5', provider: 'anthropic' },
  { value: 'claude-sonnet-5.5', label: 'Claude Sonnet 5.5', provider: 'anthropic' },
  { value: 'gpt-5.5', label: 'GPT-5.5', provider: 'openai' },
  { value: 'gpt-5.4-thinking', label: 'GPT-5.4 Thinking', provider: 'openai' },
  { value: 'gemini-3.1-pro', label: 'Gemini 3.1 Pro', provider: 'google' },
];

export default function ModelPickerPrompt() {
  return (
    <div className="w-full max-w-xl">
      <Prompt>
        <Prompt.Field>
          <Prompt.Textarea placeholder="Plan a three-day trip to Lisbon" minRows={2} />
          <Prompt.Footer>
            <Prompt.Tools>
              <Prompt.Menu />
              <ModelPicker items={MODELS} providers={PROVIDERS} defaultValue="claude-sonnet-5.5" defaultFavorites={['claude-sonnet-5.5', 'gpt-5.5']}>
                <ModelPicker.Trigger />
                <ModelPicker.Content side="top" />
              </ModelPicker>
            </Prompt.Tools>
            <Prompt.Submit />
          </Prompt.Footer>
        </Prompt.Field>
      </Prompt>
    </div>
  );
}

API Reference

ModelPicker

PropTypeDefaultDescription
itemsreadonly ModelPickerItem[]—The models to choose from, in the order they are listed.
providersreadonly ModelPickerProvider[]—The providers the models come from, in the order the rail and the headings show them.
valuestring | null—The chosen model, when the caller holds it.
defaultValuestring | nullnullThe model chosen to begin with, when the caller does not hold it.
onValueChange(value: string) => void—Called with the model's value when another is chosen.
favoritesreadonly string[]—The starred models, when the caller holds them.
defaultFavoritesreadonly string[][]The models starred to begin with.
onFavoritesChange(favorites: string[]) => void—Called with every starred model when one is starred or unstarred.
variantModelPickerVariant'rail'The layout of the list: rail, list, cards, compare or detail. Default rail.
hotkeystring—Keys that open it from anywhere on the page, such as alt+m. mod is ⌘ on a Mac and Ctrl elsewhere. Avoid keys the browser keeps for itself, such as ⌘1 to ⌘9, ⌘T and ⌘⇧M, which a page never receives.
openboolean—Whether the list is open, when the caller holds it.
defaultOpenbooleanfalseWhether the list is open to begin with.
onOpenChange(open: boolean) => void—Called when the list opens or closes.
labelstring'Model'What a screen reader calls the trigger and the list. Default Model.

ModelPicker.Trigger

PropTypeDefaultDescription
appearance'ghost' | 'outline'ghostghost has no box until hovered, for a composer's footer. outline has an edge, for a toolbar. Default ghost.
placeholderReactNode'Search models…'What it says before a model is chosen. Default Choose a model.
classNamestring—

ModelPicker.Content

PropTypeDefaultDescription
sidePopover.Positioner.Props['side']'bottom'Which side of the trigger it opens on. Default bottom.
alignPopover.Positioner.Props['align']'start'How it lines up with the trigger along that side. Default start.
sideOffsetnumber8The gap between the trigger and the list, in pixels. Default 8.
placeholderstring'Search models…'What the search field says while empty. Default Search models….
emptyTextReactNode'No models match'What the list says when nothing matches. Default No models match.
containerPopover.Portal.Props['container']—Where in the document it is put. Default document.body.
classNamestring—

Every part also takes the props of the element it renders, and a className.

Notes

Keyboard

With the list open, the search field has focus. The arrow keys move the highlight, Enter chooses the highlighted model, and Escape closes the list. ⌥1 to ⌥9, or Alt+1 to Alt+9 on Windows and Linux, choose the row with that number, and ⌥S, or Alt+S, stars or unstars the highlighted model. Tab and Shift+Tab move between the search field and the provider filters.

Accessibility

The trigger's accessible name is the root's label and the chosen model, such as "Model: Claude Sonnet 5.5". The list is a listbox named by the search field. The meters in compare and detail are read as "Speed 4 of 5".

Searching

The search matches every word typed against a model's name, its provider's name, its description and its tags, in any order, so "fast google" finds a Gemini model tagged Fast.

Choosing a hotkey

A page never receives the keys a browser keeps for itself: ⌘1 to ⌘9 and Ctrl+1 to Ctrl+9 switch tabs, and in Chrome on a Mac ⌘⇧M opens the profile menu. That is why the row numbers are on ⌥. Pick a hotkey the browser leaves alone, such as alt+m.

Server Components

ModelPicker is a client component. In a file that starts with "use client", write its parts with a dot, as on this page. A Server Component cannot read ModelPicker.Trigger off the import, so each part is also exported by name: ModelPickerTrigger is ModelPicker.Trigger, and so on for the others.

import { ModelPicker, ModelPickerTrigger, ModelPickerContent } from "@/components/panelui/model-picker"

On this page