How I'd Think About This Problem
This is not a dropdown exercise. Interviewers are checking whether you understand that model choice changes the product surface. Latency badges set expectations. Cost chips educate tradeoffs. Capability tags prevent dead-end clicks. If you only change a label in the select and leave the rest of the UI identical, you've missed the point.
I split the problem into three layers before coding: config (static model catalog), session state (selected modelId that survives sends), and capability gates (actions that consult the selected model's capabilities before running).
Config as Source of Truth
const MODELS = [
{ id: 'flash', name: 'Flash Mini', latencyMs: 280, costPer1k: 0.15, capabilities: ['chat'] },
{ id: 'sonnet', name: 'Sonnet Pro', latencyMs: 520, costPer1k: 3.0, capabilities: ['chat', 'tools'] },
{ id: 'vision', name: 'Vision XL', latencyMs: 900, costPer1k: 8.5, capabilities: ['chat', 'tools', 'vision'] },
];
const model = MODELS.find(m => m.id === modelId);
Never denormalize latency/cost into React state. Derive them. When product adds a model, you edit one array. Same pattern for actions: declare { needs: 'vision', unlock: 'Vision XL' } once and render reasons from that.
Gates Need Visible Reasons
The junior trap: disabled={!ok} and nothing else. Users see a dead button and assume the product is broken. The senior move: keep the button visible, disable it, and put a permanent inline reason beside it — "Flash Mini lacks vision — switch to Vision XL". Also set title for hover/AT. Do not rely on click handlers for the reason; disabled buttons do not fire clicks.
⚠ Common Pitfall: Putting the reason only inside onClick
If the button is disabled, onClick never runs. Your warn banner stays blank forever. Render the reason from capability state, not from a click side-effect.
Keyboard Menu Without a Library
ArrowDown/ArrowUp move activeIdx. Enter commits MODELS[activeIdx]. Escape closes. Focus the menu when it opens (useEffect + ref), not via invalid autoFocus on a non-input. Click-outside closes. That is enough for most frontend loops.
What Changes in Production
- Model list comes from your BFF (feature flags, regional availability, enterprise allowlists).
- Cost chips should use live pricing or relative tiers your PM owns — not hardcoded dollars in the client forever.
- Capability gating should match server-side enforcement. The UI is a courtesy; the gateway still rejects unsupported modalities.
ℹ Interview Tip
Say out loud: "I'd surface cost and TTFT next to the picker because users will pick the expensive model for everything unless you make the tradeoff visible." That sentence alone separates people who've shipped AI UX from people who've only called OpenAI once.