About this snippet

A model picker inside a prompt composer that switches between four AI models with different context windows, speed tiers and reasoning modes. Opening the pill reveals each model's description, context limit and relative cost. Picking a model updates the trigger badge, adjusts the live token counter in the footer and enables or locks the Extended thinking switch when a model always reasons or never reasons. The menu starts open so you can compare the models right away.

The menu follows the WAI-ARIA listbox pattern with roving focus on the option elements. ArrowUp, ArrowDown, Home and End move focus through the list, Enter or Space selects the focused model and returns focus to the trigger button, and Escape or an outside click closes the menu cleanly. The token counter estimates prompt tokens from character length and adds a 4,000-token reasoning budget whenever Extended thinking is active.

The trigger button sets aria-haspopup="listbox" and aria-expanded, while the thinking control uses role="switch" with aria-checked. When you pick a flash model or a dedicated reasoner, the switch disables itself and explains why in the note beside it. A live status region announces model changes and sent prompts for screen reader users.

Size
16.3 KB
Uses
HTML, CSS, JavaScript
Loads
Nothing external
Views
0
<main class="page">
  <section class="composer" aria-label="AI prompt composer">
    <div class="composer-top">
      <div class="picker" id="picker">
        <button type="button" class="picker-btn" id="picker-btn" aria-haspopup="listbox" aria-expanded="true" aria-controls="model-list">
          <span class="picker-dot" id="picker-dot" aria-hidden="true"></span>
          <span class="picker-name" id="picker-name">Nova 4 Balanced</span>
          <span class="picker-tag" id="picker-tag">200k</span>
          <svg class="picker-chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6"></path></svg>
        </button>
        <ul class="model-list" id="model-list" role="listbox" aria-label="Select AI model"></ul>
      </div>
      <span class="model-note" id="model-note">Everyday coding and writing</span>
    </div>
    <label class="sr-only" for="prompt">Prompt</label>
    <textarea id="prompt" rows="3">Compare B-tree and LSM-tree indexes for a write-heavy event log.</textarea>
    <div class="composer-foot">
      <button type="button" class="think-switch" id="think-switch" role="switch" aria-checked="false">
        <span class="switch-track" aria-hidden="true"></span>
        <span class="switch-label">Extended thinking</span>
        <span class="switch-note" id="think-note">+4k budget</span>
      </button>
      <div class="foot-right">
        <span class="tokens" id="tokens">18 / 200,000 tokens</span>
        <button type="button" class="send-btn" id="send">
          <span>Send</span>
          <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"></path></svg>
        </button>
      </div>
    </div>
    <p class="reply" id="reply" hidden></p>
    <p class="sr-only" id="status" role="status"></p>
  </section>
</main>
*, *::before, *::after {
  box-sizing: border-box;
}

:root {
  --page: #f1f5f9;
  --card: #ffffff;
  --text: #0f172a;
  --muted: #64748b;
  --soft: #f1f5f9;
  --hover: #e2e8f0;
  --accent: #2563eb;
  --accent-soft: #eff6ff;
  --accent-text: #1d4ed8;
  --ring: rgba(15, 23, 42, 0.06);
  --shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 18px 40px -14px rgba(15, 23, 42, 0.14);
  --menu-shadow: 0 4px 12px rgba(15, 23, 42, 0.06), 0 22px 44px -12px rgba(15, 23, 42, 0.18);
}

body {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  margin: 0;
  background: var(--page);
  color: var(--text);
}

.page {
  display: grid;
  grid-template-columns: minmax(0, 580px);
  justify-content: center;
  align-content: start;
  min-height: 100vh;
  padding: max(20px, calc(50vh - 177px)) 16px 20px;
}

.composer {
  position: relative;
  padding: 16px;
  border-radius: 22px;
  background: var(--card);
  box-shadow: 0 0 0 1px var(--ring), var(--shadow);
}

.composer-top {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding-bottom: 10px;
}

.picker {
  position: static;
}

.picker-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 36px;
  padding: 6px 12px;
  border: 0;
  border-radius: 999px;
  background: var(--soft);
  color: var(--text);
  font: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease;
}

.picker-btn:hover {
  background: var(--hover);
}

