Skip to content
Scrim UI

Model Selector UI

A standalone model picker — compare models in a dropdown with badges for speed, context length and what each supports.

modelselectordropdownsettings

Preview

Presets

Props

Also takes onSelect.

Agent promptClaude Code · Cursor · any agent
Add the Model Selector UI component from the Scrim UI registry to this project and use it in the configuration described below.

Model Selector UI — A standalone model picker — compare models in a dropdown with badges for speed, context length and what each supports.

## 1. Install

```bash
npx shadcn@latest add @scrimui/model-selector
```

This writes a single file to `components/ui/model-selector.tsx`. It is plain React + Tailwind with no runtime dependencies — no Radix, no CVA, nothing to add to package.json. If this project does not use the shadcn CLI, copy the source from https://scrimui.dev/components/model-selector to the same path by hand; nothing in the file depends on shadcn.

## 2. Use it

```tsx
import { ModelSelector } from "@/components/ui/model-selector";

const MODELS = [
  { id: "claude", name: "Claude Sonnet 5", hint: "Balanced speed and reasoning", badges: ["Default"] },
  { id: "gpt", name: "GPT-5.6 Sol", hint: "Multimodal, fast", badges: ["Popular"] },
  { id: "gemini", name: "Gemini 3.1 Pro", hint: "1M token context" },
  { id: "deepseek", name: "DeepSeek-V4-Pro", hint: "Lowest cost per token", badges: ["Cheap"] },
];

<ModelSelector
  value="claude"
  placeholder="Choose a model"
  options={MODELS}
  onSelect={() => {}}
/>
```

## 3. The configuration that matters

This is the component's "Closed" state — The resting state — current model and its headline badge.

Every prop is at its default value. Keep the call site minimal — do not write out default values.

## 4. Wire the handlers

`onSelect` is a stub in the snippet above (`() => {}`). Connect it to this project's own state and data layer instead of shipping the empty function.

Reference: https://scrimui.dev/components/model-selector

Follows the props below — change a control and the prompt changes with it, so an agent reproduces that configuration instead of the defaults.

Install

npx shadcn@latest add @scrimui/model-selector

Component source

React + Tailwind component using shadcn/ui (badge, button). The install command adds these dependencies automatically.

When copying source manually, install the required primitives first:

npx shadcn@latest add badge button
model-selector.tsx
"use client";

import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import * as React from "react";

/* ------------------------------------------------------------------ */
/* Types                                                               */
/* ------------------------------------------------------------------ */

export type ModelOption = {
  id: string;
  name: string;
  hint: string;
  badges?: string[];
  /**
   * Optional leading mark, e.g. the provider's logo. A slot rather than a
   * built-in lookup so this component stays dependency-free — pass whatever
   * icon element you already have.
   */
  icon?: React.ReactNode;
};

export type ModelSelectorProps = {
  options: ModelOption[];
  value?: string;
  onSelect?: (id: string) => void;
  placeholder?: string;
  defaultOpen?: boolean;
  className?: string;
};

/* ------------------------------------------------------------------ */
/* Icons                                                               */
/* ------------------------------------------------------------------ */

function CheckIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.5"
      strokeLinecap="round"
      strokeLinejoin="round"
      width="12"
      height="12"
      {...props}
    >
      <path d="M20 6 9 17l-5-5" />
    </svg>
  );
}

/* ------------------------------------------------------------------ */
/* ModelSelector                                                       */
/* ------------------------------------------------------------------ */

export function ModelSelector({
  options,
  value,
  onSelect,
  placeholder = "Choose a model",
  defaultOpen = false,
  className = "",
}: ModelSelectorProps) {
  const [open, setOpen] = React.useState(defaultOpen);
  const selected = options.find((o) => o.id === value) ?? options[0];

  return (
    <div className={`relative ${className}`}>
      <Button variant="outline" size="sm"
        type="button"
        onClick={() => setOpen((o) => !o)}
        aria-haspopup="listbox"
        aria-expanded={open}
        className="h-auto min-h-8 whitespace-normal justify-start inline-flex h-9 w-full items-center justify-between gap-2 rounded-md border border-border bg-card px-3 text-sm text-foreground transition-colors hover:bg-muted"
      >
        <span className="flex min-w-0 items-center gap-2">
          <span className="truncate font-medium">
            {selected?.name ?? placeholder}
          </span>
          {selected?.badges?.[0] && (
            <Badge variant="secondary" className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">
              {selected.badges[0]}
            </Badge>
          )}
        </span>
        <svg
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinecap="round"
          strokeLinejoin="round"
          width="14"
          height="14"
          className={`shrink-0 transition-transform ${open ? "rotate-180" : ""}`}
        >
          <path d="m6 9 6 6 6-6" />
        </svg>
      </Button>

      {open && (
        <>
          {/* Click-away backdrop */}
          <div className="fixed inset-0 z-10" onClick={() => setOpen(false)} />
          {/* A div, not a ul: role="listbox" overrides the ul's implicit list
              role, which makes every <li> inside it an orphaned listitem, and
              role="option" must be a *direct* child of the listbox — the <li>
              wrapper broke that too. Buttons are not valid children of <ul>
              anyway. */}
          <div
            role="listbox"
            aria-label="Model"
            className="absolute left-0 right-0 top-full z-20 mt-1.5 overflow-hidden rounded-xl border border-border bg-card shadow-md"
          >
            {options.map((opt) => {
              const active = opt.id === selected?.id;
              return (
                <Button variant="ghost" size="sm"
                  key={opt.id}
                  type="button"
                  role="option"
                  aria-selected={active}
                  onClick={() => {
                    onSelect?.(opt.id);
                    setOpen(false);
                  }}
                  className={`h-auto min-h-8 whitespace-normal justify-start flex w-full items-start gap-3 px-3.5 py-2.5 text-left transition-colors hover:bg-muted ${
                    active ? "bg-muted" : ""
                  }`}
                >
                  <div className="min-w-0 flex-1">
                    <div className="flex items-center gap-1.5">
                      {opt.icon}
                      <span className="text-sm font-medium text-foreground">
                        {opt.name}
                      </span>
                      {active && (
                        <span className="text-muted-foreground">
                          <CheckIcon />
                        </span>
                      )}
                    </div>
                    <p className="mt-0.5 text-xs text-muted-foreground">
                      {opt.hint}
                    </p>
                  </div>
                  {opt.badges && opt.badges.length > 0 && (
                    <div className="flex shrink-0 flex-wrap items-center gap-1 pt-0.5">
                      {opt.badges.map((b) => (
                        <Badge variant="secondary"
                          key={b}
                          className="rounded-full bg-muted px-1.5 py-0.5 text-xs text-muted-foreground"
                        >
                          {b}
                        </Badge>
                      ))}
                    </div>
                  )}
                </Button>
              );
            })}
          </div>
        </>
      )}
    </div>
  );
}

When to use it

  • Show capability badges (reasoning, tools, speed) so users pick on substance, not name.
  • Default to the model that best serves the current task, not the most powerful one.
  • Remember the user's last choice — re-picking a model every session is friction.
  • Label speed/cost trade-offs honestly; 'fast' next to a slow model destroys trust.

What breaks in production

  • Listing models with no hints — users choose blindly between identical-looking rows.
  • Burying the selected model so the current choice is never visible.
  • Making the dropdown unusable on mobile (too wide, tiny touch targets).

Related Components