AI Model Selector

Pro

4 variants: dropdown picker, card grid, side-by-side comparison and settings panel — with provider badges, capability tags and latency/cost/context indicators.

</>sv-ai-model-selector aimodelselector
import { SvAiModelSelectorComponent } from '@styloviz-pro/ai-model-selector';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Grid & comparison

Pro

When to use

  • Letting users choose an LLM with provider, capability, latency, cost and context info.
  • AI products that expose multiple models or providers.
  • For a generic single-choice list, a Select Dropdown is simpler.

Async states

  • loading
  • empty
  • error
  • retry

Item identity & owned state

Restorable state
  • compareIds

Bind these to put a saved view back — the open sections, the hidden columns, the selected rows — and read their Change events to store what the user does.

Inputs

NameTypeDefaultDescription
loadingbooleanfalseTrue while the data is being fetched.
errorSvDataError | string | nullnullLoad failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`.
emptyMessagestring''Shown when no models are on offer.
overlayContainerHTMLElement | nullnullWhere the dropdown panel is mounted while it is open. `null` — the default and the supported path — means `document.body`. AN APP-WIDE OVERLAY STACK IS A REAL THING AND IT OWNS THE TOP OF THE PAGE. A host with a modal layer, a toast layer and a command palette has one element every overlay must live inside so that its own ordering rules hold; a component that appends to `<body>` regardless is outside those rules, and lands under or over the wrong thing depending on document order — which no z-index on our side can fix, because stacking contexts do not compare across them. AND IT CHANGES WHAT `position: fixed` MEANS, WHICH IS THE PART THAT BITES. A `transform`, `filter`, `perspective`, `will-change: transform` or `contain: paint` on ANY ancestor of the container makes that ancestor the containing block for fixed descendants, so viewport coordinates are silently re-based on its box and the panel appears offset by wherever that ancestor happens to sit — a bug that looks like "the dropdown is in the wrong place on one page". `_computePanelPosition` finds that ancestor and converts into its space (`e2e/ai-model-selector.at.spec.ts` proves both halves; jsdom has no layout and cannot see either).
overlayZIndexnumberSV_MODEL_OVERLAY_Z_INDEXThe panel's `z-index` within its container. THE DEFAULT IS A GUESS ABOUT SOMEBODY ELSE'S PAGE, so it has to be overridable. `9999` wins against most things and loses against a host whose modal layer sits higher, and a dropdown rendered UNDER the dialog that opened it is unusable rather than merely ugly.
variantModelSelectorVariant'dropdown'Presentation: dropdown, card grid, comparison or settings.
modelsreadonly AiModel[][]The models on offer.
policySvRoutingPolicy | nullnullThe fallback chain and spend cap the `routing` variant renders. `null` until you supply one — the variant draws its empty state rather than inventing a policy, because a policy nobody wrote is a policy nobody can be held to.
catalogueAsOfstring | number | Date | nullnullWhen the figures in `models` were true. EVERY NUMBER HERE GOES STALE ON ITS OWN. Context windows, prices and availability change monthly, and this component renders them in the present tense. A price with no date is a claim about the past presented as the present, and the reader who budgets against it is comparing spend with a figure that moved three weeks ago. SUPPLY NOTHING AND NOTHING IS RENDERED — plus a dev warning to the integrator. A banner on every host that has not migrated yet is noise the reader cannot act on, and the person who can act on it is the one reading the console.
staleAfterDaysnumberSV_CATALOGUE_STALE_DAYSDays before the catalogue's figures are called stale.
nownumber | nullnullClock for the freshness read, injectable so a test is not at the mercy of the wall clock.
regionstring | nullnullThe deployment's region, when the catalogue declares regions. A model absent from this region stays in the list, marked, rather than disappearing: your configuration may still name it, and that is exactly the fact worth surfacing.
recommendationPolicystring | nullnullOne line naming who decided what "recommended" means here. A STAR WITH NO POLICY READS AS AUTHORITY. It cannot be audited, cannot be argued with, and cannot be told apart from a vendor placement. A model's own `featuredReason` beats this; this is the catalogue-wide fallback, and with neither the component says plainly that no reason was recorded.
selectedIdstring (two-way)''The chosen model's id. Two-way, so it can be driven from your own state. Empty until a catalogue arrives — the first offered model is selected automatically if you do not pick one. A bound id the catalogue marks `retired` is refused: the first selectable model is written back instead, a line under the variant names the swap, and a dev warning fires once (`modelChange` is not emitted — nobody picked it).
compareIdsReadonlySet<string> (two-way)new Set()Ids shown side by side in the `comparison` variant (two-way). Seeded from the first three offered models the first time a catalogue arrives — unless the host has bound a set, which wins — and owned by the user's toggles after that. A `model` rather than a signal plus a `compareChange` output: that pairing let a host persist the comparison but never restore it, which is the half of the job that matters after a reload.
configModelConfig (two-way){ ...DEFAULT_CONFIG }Generation settings. Two-way — bind it to persist what the user tunes. A `modelId` the catalogue marks `retired` is replaced with the first selectable model, the same way as `selectedId`.
budgetWarningPctnumberSV_BUDGET_WARNING_PCTShare of the cap at which the budget reads `warning`. EIGHTY PER CENT IS A STATEMENT ABOUT HOW FAST THE BUDGET IS SPENT. A team burning a monthly cap evenly wants the warning with a fifth of the money left; a team whose traffic triples at month end wants it far earlier. It must stay below 100 — at 100 the warning band vanishes and every state is either `ok` or `exceeded`, with nothing left of the stage that exists to be acted on.

Outputs

EventPayload typeDescription
retryvoidThe host should re-run the failed load.
modelChangestringEmitted with the id of the newly selected model.
selectedIdChangestringEmits the new `selectedId` whenever it changes — the output half of `[(selectedId)]`.
compareIdsChangeReadonlySet<string>Emits the new `compareIds` whenever it changes — the output half of `[(compareIds)]`.
configChangeModelConfigEmits the new `config` whenever it changes — the output half of `[(config)]`.

Wire it to your API

This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.

Template

Pro

Bound to the names this component actually exposes.

Host class

Pro

Drive the three async inputs from your own fetch.

  • This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `models`, `loading` and `error`.
  • Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.

Full walkthrough: Connecting to a backend.

Customization & theming

  • A retired model cannot be selected by any path. Clicks and keys are refused; a `[selectedId]` or `[config].modelId` bound to a retired id is replaced with the first selectable model, written back through the two-way binding, and named on screen ("X is retired and cannot be selected, so Y is selected instead") — `modelChange` does not fire, because the reader chose nothing. The `routing` variant shows the same lifecycle and region marks, faults a retired or out-of-region step (so a chain containing one no longer reads as sound) and warns on a deprecated one.
  • `models` is an input defaulting to the bundled catalogue, so the component demonstrates itself out of the box and offers your own models when you have them. The selection is moved onto the offered list if it is not already on it.
  • `selectedId`, `config` and `compareIds` are two-way, so the chosen model, the tuned generation settings and the comparison set can each be driven from — and restored into — your own state.
  • Dropdown, grid, comparison and settings layouts.
  • Provider badges and capability tags.
  • Latency, cost and context indicators.
  • Emits `modelChange` with the selected model id.
  • `[budgetWarningPct]` (80, from the still-exported `SV_BUDGET_WARNING_PCT`) is the share of a routing cap at which the budget reads `warning`. Eighty per cent is early enough to act on for a steady burn; a fleet whose traffic triples at month end wants it earlier and a prepaid credit later. It must stay below 100 — at 100 the warning band vanishes and every state is either ok or exceeded — so a figure at or above the cap is refused with a dev warning.
  • `[catalogueAsOf]` is when the figures were true, rendered as an 'as of' line under every variant and marked stale past `[staleAfterDays]` (30 — prices and context windows change monthly). Supply nothing and nothing is rendered, plus a dev warning: a banner on an unmigrated host is noise the reader cannot act on, and the person who can act on it is reading the console.
  • `isFeatured` now has to answer for itself. `featuredReason` on the model, or `[recommendationPolicy]` for the catalogue, is rendered beside the star; with neither, the card says plainly that no reason was recorded and the console says so too. 'Recommended' with no visible policy reads as authority and cannot be told apart from a vendor placement.
  • `lifecycle: 'preview' | 'ga' | 'deprecated' | 'retired'` plus `retiresAt` and `regions`, with `[region]` for the deployment. A deprecated or out-of-region model stays **visible and marked** rather than silently absent — dropping it is how a team finds out about a retirement from a 404 in production. Only `retired` is refused, because calling it cannot succeed. The lifecycle badge, the retirement date and a 'Not in <region>' mark render in **every** variant — dropdown options and trigger, cards, the comparison header and the settings list — and `retiresAt` shows on a deprecated model that is still available, because the date is the planning fact. A retired model is refused on every path — pointer, Enter and Space, and a programmatic `selectModel()` / `selectConfigModel()` — is marked `aria-disabled` (not `disabled`, so it stays focusable and its reason readable), and is skipped when the component picks a default.
  • `[overlayContainer]` and `[overlayZIndex]` (9999, from the exported `SV_MODEL_OVERLAY_Z_INDEX`) hand the dropdown panel to an app that owns the top of its own page. The default appends to `<body>`, which is outside a host overlay stack's ordering rules and lands under or over the wrong layer depending on document order — something no z-index on this side can fix, because stacking contexts do not compare across them. Give it your overlay root instead, and raise the index above your modal layer if the panel has to open from inside a dialog.
  • A container brings a browser subtlety with it and the component handles it: a `transform`, `filter`, `perspective`, `will-change` or `contain: paint` on **any ancestor** of that container makes the ancestor the containing block for `position: fixed`, so viewport coordinates are silently re-based on its box and the panel appears offset by wherever the overlay root sits — the properties an app puts on an overlay root for compositing and modal animations are exactly these. The panel finds that ancestor and converts into its space, and a container that is not yet in the document falls back to `<body>` with a dev warning rather than rendering nowhere at all.

Best practices

  • Do: Surface cost, context window and latency so users make informed choices.
  • Do: Use the comparison variant when the trade-offs between models matter.
  • Tip: Badge providers and capabilities consistently across the app.

Accessibility

  • The scrolling list of models is the `listbox`, named, with the options as its own children — the search box and the footer sit outside it, because a listbox may own only options. Getting this wrong (the role was on the whole panel) left a screen reader with an empty listbox and options belonging to nobody; it was found by the browser accessibility gate, not by a unit test.
  • The trigger is `aria-haspopup="listbox"` with `aria-expanded`, and `aria-controls` the list while it is open.
  • Options expose full model metadata as text.
  • Search inputs are labelled; parameter sliders carry aria-label (and aria-valuetext for temperature).
  • Keyboard selection with clear focus.

Feedback on AI Model Selector

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.