.picker-btn[aria-expanded="true"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}

.picker-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--accent);
}

.picker-tag {
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--card);
  color: var(--muted);
  font-family: ui-monospace, Consolas, monospace;
  font-size: 0.6875rem;
  font-weight: 700;
}

.picker-btn[aria-expanded="true"] .picker-tag {
  color: var(--accent-text);
}

.picker-chevron {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 0.2s ease;
}

.picker-btn[aria-expanded="true"] .picker-chevron {
  transform: rotate(180deg);
}

.model-note {
  overflow: hidden;
  font-size: 0.8125rem;
  color: var(--muted);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.model-list {
  position: absolute;
  top: calc(100% + 2px);
  right: 0;
  left: 0;
  z-index: 20;
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 5px;
  border-radius: 16px;
  background: var(--card);
  box-shadow: 0 0 0 1px var(--ring), var(--menu-shadow);
  list-style: none;
}

.model-list[hidden] {
  display: none;
}

.model-opt {
  display: grid;
  grid-template-columns: 32px minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  padding: 5px 10px;
  border-radius: 12px;
  cursor: pointer;
}

.model-opt:hover {
  background: var(--soft);
}

.model-opt[aria-selected="true"] {
  background: var(--accent-soft);
}

.model-opt:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.opt-ico {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 9px;
  color: #ffffff;
}

.opt-ico svg {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.opt-head {
  display: flex;
  align-items: center;
  gap: 7px;
}

.opt-name {
  font-size: 0.875rem;
  font-weight: 600;
}

.opt-badge {
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--soft);
  color: var(--muted);
  font-size: 0.6875rem;
  font-weight: 600;
}

.model-opt[aria-selected="true"] .opt-badge {
  background: var(--card);
  color: var(--accent-text);
}

.opt-desc {
  margin: 1px 0 0;
  font-size: 0.75rem;
  color: var(--muted);
}

.opt-meta {
  font-family: ui-monospace, Consolas, monospace;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
}

.model-opt[aria-selected="true"] .opt-meta {
  color: var(--accent-text);
}

textarea {
  display: block;
  width: 100%;
  min-height: 228px;
  padding: 10px 4px;
  border: 0;
  outline: 0;
  resize: none;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 0.9375rem;
  line-height: 1.55;
}

.composer-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding-top: 12px;
  border-top: 1px solid var(--soft);
}

.think-switch {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 34px;
  padding: 4px 11px 4px 7px;
  border: 0;
  border-radius: 999px;
  background: var(--soft);
  color: var(--text);
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
}

.think-switch:disabled {
  opacity: 0.6;
  cursor: default;
}

.switch-track {
  position: relative;
  width: 30px;
  height: 18px;
  border-radius: 999px;
  background: #cbd5e1;
  transition: background 0.15s ease;
}

.switch-track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #ffffff;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.2);
  transition: transform 0.15s ease;
}

.think-switch[aria-checked="true"] .switch-track {
  background: var(--accent);
}

.think-switch[aria-checked="true"] .switch-track::after {
  transform: translateX(12px);
}

.switch-note {
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--muted);
}

.foot-right {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
}

.tokens {
  font-family: ui-monospace, Consolas, monospace;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.send-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 34px;
  padding: 6px 14px;
  border: 0;
  border-radius: 999px;
  background: var(--text);
  color: var(--card);
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.15s ease, opacity 0.15s ease;
}

.send-btn:hover {
  transform: translateY(-1px);
}

.send-btn svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.picker-btn:focus-visible,
.think-switch:focus-visible,
.send-btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.reply {
  margin: 10px 0 0;
  padding: 9px 12px;
  border-radius: 12px;
  background: var(--accent-soft);
  color: var(--accent-text);
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.45;
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (max-width: 520px) {
  .page {
    padding-top: 20px;
  }

  .model-note {
    display: none;
  }
}

@media (pointer: coarse) {
  .picker-btn,
  .think-switch,
  .send-btn,
  .model-opt,
  textarea {
    min-height: 44px;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --page: #0b1120;
    --card: #151e30;
    --text: #f1f5f9;
    --muted: #94a3b8;
    --soft: #1e293b;
    --hover: #293548;
    --accent: #3b82f6;
    --accent-soft: #172554;
    --accent-text: #93c5fd;
    --ring: rgba(255, 255, 255, 0.08);
    --shadow: 0 18px 40px -14px rgba(0, 0, 0, 0.6);
    --menu-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.75);
  }

  .switch-track {
    background: #334155;
  }
}

