UI Components Easy
Model Selector
LLM model picker dropdown with provider logos, context window size, capability tags, and active model indicator.
Open in Lab
MCP
vanilla-js css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f9fafb;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 32px 16px;
}
.demo {
position: relative;
width: 100%;
max-width: 360px;
}
.ms-trigger-area {
display: flex;
justify-content: center;
}
.ms-trigger {
display: inline-flex;
align-items: center;
gap: 8px;
background: #fff;
border: 1.5px solid #e5e7eb;
border-radius: 10px;
padding: 9px 14px;
font-size: 14px;
font-weight: 600;
color: #374151;
cursor: pointer;
transition: border-color 0.15s;
}
.ms-trigger:hover {
border-color: #d1d5db;
}
.ms-trigger.open {
border-color: #6366f1;
}
.ms-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.ms-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
width: 320px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 14px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
z-index: 20;
overflow: hidden;
padding: 6px;
animation: ms-in 0.15s ease;
}
@keyframes ms-in {
from {
opacity: 0;
transform: translateX(-50%) translateY(-6px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.ms-section-label {
font-size: 10px;
font-weight: 700;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.07em;
padding: 6px 10px 3px;
}
.ms-divider {
height: 1px;
background: #f3f4f6;
margin: 4px 0;
}
.ms-option {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 8px 10px;
background: none;
border: none;
border-radius: 8px;
cursor: pointer;
text-align: left;
transition: background 0.12s;
}
.ms-option:hover {
background: #f9fafb;
}
.ms-option--selected {
background: #f5f3ff;
}
.ms-check {
width: 14px;
font-size: 12px;
color: #6366f1;
flex-shrink: 0;
}
.ms-info {
flex: 1;
min-width: 0;
}
.ms-name {
font-size: 13px;
font-weight: 700;
color: #111827;
display: block;
}
.ms-caps {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 3px;
}
.cap {
font-size: 10px;
font-weight: 600;
background: #f3f4f6;
color: #6b7280;
padding: 1px 6px;
border-radius: 4px;
}
.cap--new {
background: #eef2ff;
color: #6366f1;
}
.ms-ctx {
font-size: 11px;
color: #9ca3af;
flex-shrink: 0;
}
.ms-backdrop {
position: fixed;
inset: 0;
z-index: 10;
}const trigger = document.getElementById("msTrigger");
const dropdown = document.getElementById("msDropdown");
const backdrop = document.getElementById("msBackdrop");
const selectedLabel = document.getElementById("msSelected");
function open() {
dropdown.hidden = false;
backdrop.hidden = false;
trigger.classList.add("open");
}
function close() {
dropdown.hidden = true;
backdrop.hidden = true;
trigger.classList.remove("open");
}
trigger.addEventListener("click", () => (dropdown.hidden ? open() : close()));
backdrop.addEventListener("click", close);
dropdown.querySelectorAll(".ms-option").forEach((opt) => {
opt.addEventListener("click", () => {
// Update selection
dropdown.querySelectorAll(".ms-option").forEach((o) => {
o.classList.remove("ms-option--selected");
o.querySelector(".ms-check").textContent = "";
});
opt.classList.add("ms-option--selected");
opt.querySelector(".ms-check").textContent = "✓";
// Update trigger
const color = opt.dataset.color;
const name = opt.dataset.name;
selectedLabel.textContent = name;
trigger.querySelector(".ms-dot").style.background = color;
close();
});
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Model Selector</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<div class="ms-trigger-area">
<button class="ms-trigger" id="msTrigger">
<span class="ms-dot" style="background:#a78bfa;"></span>
<span id="msSelected">Claude Sonnet</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>
</button>
</div>
<div class="ms-dropdown" id="msDropdown" hidden>
<div class="ms-section-label">Anthropic</div>
<button class="ms-option ms-option--selected" data-name="Claude Sonnet" data-ctx="200K" data-color="#a78bfa" data-caps="vision,code,reasoning">
<span class="ms-check">✓</span>
<span class="ms-dot" style="background:#a78bfa;"></span>
<div class="ms-info">
<span class="ms-name">Claude Sonnet</span>
<div class="ms-caps"><span class="cap">Vision</span><span class="cap">Code</span><span class="cap">Reasoning</span></div>
</div>
<span class="ms-ctx">200K</span>
</button>
<button class="ms-option" data-name="Claude Haiku" data-ctx="200K" data-color="#93c5fd" data-caps="fast,vision">
<span class="ms-check"></span>
<span class="ms-dot" style="background:#93c5fd;"></span>
<div class="ms-info">
<span class="ms-name">Claude Haiku</span>
<div class="ms-caps"><span class="cap">Fast</span><span class="cap">Vision</span></div>
</div>
<span class="ms-ctx">200K</span>
</button>
<button class="ms-option" data-name="Claude Opus" data-ctx="200K" data-color="#c084fc" data-caps="vision,code,reasoning,analysis">
<span class="ms-check"></span>
<span class="ms-dot" style="background:#c084fc;"></span>
<div class="ms-info">
<span class="ms-name">Claude Opus</span>
<div class="ms-caps"><span class="cap">Vision</span><span class="cap">Code</span><span class="cap">Reasoning</span><span class="cap cap--new">Strongest</span></div>
</div>
<span class="ms-ctx">200K</span>
</button>
<div class="ms-divider"></div>
<div class="ms-section-label">OpenAI</div>
<button class="ms-option" data-name="GPT-4o" data-ctx="128K" data-color="#4ade80" data-caps="vision,code">
<span class="ms-check"></span>
<span class="ms-dot" style="background:#4ade80;"></span>
<div class="ms-info">
<span class="ms-name">GPT-4o</span>
<div class="ms-caps"><span class="cap">Vision</span><span class="cap">Code</span></div>
</div>
<span class="ms-ctx">128K</span>
</button>
<button class="ms-option" data-name="o3-mini" data-ctx="200K" data-color="#86efac" data-caps="reasoning">
<span class="ms-check"></span>
<span class="ms-dot" style="background:#86efac;"></span>
<div class="ms-info">
<span class="ms-name">o3-mini</span>
<div class="ms-caps"><span class="cap">Reasoning</span></div>
</div>
<span class="ms-ctx">200K</span>
</button>
<div class="ms-divider"></div>
<div class="ms-section-label">Google</div>
<button class="ms-option" data-name="Gemini 2.0 Flash" data-ctx="1M" data-color="#fbbf24" data-caps="vision,fast">
<span class="ms-check"></span>
<span class="ms-dot" style="background:#fbbf24;"></span>
<div class="ms-info">
<span class="ms-name">Gemini 2.0 Flash</span>
<div class="ms-caps"><span class="cap">Vision</span><span class="cap">Fast</span><span class="cap cap--new">1M ctx</span></div>
</div>
<span class="ms-ctx">1M</span>
</button>
</div>
<div class="ms-backdrop" id="msBackdrop" hidden></div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState } from "react";
interface Model {
id: string;
name: string;
provider: string;
color: string;
context: string;
caps: string[];
badge?: string;
}
const MODELS: Model[] = [
{
id: "claude-opus-4",
name: "Claude Opus 4",
provider: "Anthropic",
color: "#e89537",
context: "200K",
caps: ["vision", "code", "reasoning"],
badge: "Most capable",
},
{
id: "claude-sonnet-4",
name: "Claude Sonnet 4",
provider: "Anthropic",
color: "#e89537",
context: "200K",
caps: ["vision", "code", "fast"],
},
{
id: "claude-haiku-4",
name: "Claude Haiku 4",
provider: "Anthropic",
color: "#e89537",
context: "200K",
caps: ["fast", "cheap"],
badge: "Fastest",
},
{
id: "gpt-4o",
name: "GPT-4o",
provider: "OpenAI",
color: "#10a37f",
context: "128K",
caps: ["vision", "code", "audio"],
},
{
id: "gpt-4o-mini",
name: "GPT-4o mini",
provider: "OpenAI",
color: "#10a37f",
context: "128K",
caps: ["fast", "cheap"],
},
{
id: "gemini-2.0-flash",
name: "Gemini 2.0 Flash",
provider: "Google",
color: "#4285f4",
context: "1M",
caps: ["vision", "code", "fast"],
badge: "Largest context",
},
{
id: "gemini-2.0-pro",
name: "Gemini 2.0 Pro",
provider: "Google",
color: "#4285f4",
context: "1M",
caps: ["vision", "code", "reasoning"],
},
];
const CAP_COLORS: Record<string, string> = {
vision: "bg-purple-500/10 text-purple-400 border-purple-500/20",
code: "bg-[#58a6ff]/10 text-[#58a6ff] border-[#58a6ff]/20",
reasoning: "bg-amber-500/10 text-amber-400 border-amber-500/20",
audio: "bg-pink-500/10 text-pink-400 border-pink-500/20",
fast: "bg-green-500/10 text-green-400 border-green-500/20",
cheap: "bg-teal-500/10 text-teal-400 border-teal-500/20",
};
export default function ModelSelectorRC() {
const [selected, setSelected] = useState("claude-sonnet-4");
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const current = MODELS.find((m) => m.id === selected)!;
const providers = [...new Set(MODELS.map((m) => m.provider))];
const q = search.toLowerCase();
const filtered = MODELS.filter(
(m) =>
m.name.toLowerCase().includes(q) ||
m.provider.toLowerCase().includes(q) ||
m.caps.some((c) => c.includes(q))
);
return (
<div className="min-h-screen bg-[#0d1117] p-6 flex justify-center items-start">
<div className="w-full max-w-[480px] space-y-4">
<p className="text-[13px] text-[#8b949e]">Select model</p>
{/* Trigger */}
<div className="relative">
<button
onClick={() => setOpen((v) => !v)}
className={`w-full flex items-center gap-3 px-4 py-3 bg-[#161b22] border rounded-xl transition-colors text-left ${
open ? "border-[#58a6ff]" : "border-[#30363d] hover:border-[#8b949e]"
}`}
>
<span
className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 font-bold text-[11px]"
style={{ background: current.color + "22", color: current.color }}
>
{current.provider[0]}
</span>
<div className="flex-1 min-w-0">
<p className="text-[13px] font-semibold text-[#e6edf3]">{current.name}</p>
<p className="text-[11px] text-[#8b949e]">
{current.provider} · {current.context} context
</p>
</div>
<div className="flex flex-wrap gap-1 justify-end max-w-[140px]">
{current.caps.slice(0, 2).map((c) => (
<span
key={c}
className={`text-[9px] px-1.5 py-0.5 rounded-full border font-semibold uppercase tracking-wide ${CAP_COLORS[c]}`}
>
{c}
</span>
))}
</div>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
className={`text-[#8b949e] flex-shrink-0 transition-transform ${open ? "rotate-180" : ""}`}
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
{/* Dropdown */}
{open && (
<div className="absolute top-full mt-1 left-0 right-0 bg-[#21262d] border border-[#30363d] rounded-xl overflow-hidden shadow-2xl z-20">
{/* Search */}
<div className="p-2 border-b border-[#30363d]">
<input
type="text"
placeholder="Search models…"
value={search}
onChange={(e) => setSearch(e.target.value)}
autoFocus
className="w-full bg-[#161b22] border border-[#30363d] rounded-lg px-3 py-1.5 text-[12px] text-[#e6edf3] placeholder-[#484f58] outline-none focus:border-[#58a6ff] transition-colors"
/>
</div>
{/* List */}
<div className="max-h-[360px] overflow-y-auto">
{providers.map((prov) => {
const group = filtered.filter((m) => m.provider === prov);
if (!group.length) return null;
return (
<div key={prov}>
<div className="px-3 py-1.5 text-[10px] font-bold text-[#484f58] uppercase tracking-wider">
{prov}
</div>
{group.map((m) => (
<button
key={m.id}
onClick={() => {
setSelected(m.id);
setOpen(false);
setSearch("");
}}
className={`w-full flex items-center gap-3 px-3 py-2.5 transition-colors text-left hover:bg-white/[0.04] ${
m.id === selected ? "bg-[#58a6ff]/[0.08]" : ""
}`}
>
<span
className="w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 font-bold text-[10px]"
style={{ background: m.color + "22", color: m.color }}
>
{m.provider[0]}
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span
className={`text-[12px] font-semibold ${m.id === selected ? "text-[#58a6ff]" : "text-[#e6edf3]"}`}
>
{m.name}
</span>
{m.badge && (
<span className="text-[9px] px-1.5 py-0.5 bg-[#58a6ff]/10 text-[#58a6ff] border border-[#58a6ff]/20 rounded-full font-semibold">
{m.badge}
</span>
)}
</div>
<div className="flex items-center gap-1.5 mt-0.5">
<span className="text-[10px] text-[#484f58]">{m.context} ctx</span>
{m.caps.map((c) => (
<span
key={c}
className={`text-[9px] px-1 py-px rounded border font-semibold ${CAP_COLORS[c]}`}
>
{c}
</span>
))}
</div>
</div>
{m.id === selected && (
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="#58a6ff"
strokeWidth="2.5"
>
<polyline points="20 6 9 17 4 12" />
</svg>
)}
</button>
))}
</div>
);
})}
{filtered.length === 0 && (
<div className="px-3 py-6 text-center text-[12px] text-[#484f58]">
No models found
</div>
)}
</div>
</div>
)}
</div>
{/* Selected info */}
{!open && (
<div className="bg-[#161b22] border border-[#30363d] rounded-xl p-4 space-y-2">
<p className="text-[11px] font-bold text-[#8b949e] uppercase tracking-wider">
Selected model details
</p>
<div className="grid grid-cols-2 gap-3 text-[12px]">
<div>
<p className="text-[#484f58]">Provider</p>
<p className="text-[#e6edf3] font-semibold">{current.provider}</p>
</div>
<div>
<p className="text-[#484f58]">Context window</p>
<p className="text-[#e6edf3] font-semibold">{current.context} tokens</p>
</div>
</div>
<div>
<p className="text-[11px] text-[#484f58] mb-1.5">Capabilities</p>
<div className="flex flex-wrap gap-1.5">
{current.caps.map((c) => (
<span
key={c}
className={`text-[10px] px-2 py-0.5 rounded-full border font-semibold ${CAP_COLORS[c]}`}
>
{c}
</span>
))}
</div>
</div>
</div>
)}
</div>
</div>
);
}<script setup>
import { ref, computed } from "vue";
const selected = ref("claude-sonnet-4");
const open = ref(false);
const search = ref("");
const MODELS = [
{
id: "claude-opus-4",
name: "Claude Opus 4",
provider: "Anthropic",
color: "#e89537",
context: "200K",
caps: ["vision", "code", "reasoning"],
badge: "Most capable",
},
{
id: "claude-sonnet-4",
name: "Claude Sonnet 4",
provider: "Anthropic",
color: "#e89537",
context: "200K",
caps: ["vision", "code", "fast"],
},
{
id: "claude-haiku-4",
name: "Claude Haiku 4",
provider: "Anthropic",
color: "#e89537",
context: "200K",
caps: ["fast", "cheap"],
badge: "Fastest",
},
{
id: "gpt-4o",
name: "GPT-4o",
provider: "OpenAI",
color: "#10a37f",
context: "128K",
caps: ["vision", "code", "audio"],
},
{
id: "gpt-4o-mini",
name: "GPT-4o mini",
provider: "OpenAI",
color: "#10a37f",
context: "128K",
caps: ["fast", "cheap"],
},
{
id: "gemini-2.0-flash",
name: "Gemini 2.0 Flash",
provider: "Google",
color: "#4285f4",
context: "1M",
caps: ["vision", "code", "fast"],
badge: "Largest context",
},
{
id: "gemini-2.0-pro",
name: "Gemini 2.0 Pro",
provider: "Google",
color: "#4285f4",
context: "1M",
caps: ["vision", "code", "reasoning"],
},
];
const CAP_COLORS = {
vision: "bg-purple-500/10 text-purple-400 border-purple-500/20",
code: "bg-[#58a6ff]/10 text-[#58a6ff] border-[#58a6ff]/20",
reasoning: "bg-amber-500/10 text-amber-400 border-amber-500/20",
audio: "bg-pink-500/10 text-pink-400 border-pink-500/20",
fast: "bg-green-500/10 text-green-400 border-green-500/20",
cheap: "bg-teal-500/10 text-teal-400 border-teal-500/20",
};
const current = computed(() => MODELS.find((m) => m.id === selected.value));
const providers = computed(() => [...new Set(MODELS.map((m) => m.provider))]);
const q = computed(() => search.value.toLowerCase());
const filtered = computed(() =>
MODELS.filter(
(m) =>
m.name.toLowerCase().includes(q.value) ||
m.provider.toLowerCase().includes(q.value) ||
m.caps.some((c) => c.includes(q.value))
)
);
function groupByProvider(prov) {
return filtered.value.filter((m) => m.provider === prov);
}
function selectModel(id) {
selected.value = id;
open.value = false;
search.value = "";
}
</script>
<template>
<div class="min-h-screen bg-[#0d1117] p-6 flex justify-center items-start">
<div class="w-full max-w-[480px] space-y-4">
<p class="text-[13px] text-[#8b949e]">Select model</p>
<!-- Trigger -->
<div class="relative">
<button
@click="open = !open"
:class="[
'w-full flex items-center gap-3 px-4 py-3 bg-[#161b22] border rounded-xl transition-colors text-left',
open ? 'border-[#58a6ff]' : 'border-[#30363d] hover:border-[#8b949e]'
]"
>
<template v-if="current">
<span
class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 font-bold text-[11px]"
:style="{ background: current.color + '22', color: current.color }"
>
{{ current.provider[0] }}
</span>
<div class="flex-1 min-w-0">
<p class="text-[13px] font-semibold text-[#e6edf3]">{{ current.name }}</p>
<p class="text-[11px] text-[#8b949e]">{{ current.provider }} · {{ current.context }} context</p>
</div>
<div class="flex flex-wrap gap-1 justify-end max-w-[140px]">
<span
v-for="c in current.caps.slice(0, 2)"
:key="c"
:class="'text-[9px] px-1.5 py-0.5 rounded-full border font-semibold uppercase tracking-wide ' + CAP_COLORS[c]"
>
{{ c }}
</span>
</div>
</template>
<svg
width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" :class="['text-[#8b949e] flex-shrink-0 transition-transform', open ? 'rotate-180' : '']"
>
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<!-- Dropdown -->
<div v-if="open" class="absolute top-full mt-1 left-0 right-0 bg-[#21262d] border border-[#30363d] rounded-xl overflow-hidden shadow-2xl z-20">
<!-- Search -->
<div class="p-2 border-b border-[#30363d]">
<input
type="text"
placeholder="Search models…"
v-model="search"
autofocus
class="w-full bg-[#161b22] border border-[#30363d] rounded-lg px-3 py-1.5 text-[12px] text-[#e6edf3] placeholder-[#484f58] outline-none focus:border-[#58a6ff] transition-colors"
/>
</div>
<!-- List -->
<div class="max-h-[360px] overflow-y-auto">
<template v-for="prov in providers" :key="prov">
<div v-if="groupByProvider(prov).length > 0">
<div class="px-3 py-1.5 text-[10px] font-bold text-[#484f58] uppercase tracking-wider">
{{ prov }}
</div>
<button
v-for="m in groupByProvider(prov)"
:key="m.id"
@click="selectModel(m.id)"
:class="[
'w-full flex items-center gap-3 px-3 py-2.5 transition-colors text-left hover:bg-white/[0.04]',
m.id === selected ? 'bg-[#58a6ff]/[0.08]' : ''
]"
>
<span
class="w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 font-bold text-[10px]"
:style="{ background: m.color + '22', color: m.color }"
>
{{ m.provider[0] }}
</span>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span :class="['text-[12px] font-semibold', m.id === selected ? 'text-[#58a6ff]' : 'text-[#e6edf3]']">
{{ m.name }}
</span>
<span v-if="m.badge" class="text-[9px] px-1.5 py-0.5 bg-[#58a6ff]/10 text-[#58a6ff] border border-[#58a6ff]/20 rounded-full font-semibold">
{{ m.badge }}
</span>
</div>
<div class="flex items-center gap-1.5 mt-0.5">
<span class="text-[10px] text-[#484f58]">{{ m.context }} ctx</span>
<span
v-for="c in m.caps"
:key="c"
:class="'text-[9px] px-1 py-px rounded border font-semibold ' + CAP_COLORS[c]"
>
{{ c }}
</span>
</div>
</div>
<svg v-if="m.id === selected" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#58a6ff" stroke-width="2.5">
<polyline points="20 6 9 17 4 12"/>
</svg>
</button>
</div>
</template>
<div v-if="filtered.length === 0" class="px-3 py-6 text-center text-[12px] text-[#484f58]">No models found</div>
</div>
</div>
</div>
<!-- Selected info -->
<div v-if="!open && current" class="bg-[#161b22] border border-[#30363d] rounded-xl p-4 space-y-2">
<p class="text-[11px] font-bold text-[#8b949e] uppercase tracking-wider">Selected model details</p>
<div class="grid grid-cols-2 gap-3 text-[12px]">
<div>
<p class="text-[#484f58]">Provider</p>
<p class="text-[#e6edf3] font-semibold">{{ current.provider }}</p>
</div>
<div>
<p class="text-[#484f58]">Context window</p>
<p class="text-[#e6edf3] font-semibold">{{ current.context }} tokens</p>
</div>
</div>
<div>
<p class="text-[11px] text-[#484f58] mb-1.5">Capabilities</p>
<div class="flex flex-wrap gap-1.5">
<span
v-for="c in current.caps"
:key="c"
:class="'text-[10px] px-2 py-0.5 rounded-full border font-semibold ' + CAP_COLORS[c]"
>
{{ c }}
</span>
</div>
</div>
</div>
</div>
</div>
</template><script>
let selected = "claude-sonnet-4";
let open = false;
let search = "";
const MODELS = [
{
id: "claude-opus-4",
name: "Claude Opus 4",
provider: "Anthropic",
color: "#e89537",
context: "200K",
caps: ["vision", "code", "reasoning"],
badge: "Most capable",
},
{
id: "claude-sonnet-4",
name: "Claude Sonnet 4",
provider: "Anthropic",
color: "#e89537",
context: "200K",
caps: ["vision", "code", "fast"],
},
{
id: "claude-haiku-4",
name: "Claude Haiku 4",
provider: "Anthropic",
color: "#e89537",
context: "200K",
caps: ["fast", "cheap"],
badge: "Fastest",
},
{
id: "gpt-4o",
name: "GPT-4o",
provider: "OpenAI",
color: "#10a37f",
context: "128K",
caps: ["vision", "code", "audio"],
},
{
id: "gpt-4o-mini",
name: "GPT-4o mini",
provider: "OpenAI",
color: "#10a37f",
context: "128K",
caps: ["fast", "cheap"],
},
{
id: "gemini-2.0-flash",
name: "Gemini 2.0 Flash",
provider: "Google",
color: "#4285f4",
context: "1M",
caps: ["vision", "code", "fast"],
badge: "Largest context",
},
{
id: "gemini-2.0-pro",
name: "Gemini 2.0 Pro",
provider: "Google",
color: "#4285f4",
context: "1M",
caps: ["vision", "code", "reasoning"],
},
];
const CAP_COLORS = {
vision: "bg-purple-500/10 text-purple-400 border-purple-500/20",
code: "bg-[#58a6ff]/10 text-[#58a6ff] border-[#58a6ff]/20",
reasoning: "bg-amber-500/10 text-amber-400 border-amber-500/20",
audio: "bg-pink-500/10 text-pink-400 border-pink-500/20",
fast: "bg-green-500/10 text-green-400 border-green-500/20",
cheap: "bg-teal-500/10 text-teal-400 border-teal-500/20",
};
$: current = MODELS.find((m) => m.id === selected);
$: providers = [...new Set(MODELS.map((m) => m.provider))];
$: q = search.toLowerCase();
$: filtered = MODELS.filter(
(m) =>
m.name.toLowerCase().includes(q) ||
m.provider.toLowerCase().includes(q) ||
m.caps.some((c) => c.includes(q))
);
function selectModel(id) {
selected = id;
open = false;
search = "";
}
function groupByProvider(prov) {
return filtered.filter((m) => m.provider === prov);
}
</script>
<div class="min-h-screen bg-[#0d1117] p-6 flex justify-center items-start">
<div class="w-full max-w-[480px] space-y-4">
<p class="text-[13px] text-[#8b949e]">Select model</p>
<!-- Trigger -->
<div class="relative">
<button
on:click={() => (open = !open)}
class="w-full flex items-center gap-3 px-4 py-3 bg-[#161b22] border rounded-xl transition-colors text-left {open ? 'border-[#58a6ff]' : 'border-[#30363d] hover:border-[#8b949e]'}"
>
{#if current}
<span
class="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 font-bold text-[11px]"
style="background: {current.color}22; color: {current.color}"
>
{current.provider[0]}
</span>
<div class="flex-1 min-w-0">
<p class="text-[13px] font-semibold text-[#e6edf3]">{current.name}</p>
<p class="text-[11px] text-[#8b949e]">{current.provider} · {current.context} context</p>
</div>
<div class="flex flex-wrap gap-1 justify-end max-w-[140px]">
{#each current.caps.slice(0, 2) as c}
<span class="text-[9px] px-1.5 py-0.5 rounded-full border font-semibold uppercase tracking-wide {CAP_COLORS[c]}">
{c}
</span>
{/each}
</div>
{/if}
<svg
width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" class="text-[#8b949e] flex-shrink-0 transition-transform {open ? 'rotate-180' : ''}"
>
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<!-- Dropdown -->
{#if open}
<div class="absolute top-full mt-1 left-0 right-0 bg-[#21262d] border border-[#30363d] rounded-xl overflow-hidden shadow-2xl z-20">
<!-- Search -->
<div class="p-2 border-b border-[#30363d]">
<!-- svelte-ignore a11y-autofocus -->
<input
type="text"
placeholder="Search models…"
bind:value={search}
autofocus
class="w-full bg-[#161b22] border border-[#30363d] rounded-lg px-3 py-1.5 text-[12px] text-[#e6edf3] placeholder-[#484f58] outline-none focus:border-[#58a6ff] transition-colors"
/>
</div>
<!-- List -->
<div class="max-h-[360px] overflow-y-auto">
{#each providers as prov}
{#if groupByProvider(prov).length > 0}
<div>
<div class="px-3 py-1.5 text-[10px] font-bold text-[#484f58] uppercase tracking-wider">
{prov}
</div>
{#each groupByProvider(prov) as m}
<button
on:click={() => selectModel(m.id)}
class="w-full flex items-center gap-3 px-3 py-2.5 transition-colors text-left hover:bg-white/[0.04] {m.id === selected ? 'bg-[#58a6ff]/[0.08]' : ''}"
>
<span
class="w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 font-bold text-[10px]"
style="background: {m.color}22; color: {m.color}"
>
{m.provider[0]}
</span>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-[12px] font-semibold {m.id === selected ? 'text-[#58a6ff]' : 'text-[#e6edf3]'}">
{m.name}
</span>
{#if m.badge}
<span class="text-[9px] px-1.5 py-0.5 bg-[#58a6ff]/10 text-[#58a6ff] border border-[#58a6ff]/20 rounded-full font-semibold">
{m.badge}
</span>
{/if}
</div>
<div class="flex items-center gap-1.5 mt-0.5">
<span class="text-[10px] text-[#484f58]">{m.context} ctx</span>
{#each m.caps as c}
<span class="text-[9px] px-1 py-px rounded border font-semibold {CAP_COLORS[c]}">
{c}
</span>
{/each}
</div>
</div>
{#if m.id === selected}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#58a6ff" stroke-width="2.5">
<polyline points="20 6 9 17 4 12"/>
</svg>
{/if}
</button>
{/each}
</div>
{/if}
{/each}
{#if filtered.length === 0}
<div class="px-3 py-6 text-center text-[12px] text-[#484f58]">No models found</div>
{/if}
</div>
</div>
{/if}
</div>
<!-- Selected info -->
{#if !open && current}
<div class="bg-[#161b22] border border-[#30363d] rounded-xl p-4 space-y-2">
<p class="text-[11px] font-bold text-[#8b949e] uppercase tracking-wider">Selected model details</p>
<div class="grid grid-cols-2 gap-3 text-[12px]">
<div>
<p class="text-[#484f58]">Provider</p>
<p class="text-[#e6edf3] font-semibold">{current.provider}</p>
</div>
<div>
<p class="text-[#484f58]">Context window</p>
<p class="text-[#e6edf3] font-semibold">{current.context} tokens</p>
</div>
</div>
<div>
<p class="text-[11px] text-[#484f58] mb-1.5">Capabilities</p>
<div class="flex flex-wrap gap-1.5">
{#each current.caps as c}
<span class="text-[10px] px-2 py-0.5 rounded-full border font-semibold {CAP_COLORS[c]}">
{c}
</span>
{/each}
</div>
</div>
</div>
{/if}
</div>
</div>LLM model selector dropdown with model names, provider color indicators, context window size badges, capability tags (vision, code, reasoning), and a checkmark on the currently selected model.