About this snippet

A reply that shows its reasoning while the model works. The label reads Thinking with a shimmer and a seconds counter, and the reasoning streams in word by word underneath. When the reasoning ends, the panel folds away, the label switches to a Thought for N seconds summary with the real elapsed time, and the answer streams in below it. Stop ends the reply early, and Ask again replays it.

The toggle is a button with aria-expanded and aria-controls, so screen readers know whether the reasoning is open. The panel opens and closes by animating grid-template-rows between 1fr and 0fr, which reaches the content's real height without measuring it. The panel folds away by itself only if the reader has not used the toggle, so someone who closed or reopened it keeps that choice. A hidden status region announces Thinking, the time taken and when the answer is ready.

The sample question asks whether 221 is prime, and the reasoning tests prime divisors up to 13, which is enough because 15 times 15 is already more than 221. To use a real model, append the reasoning and answer chunks it streams to the two paragraphs in place of streamWords. The shimmer and the folding animation turn off for visitors who prefer reduced motion.

Size
8.9 KB
Uses
HTML, CSS, JavaScript
Loads
Nothing external
Views
23
<main class="thread">
  <p class="msg-user">Is 221 a prime number?</p>
  <article class="msg-ai" id="reply" aria-label="Assistant reply" aria-busy="false">
    <div class="reasoning" id="reasoning">
      <button type="button" class="reasoning-toggle" id="reasoning-toggle" aria-expanded="true" aria-controls="reasoning-body">
        <span class="reasoning-label" id="reasoning-label">Thinking</span>
        <span class="reasoning-time" id="reasoning-time"></span>
        <svg class="chevron" viewBox="0 0 20 20" aria-hidden="true"><path d="M6 8l4 4 4-4"></path></svg>
      </button>
      <div class="reasoning-body" id="reasoning-body">
        <div class="reasoning-inner">
          <p id="reasoning-text"></p>
        </div>
      </div>
    </div>
    <p class="answer" id="answer"></p>
  </article>
  <div class="actions">
    <button type="button" class="btn" id="stop" disabled>Stop</button>
    <button type="button" class="btn" id="retry">Ask again</button>
  </div>
  <p class="sr-only" id="status" role="status"></p>
</main>
*, *::before, *::after {
  box-sizing: border-box;
}

:root {
  --page: #f8fafc;
  --text: #0f172a;
  --muted: #5b6678;
  --faint: #64748b;
  --line: #e2e8f0;
  --bubble: #e8eef6;
  --shine: #0f172a;
  --focus: #2563eb;
}

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

.thread {
  display: flex;
  flex-direction: column;
  gap: 20px;
  max-width: 640px;
  margin: 0 auto;
  padding: clamp(28px, 12vh, 96px) 20px 32px;
}

.msg-user {
  align-self: flex-end;
  max-width: 80%;
  margin: 0;
  padding: 12px 18px;
  border-radius: 20px 20px 6px 20px;
  background: var(--bubble);
  line-height: 1.5;
  overflow-wrap: anywhere;
}

.msg-ai {
  font-size: 1.0625rem;
  line-height: 1.65;
  overflow-wrap: anywhere;
}

.reasoning-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 36px;
  padding: 4px 6px;
  margin-left: -6px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 600;
  cursor: pointer;
}

.reasoning-toggle:hover {
  color: var(--text);
}

.reasoning-toggle:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.reasoning-time {
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--faint);
}

.reasoning-time:empty {
  display: none;
}

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

.is-collapsed .chevron {
  transform: rotate(-90deg);
}

.is-thinking .reasoning-label {
  background: linear-gradient(90deg, var(--muted) 0%, var(--muted) 40%, var(--shine) 50%, var(--muted) 60%, var(--muted) 100%);
  background-size: 250% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: shimmer 1.8s linear infinite;
}

.reasoning-body {
  display: grid;
  grid-template-rows: 1fr;
  transition: grid-template-rows 0.3s ease;
}

.is-collapsed .reasoning-body {
  grid-template-rows: 0fr;
}

.reasoning-inner {
  min-height: 0;
  overflow: hidden;
}

.is-collapsed .reasoning-inner {
  visibility: hidden;
  transition: visibility 0s 0.3s;
}

.reasoning-inner p {
  margin: 8px 0 4px;
  padding-left: 14px;
  border-left: 2px solid var(--line);
  color: var(--muted);
  font-size: 0.9375rem;
  line-height: 1.65;
}

.answer {
  margin: 14px 0 0;
}

.answer:empty {
  display: none;
}

.answer.is-muted {
  color: var(--faint);
  font-style: italic;
}

.actions {
  display: flex;
  gap: 10px;
}

.btn {
  min-height: 40px;
  padding: 8px 16px;
  border: 1px solid #b8c2d0;
  border-radius: 10px;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 600;
  cursor: pointer;
}

.btn:hover:not(:disabled) {
  background: var(--bubble);
}

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

.btn:disabled {
  opacity: 0.5;
  cursor: default;
}

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

@keyframes shimmer {
  from {
    background-position: 100% 0;
  }
  to {
    background-position: 0 0;
  }
}