@media (forced-colors: active) {
  .composer,
  .model-list {
    border: 1px solid CanvasText;
  }

  .model-opt[aria-selected="true"] {
    outline: 2px solid Highlight;
  }

  .switch-track {
    border: 1px solid ButtonText;
  }
}

@media (prefers-reduced-motion: reduce) {
  .picker-btn,
  .picker-chevron,
  .switch-track,
  .switch-track::after,
  .send-btn {
    transition: none;
  }
}
const MODELS = [
  { id: "balanced", name: "Nova 4 Balanced", badge: "Default", color: "#2563eb", context: 200000, tag: "200k", cost: "$$", desc: "Everyday coding and writing", thinking: "optional", icon: "M12 2L2 7l10 5 10-5-10-5ZM2 17l10 5 10-5M2 12l10 5 10-5" },
  { id: "pro", name: "Nova 4 Pro", badge: "Deep work", color: "#7c3aed", context: 200000, tag: "200k", cost: "$$$", desc: "Hard refactors and long documents", thinking: "optional", icon: "M6 3h12l4 6-10 12L2 9l4-6ZM2 9h20" },
  { id: "flash", name: "Nova 4 Flash", badge: "Fastest", color: "#059669", context: 128000, tag: "128k", cost: "$", desc: "Quick answers and low latency", thinking: "off", icon: "M13 2L3 14h9l-1 8 10-12h-9l1-8Z" },
  { id: "reasoner", name: "Nova R1 Reasoner", badge: "Reasoning", color: "#ea580c", context: 128000, tag: "128k", cost: "$$$", desc: "Step-by-step math and architecture", thinking: "locked", icon: "M9 3h6v6H9V3ZM4 15h6v6H4v-6ZM14 15h6v6h-6v-6ZM12 9v3m0 0H7v3m5-3h5v3" }
];

const picker = document.getElementById("picker");
const pickerBtn = document.getElementById("picker-btn");
const pickerDot = document.getElementById("picker-dot");
const pickerName = document.getElementById("picker-name");
const pickerTag = document.getElementById("picker-tag");
const list = document.getElementById("model-list");
const modelNote = document.getElementById("model-note");
const prompt = document.getElementById("prompt");
const thinkSwitch = document.getElementById("think-switch");
const thinkNote = document.getElementById("think-note");
const tokensEl = document.getElementById("tokens");
const sendBtn = document.getElementById("send");
const reply = document.getElementById("reply");
const status = document.getElementById("status");

let selectedIndex = 0;
let userThinking = false;

function el(tag, className, text) {
  const node = document.createElement(tag);
  if (className) node.className = className;
  if (text) node.textContent = text;
  return node;
}

function makeIcon(pathData) {
  const ns = "http://www.w3.org/2000/svg";
  const svg = document.createElementNS(ns, "svg");
  const path = document.createElementNS(ns, "path");
  svg.setAttribute("viewBox", "0 0 24 24");
  svg.setAttribute("aria-hidden", "true");
  path.setAttribute("d", pathData);
  svg.append(path);
  return svg;
}

function isThinkingOn(model) {
  if (model.thinking === "locked") return true;
  if (model.thinking === "off") return false;
  return userThinking;
}

// Estimate ~4 chars per token plus the 4k reasoning budget
function updateTokens() {
  const model = MODELS[selectedIndex];
  const chars = prompt.value.trim().length;
  const promptTokens = chars ? Math.max(1, Math.ceil(chars / 4)) : 0;
  const budget = isThinkingOn(model) ? 4000 : 0;
  const total = promptTokens + budget;
  tokensEl.textContent = total.toLocaleString("en-US") + " / " + model.context.toLocaleString("en-US") + " tokens";
}

