DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
ChatBubbleStreamingResponseCitationsToolCallPlanListParameterDiffModelSelectorElicitationArtifactPanelPromptInputKeyboardToolbar
AI agentsContributing
  1. Components
  2. ModelSelector

ModelSelector

An AI model picker grouped by provider — per-model context window, cost tier and capability badges, disabled models that say why, searchable once the list is long, and a compact ghost trigger for the composer bar.

$ pnpm add @tessinaui/ui

When to use

  • Choosing which AI model answers, in a chat composer, a settings form or a playground.
  • Options that need more than a name: context window, cost tier, capabilities, or why a model is not available right now.
  • A list grouped by provider. Set provider on each model and the sections follow, in the order each provider first appears.
  • A model that is unavailable but worth showing. Give it disabled and a disabledReason; it stays in the list, reachable by keyboard, with the reason where the description would be.
  • A long list. Turn on searchable and the trigger becomes a text field that filters as you type.

When not to use

  • A flat mode or scope pill in the composer ("Search", "Deep research", "Computer"). Use PromptInputModelSelect, the generic single-select pill; it takes a model list too. Reach for ModelSelector when the list needs provider groups, metadata or search.
  • Plain options with no metadata. A Select is enough.
  • A searchable list of ordinary values. Use a Combobox; ModelSelector's searchable is for model rows.
  • Choosing several items. It picks one model at a time.
  • A real price list. costTier is a three-step hint, not a price; put real pricing in description if it matters.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { ModelSelector } from "@tessinaui/ui/model-selector";

<ModelSelector
  defaultValue="kestrel-swift"
  models={[
    { value: "auto", label: "Auto", description: "Picks a model for each message" },
    {
      value: "kestrel-pro",
      label: "Kestrel Pro",
      provider: "Kestrel",
      description: "Most capable for complex work",
      contextWindow: 200_000,
      costTier: "high",
      capabilities: ["vision", "tools"],
    },
    {
      value: "kestrel-max",
      label: "Kestrel Max",
      provider: "Kestrel",
      disabled: true,
      disabledReason: "Not available on your plan",
    },
  ]}
/>

Models are one flat array. provider becomes the section heading, in the order each provider first appears; models with no provider sit at the top.

Examples

Default

Grouped by provider, with each model's context window, cost tier and capabilities in a trailing column.

Disabled with a reason

A model that cannot be picked stays in the list, and its reason takes the description line. The keyboard still reaches it, so the reason is read, not just seen.

Searchable

Past about fifteen models, turn on searchable: the trigger becomes a text field that filters as you type (a Combobox), with an empty state when nothing matches.

Validation

errorMessage replaces the supportingText line, turns the boundary to the error intent and marks the control invalid, in both the button and the search field. Clear it as soon as a model is picked.

In the composer

variant="ghost" is the compact trigger for a composer toolbar: no well, a visually hidden label for its name, and w-auto on both the root and the trigger so it sizes to the model it shows. w-auto shrinks it only inside a flex row, as the toolbar is; in a block parent it still fills the width.

Model menu

ModelMenu is the menu form: a button you supply opens a short list of models, one click picks, and the chosen row ends in a check. Each row can carry an icon at its start and a quiet note before the check, such as the maker or a price. Use it beside other menu triggers in a header or toolbar. Pass children for content after the models, a separator and an effort submenu, say, or a footer. The trigger names itself, so say the chosen model in it.

Right to left

dir="rtl" mirrors the layout and defaults every string to Arabic, including the spoken description of each model.

On color

The trigger reads the plate's tone. The popup is a page surface, so its rows keep the page's inks.

API Reference

ModelSelector

PropTypeDefaultDescription
modelsModelSelectorModel[]—The models, flat. Grouped by provider
valuestring | null—Controlled selection
defaultValuestring | nullnullUncontrolled initial selection
onValueChange(value: string | null) => void—Called with the picked model's value
searchablebooleanfalseA text field that filters as you type (Combobox) instead of a button (Select)
size"xs" | "sm" | "md" | "lg" | "xl""md"The input family's size ladder
variant"outline" | "ghost" | "underline""outline"ghost is the composer trigger
intent"none" | "error" | "warning" | "success" | "info""none"Boundary tint
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""full"Trigger radius
tone"default" | "on-color"inheritedInherited from a wrapping <Surface>
disabledbooleanfalseDisables the whole control
labelstring"Model"The control's accessible name
visuallyHiddenLabelbooleanfalseKeeps the label for AT, hides it on screen
placeholderstring"Select a model" / "Search models…"Shown with nothing selected
requiredbooleanfalseStar on the label and required on the control
supportingTextstring—Helper line under the trigger, joined into its description
errorMessagestring—Validation message. Replaces supportingText, implies intent="error" and marks the control invalid
dir"ltr" | "rtl"—Text direction; "rtl" also picks the Arabic strings
stringsPartial<ModelSelectorStrings>—Per-entry string overrides
classNamestring—Classes on the root wrapper
triggerClassNamestring—Classes on the trigger (Select) or the field (Combobox)

