Model select
A select for AI models, each with a one-line description and mono meta.
npx shadcn@latest add https://ui.goodtabs.co/r/model-select.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/model-select
Four Claude models with a description and context window each.
With a retired model
A disabled legacy option stays in the list so people know where it went.
Compact
No descriptions and the sm size: single-line items at 28px.
Usage
import { ModelSelect } from "@/components/ui/model-select"
<ModelSelect
models={[
{ value: "claude-fable-5-1", label: "Claude Fable 5.1", description: "Long agentic sessions.", meta: "1M context" },
{ value: "claude-haiku-5-5", label: "Claude Haiku 5.5", description: "Fastest for simple calls.", meta: "500K context" },
]}
value={model}
onValueChange={setModel}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| models | { value: string; label: string; description?: string; meta?: string; disabled?: boolean }[] | The models to choose from, in order. | |
| value, defaultValue, onValueChange | string, string, (value: string) => void | Controlled or uncontrolled, as on radix Select. | |
| width | string | "14rem" | Width of the trigger, any CSS length. |
| placeholder | string | "Choose a model" | Trigger text before a choice. |
| size | "sm" | "default" | "default" | Trigger height: 28px or 32px. |
| aria-label | string | "Model" | Accessible name of the trigger. |
Source
model-select/model-select.tsx
"use client";
import * as React from "react";
import { CheckIcon } from "lucide-react";
import { Select as SelectPrimitive } from "radix-ui";
import { Select, SelectContent, SelectGroup, SelectTrigger, SelectValue } from "@/components/ui/select";
import { cn } from "@/lib/utils";
export type ModelOption = {
value: string;
label: string;
/** One line under the label, in the muted tone. */
description?: string;
/** Mono meta on the right: context window, price. */
meta?: string;
disabled?: boolean;
};
type ModelSelectProps = Omit<React.ComponentProps<typeof Select>, "children"> & {
/** The models to choose from, in order. */
models: ModelOption[];
/** Width of the trigger, any CSS length. */
width?: string;
/** Trigger text before a choice is made. */
placeholder?: string;
/** Trigger height: 28px or 32px. */
size?: "sm" | "default";
/** Accessible name of the trigger. */
"aria-label"?: string;
/** Classes for the trigger. */
className?: string;
};
/**
* A select for AI models. Each option carries a label, a one-line description and a mono meta line.
* The trigger shows the label only, so it stays one line wide.
*/
export function ModelSelect({
models,
width = "14rem",
placeholder = "Choose a model",
size = "default",
"aria-label": ariaLabel = "Model",
className,
...props
}: ModelSelectProps) {
return (
<Select {...props}>
<SelectTrigger
size={size}
aria-label={ariaLabel}
data-slot="model-select"
className={cn("max-w-full", className)}
style={{ width }}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent position="popper" align="start" className="min-w-64 rounded-md border shadow-md ring-0">
<SelectGroup>
{models.map((model) => {
const detailed = Boolean(model.description);
return (
<SelectPrimitive.Item
key={model.value}
value={model.value}
disabled={model.disabled}
className={cn(
"group/model-item relative flex w-full cursor-default flex-col justify-center gap-0.5 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-muted focus:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
!detailed && "h-8 py-0",
)}
>
<span className="pointer-events-none absolute top-1/2 right-2 flex size-4 -translate-y-1/2 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" aria-hidden="true" />
</SelectPrimitive.ItemIndicator>
</span>
<span className="flex items-center gap-3">
<SelectPrimitive.ItemText>{model.label}</SelectPrimitive.ItemText>
{model.meta ? (
<span className="ml-auto shrink-0 font-mono text-xs text-muted-foreground group-focus/model-item:text-foreground/70">
{model.meta}
</span>
) : null}
</span>
{model.description ? (
<span className="text-xs text-muted-foreground group-focus/model-item:text-foreground/70">
{model.description}
</span>
) : null}
</SelectPrimitive.Item>
);
})}
</SelectGroup>
</SelectContent>
</Select>
);
}
For AI agents
Hand your agent this page, the markdown at /components/model-select/prompt.md, or the Copy AI prompt button above.
When to use
- Choosing a model in a chat composer, playground or settings page
- Any select where each option needs a line of help and a figure such as a limit or price
- When the trigger must stay compact while the options carry detail
When not to use
- For long lists that need search; build a combobox on Command instead
- For options without descriptions, use the plain shadcn Select
- For switching between two or three modes inline, use segmented-tabs
Agent notes
- A demo or parent that passes onValueChange must be a client component
- Items are built on radix Select.Item directly so only the label lands in the trigger; the description and meta stay in the list
- Options without a description render as single 32px rows; mix them freely with detailed ones
- The content opens in popper mode under the trigger and is at least 16rem wide; width sets the trigger only