@media (pointer: coarse) {
  .reasoning-toggle,
  .btn {
    min-height: 44px;
  }
}

@media (prefers-color-scheme: dark) {
  :root {
    --page: #0f172a;
    --text: #e2e8f0;
    --muted: #a3afc2;
    --faint: #94a3b8;
    --line: #334155;
    --bubble: #1e293b;
    --shine: #ffffff;
    --focus: #60a5fa;
  }

  .btn {
    border-color: #475569;
  }
}

@media (prefers-reduced-motion: reduce) {
  .is-thinking .reasoning-label {
    animation: none;
    background: none;
    color: var(--muted);
  }

  .reasoning-body,
  .chevron {
    transition: none;
  }
}
const reasoningText = "221 is odd, so 2 is not a factor. Its digits add up to 5, so 3 is out too, and it doesn't end in 0 or 5. Next, 221 ÷ 7 is about 31.6 and 221 ÷ 11 is about 20.1, so neither divides evenly. 221 ÷ 13 is exactly 17. I only need to test primes up to 14, because 15 × 15 is already 225.";
const answerText = "No, 221 is not prime. It equals 13 × 17, so its divisors are 1, 13, 17 and 221.";

const reply = document.getElementById("reply");
const panel = document.getElementById("reasoning");
const toggle = document.getElementById("reasoning-toggle");
const label = document.getElementById("reasoning-label");
const clockEl = document.getElementById("reasoning-time");
const thoughts = document.getElementById("reasoning-text");
const answer = document.getElementById("answer");
const stopBtn = document.getElementById("stop");
const retryBtn = document.getElementById("retry");
const status = document.getElementById("status");

let run = 0;
let phase = "idle";
let started = 0;
let streamTimer = null;
let clockTimer = null;
let userToggled = false;

function setOpen(open) {
  panel.classList.toggle("is-collapsed", !open);
  toggle.setAttribute("aria-expanded", String(open));
}

function elapsed() {
  return Math.floor((performance.now() - started) / 1000);
}

function seconds(n) {
  return n + (n === 1 ? " second" : " seconds");
}

function wait(ms) {
  return new Promise(function(resolve) {
    setTimeout(resolve, ms);
  });
}

// Add the text to an element one word at a time
function streamWords(el, text, delay) {
  const words = text.split(" ");
  let i = 0;
  return new Promise(function(resolve) {
    streamTimer = setInterval(function() {
      el.textContent += (i ? " " : "") + words[i];
      i++;
      if (i === words.length) {
        clearInterval(streamTimer);
        resolve();
      }
    }, delay);
  });
}

function setBusy(busy) {
  const focused = document.activeElement;
  reply.setAttribute("aria-busy", String(busy));
  stopBtn.disabled = !busy;
  retryBtn.disabled = busy;
  // Move focus off a button that was just disabled
  if (focused === stopBtn && !busy) retryBtn.focus();
  if (focused === retryBtn && busy) stopBtn.focus();
}

async function ask() {
  const id = ++run;
  clearInterval(streamTimer);
  clearInterval(clockTimer);
  phase = "thinking";
  started = performance.now();
  userToggled = false;
  thoughts.textContent = "";
  answer.textContent = "";
  answer.classList.remove("is-muted");
  label.textContent = "Thinking";
  clockEl.textContent = "0s";
  panel.classList.add("is-thinking");
  setOpen(true);
  setBusy(true);
  status.textContent = "Thinking";

  // Tick the timer while the model thinks
  clockTimer = setInterval(function() {
    clockEl.textContent = elapsed() + "s";
  }, 250);

  await streamWords(thoughts, reasoningText, 70);
  if (id !== run) return;
  await wait(400);
  if (id !== run) return;

  clearInterval(clockTimer);
  const took = Math.max(1, elapsed());
  panel.classList.remove("is-thinking");
  label.textContent = "Thought for " + seconds(took);
  clockEl.textContent = "";
  // Fold the reasoning away unless the reader opened or closed it
  if (!userToggled) setOpen(false);
  status.textContent = "Thought for " + seconds(took);

  phase = "answering";
  await streamWords(answer, answerText, 45);
  if (id !== run) return;
  phase = "idle";
  setBusy(false);
  status.textContent = "Answer ready";
}

function stop() {
  if (phase === "idle") return;
  run++;
  clearInterval(streamTimer);
  clearInterval(clockTimer);
  if (phase === "thinking") {
    const s = elapsed();
    panel.classList.remove("is-thinking");
    label.textContent = s ? "Stopped after " + seconds(s) : "Stopped";
    clockEl.textContent = "";
  }
  if (answer.textContent) {
    answer.textContent += " (stopped)";
  } else {
    answer.textContent = "You stopped this reply.";
    answer.classList.add("is-muted");
  }
  phase = "idle";
  setBusy(false);
  status.textContent = "Reply stopped";
}

toggle.addEventListener("click", function() {
  userToggled = true;
  setOpen(toggle.getAttribute("aria-expanded") !== "true");
});

stopBtn.addEventListener("click", stop);
retryBtn.addEventListener("click", ask);

setTimeout(ask, 500);
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