aria-* props land on the control; everything else lands on the root.

ModelSelectorModel

FieldTypeDescription
valuestringWhat onValueChange reports
labelstringThe option's accessible name, alone
providerstringSection heading
descriptionstringOne line under the label
contextWindownumberTokens, rendered compact ("200K")
costTier"low" | "medium" | "high"Shown as $ / $$ / $$$, spoken as a word
capabilitiesstring[]"vision", "tools", "reasoning" and "web" are localized; anything else renders as given
disabledbooleanIn the list, reachable, not selectable
disabledReasonstringWhy. Replaces the description line; defaults to "Unavailable"
iconReactNodeDecorative glyph at the row's start

ModelMenu

PropTypeDefaultDescription
modelsModelMenuModel[]—The models, flat
valuestring—The chosen model's value. Controlled
onValueChange(value: string) => void—Called with the picked model's value
triggerReactElement—The button that opens the menu. It names itself, so say the chosen model
labelstring"Model"Names the group of models
size"xs" | "sm" | "md" | "lg" | "xl""sm"Menu density
align"start" | "center" | "end""end"Which edge of the trigger the menu lines up with
contentClassNamestring—Classes for the menu panel, its width
quietCheckbooleanfalseDraw the chosen row's check in the row's own icon ink rather than the strong foreground, for a menu whose notes carry their own quiet ink
childrenReactNode—Content after the models: a separator and a submenu, a footer

ModelMenuModel has value, label (the row's accessible name, alone), icon (decorative), note (quiet text before the check; say what it means in label or in screen-reader-only text inside it) and disabled.

composeModelDescription

composeModelDescription(model, strings, locale) returns the sentence an option is described by — exported for tests and for anything that lists models outside the picker.

DOM contract

AttributeWhereValues
data-componentthe rootmodel-selector
data-sloteach option · its metadata columnmodel-selector-option · model-selector-meta
aria-disableda disabled optiontrue
aria-describedbyevery option with metadatathe composed sentence

Accessibility

  • Two bases, on purpose. Without searchable the control is a Select: a button that opens a listbox. With it, a Combobox: a text field that filters a listbox. A control with no text field is a Select, so the role follows the choice rather than the list length.
  • The name is the label alone. An option announces as "Kestrel Pro", never as its whole row. Everything else — the description, "200K context", "supports vision and tools", the cost — is one sentence wired through aria-describedby, read after the name. The badges and the metadata column are aria-hidden decoration whose meaning is already in that sentence.
  • A disabled model says why. It stays in the list with aria-disabled, the arrow keys reach it, and its description leads with the reason. It cannot be picked by keyboard or pointer. Typeahead skips it, as it does in a native <select>.
  • An error is text, not only colour. errorMessage sets aria-invalid, joins the message to the control's description and announces it through a polite live region; the red boundary only repeats it.
  • Keyboard: the house Select and Combobox contracts — arrows move, Enter picks, Escape closes, Home/End jump; in the searchable field typing filters.
  • 375px: below md the metadata column moves under the description as one compact line — "200K context · $$$ · vision, tools" — that wraps instead of truncating. Phone readers see what the sentence gives a screen reader, and rows keep the family's 44px touch floor and 16px touch type.

AT matrix: NOT yet screen-reader tested (hence beta). NVDA and JAWS: untested, no Windows host (QA §5.6).

Notes

  • Vocabulary: this component, ModelMenu and PromptInputModelSelect all pick a model. ModelMenu is the short menu with a trigger of your own. The pill is the generic composer select — flat list, label, meta, description. ModelSelector is the model-specific one: groups, metadata, a disabled reason, search.
  • No skeleton export. Wrap the real thing while it loads — <Skeleton loading={isPending} inline rounded="full"><ModelSelector … /></Skeleton> — so the placeholder is the same box (§2b).
  • Cost is relative. costTier is a three-step hint, not a price; put real pricing in description if it matters.
ParameterDiffElicitation

On this page

When to useWhen not to useInstallationUsageExamplesDefaultDisabled with a reasonSearchableValidationIn the composerModel menuRight to leftOn colorAPI ReferenceModelSelectorModelSelectorModelModelMenucomposeModelDescriptionDOM contractAccessibilityNotes