function applyModel(index, announce) {
  selectedIndex = index;
  const model = MODELS[index];
  pickerDot.style.background = model.color;
  pickerName.textContent = model.name;
  pickerTag.textContent = model.tag;
  modelNote.textContent = model.desc;

  const on = isThinkingOn(model);
  thinkSwitch.setAttribute("aria-checked", String(on));
  thinkSwitch.disabled = model.thinking !== "optional";
  if (model.thinking === "locked") {
    thinkNote.textContent = "Always on";
  } else if (model.thinking === "off") {
    thinkNote.textContent = "Not used";
  } else {
    thinkNote.textContent = on ? "+4k active" : "+4k budget";
  }

  const options = list.querySelectorAll(".model-opt");
  options.forEach(function(node, i) {
    node.setAttribute("aria-selected", String(i === index));
  });

  updateTokens();
  if (announce) status.textContent = "Selected " + model.name + ", " + model.tag + " context";
}

function openMenu(focusSelected) {
  list.hidden = false;
  pickerBtn.setAttribute("aria-expanded", "true");
  if (focusSelected) {
    const active = list.children[selectedIndex];
    if (active) active.focus();
  }
}

function closeMenu(returnFocus) {
  if (list.hidden) return;
  list.hidden = true;
  pickerBtn.setAttribute("aria-expanded", "false");
  if (returnFocus) pickerBtn.focus();
}

MODELS.forEach(function(model, i) {
  const li = el("li", "model-opt");
  const ico = el("span", "opt-ico");
  const left = el("div", "");
  const head = el("div", "opt-head");
  const name = el("span", "opt-name", model.name);
  const badge = el("span", "opt-badge", model.badge);
  const desc = el("p", "opt-desc", model.desc);
  const meta = el("div", "opt-meta", model.tag + " \u00b7 " + model.cost);
  ico.style.background = model.color;
  ico.append(makeIcon(model.icon));
  li.setAttribute("role", "option");
  li.tabIndex = -1;
  head.append(name, badge);
  left.append(head, desc);
  li.append(ico, left, meta);
  li.addEventListener("click", function() {
    applyModel(i, true);
    closeMenu(true);
  });
  list.append(li);
});

pickerBtn.addEventListener("click", function() {
  if (list.hidden) openMenu(true);
  else closeMenu(true);
});

pickerBtn.addEventListener("keydown", function(e) {
  if (e.key === "ArrowDown" || e.key === "ArrowUp") {
    e.preventDefault();
    openMenu(true);
  }
});

list.addEventListener("keydown", function(e) {
  const options = Array.from(list.children);
  const current = options.indexOf(document.activeElement);
  if (e.key === "ArrowDown") {
    e.preventDefault();
    options[(current + 1) % options.length].focus();
  } else if (e.key === "ArrowUp") {
    e.preventDefault();
    options[(current - 1 + options.length) % options.length].focus();
  } else if (e.key === "Home") {
    e.preventDefault();
    options[0].focus();
  } else if (e.key === "End") {
    e.preventDefault();
    options[options.length - 1].focus();
  } else if (e.key === "Enter" || e.key === " ") {
    e.preventDefault();
    if (current >= 0) {
      applyModel(current, true);
      closeMenu(true);
    }
  } else if (e.key === "Escape") {
    e.preventDefault();
    closeMenu(true);
  } else if (e.key === "Tab") {
    closeMenu(false);
  }
});

thinkSwitch.addEventListener("click", function() {
  const model = MODELS[selectedIndex];
  if (model.thinking !== "optional") return;
  userThinking = !userThinking;
  applyModel(selectedIndex, false);
  status.textContent = "Extended thinking " + (userThinking ? "on" : "off");
});

prompt.addEventListener("input", updateTokens);

sendBtn.addEventListener("click", function() {
  const model = MODELS[selectedIndex];
  const thinking = isThinkingOn(model) ? "with extended thinking" : "with standard response";
  closeMenu(false);
  reply.hidden = false;
  reply.textContent = "Sent to " + model.name + " (" + model.tag + " context) " + thinking + ".";
  status.textContent = reply.textContent;
});

document.addEventListener("pointerdown", function(e) {
  if (!picker.contains(e.target)) closeMenu(false);
});

applyModel(0, false);

// Open the menu for the demo, remove this line to start closed
openMenu(false);
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save