Thinking Trace
A collapsible reasoning panel that mimics modern reasoning models — a live Thinking header with a running timer and shimmering placeholder while tokens stream in, then folds into a one-line summary the moment it finishes. Expand it to replay the full step-by-step chain of thought, watch the elapsed duration, and flip an auto-collapse switch so the trace tucks itself away automatically once reasoning completes.
MCP
Code
:root {
--bg: #0c0d10;
--surface: #15161b;
--surface-2: #1d1f27;
--ink: #e9eaf0;
--muted: #9a9cab;
--accent: #8b5cf6;
--accent-2: #22d3ee;
--ok: #34d399;
--warn: #fbbf24;
--danger: #f87171;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(900px 500px at 15% -10%, rgba(139, 92, 246, 0.16), transparent 60%),
radial-gradient(800px 500px at 100% 0%, rgba(34, 211, 238, 0.10), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100%;
display: grid;
place-items: center;
padding: 32px 18px;
}
.stage {
width: 100%;
max-width: 640px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* ===== Chat ===== */
.chat {
display: flex;
flex-direction: column;
gap: 18px;
}
.msg {
display: flex;
gap: 12px;
align-items: flex-start;
}
.msg--user { flex-direction: row-reverse; }
.avatar {
flex: 0 0 auto;
width: 34px;
height: 34px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 11px;
font-weight: 600;
border: 1px solid var(--line);
}
.avatar--user {
background: var(--surface-2);
color: var(--muted);
}
.avatar--ai {
background: linear-gradient(135deg, rgba(139, 92, 246, 0.35), rgba(34, 211, 238, 0.25));
color: var(--ink);
box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.35), 0 6px 18px rgba(139, 92, 246, 0.18);
}
.bubble {
border-radius: var(--r-lg);
padding: 12px 14px;
font-size: 14.5px;
max-width: 80%;
}
.bubble--user {
background: var(--surface-2);
border: 1px solid var(--line);
border-top-right-radius: 6px;
}
.bubble--ai {
background: transparent;
padding: 0;
max-width: 88%;
flex: 1;
min-width: 0;
}
/* ===== Trace ===== */
.trace {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
transition: border-color 0.25s ease, box-shadow 0.25s ease;
}
.trace[data-state="thinking"] {
border-color: rgba(139, 92, 246, 0.45);
box-shadow: 0 0 0 1px rgba(139, 92, 246, 0.20), 0 10px 30px rgba(0, 0, 0, 0.35);
}
.trace[data-state="done"] { border-color: var(--line-2); }
.trace__head {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 11px 13px;
background: transparent;
border: 0;
color: var(--ink);
font: inherit;
text-align: left;
cursor: pointer;
transition: background 0.18s ease;
}
.trace__head:hover { background: rgba(255, 255, 255, 0.03); }
.trace__head:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.trace__icon {
flex: 0 0 auto;
width: 26px;
height: 26px;
display: grid;
place-items: center;
border-radius: var(--r-sm);
background: var(--surface-2);
color: var(--accent);
border: 1px solid var(--line);
}
.trace[data-state="thinking"] .trace__brain { animation: pulse 1.3s ease-in-out infinite; }
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.85; }
50% { transform: scale(1.18); opacity: 1; }
}
.trace__title {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
gap: 10px;
}
.trace__label {
font-size: 13.5px;
font-weight: 600;
letter-spacing: 0.01em;
white-space: nowrap;
}
.trace[data-state="thinking"] .trace__label {
background: linear-gradient(90deg, var(--muted) 20%, var(--ink) 50%, var(--muted) 80%);
background-size: 220% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: sweep 1.6s linear infinite;
}
@keyframes sweep {
0% { background-position: 120% 0; }
100% { background-position: -120% 0; }
}
/* Shimmer skeleton line, only while thinking */
.trace__shimmer {
display: none;
flex: 1;
height: 9px;
border-radius: 999px;
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.04) 25%, rgba(255, 255, 255, 0.14) 37%, rgba(255, 255, 255, 0.04) 63%);
background-size: 400% 100%;
animation: shimmer 1.4s ease infinite;
}
.trace[data-state="thinking"] .trace__shimmer { display: block; }
@keyframes shimmer {
0% { background-position: 100% 0; }
100% { background-position: -100% 0; }
}
.trace__meta {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 9px;
color: var(--muted);
font-size: 12px;
}
.trace__timer {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--accent-2);
min-width: 40px;
text-align: right;
}
.trace[data-state="done"] .trace__timer { color: var(--muted); }
.trace__steps {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 11.5px;
padding: 2px 7px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line);
color: var(--muted);
}
.trace__chev {
transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
color: var(--muted);
}
.trace[data-open="true"] .trace__chev { transform: rotate(180deg); }
.trace[data-state="thinking"] .trace__chev { display: none; }
/* Collapser — animated grid rows, with max-height fallback */
.trace__collapser {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.32s cubic-bezier(0.4, 0, 0.2, 1);
}
.trace[data-open="true"] .trace__collapser { grid-template-rows: 1fr; }
.trace__inner {
min-height: 0;
overflow: hidden;
}
@supports not (grid-template-rows: 1fr) {
.trace__collapser {
display: block;
max-height: 0;
overflow: hidden;
transition: max-height 0.32s ease;
}
.trace[data-open="true"] .trace__collapser { max-height: 520px; }
}
.trace__steps-list {
list-style: none;
margin: 0;
padding: 4px 13px 14px 13px;
border-top: 1px solid var(--line);
display: flex;
flex-direction: column;
gap: 12px;
}
.trace__steps-list:empty { padding: 0; border-top: 0; }
.step {
display: flex;
gap: 11px;
font-size: 13px;
color: var(--muted);
animation: fadeStep 0.4s ease both;
}
@keyframes fadeStep {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: translateY(0); }
}
.step__num {
flex: 0 0 auto;
width: 20px;
height: 20px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 10.5px;
font-weight: 600;
font-family: "JetBrains Mono", monospace;
color: var(--accent);
background: rgba(139, 92, 246, 0.14);
border: 1px solid rgba(139, 92, 246, 0.30);
margin-top: 1px;
}
.step__body { min-width: 0; }
.step__head { color: var(--ink); font-weight: 500; }
.step__text { color: var(--muted); font-size: 12.5px; }
/* ===== Answer ===== */
.answer {
margin-top: 12px;
padding: 12px 14px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
border-top-left-radius: 6px;
font-size: 14.5px;
animation: fadeStep 0.45s ease both;
}
.answer p { margin: 0; }
.answer code {
font-family: "JetBrains Mono", monospace;
font-size: 12.5px;
padding: 1px 5px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.06);
color: var(--accent-2);
}
/* ===== Controls ===== */
.controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 14px;
padding: 14px 16px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.btn {
display: inline-flex;
align-items: center;
gap: 7px;
font: inherit;
font-size: 13px;
font-weight: 600;
padding: 8px 14px;
border-radius: var(--r-sm);
border: 1px solid var(--line-2);
background: var(--surface-2);
color: var(--ink);
cursor: pointer;
transition: transform 0.12s ease, background 0.18s ease, box-shadow 0.18s ease;
}
.btn:hover { background: #24262f; }
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn--primary {
background: linear-gradient(135deg, var(--accent), #6d43e0);
border-color: transparent;
box-shadow: 0 6px 18px rgba(139, 92, 246, 0.28);
}
.btn--primary:hover { background: linear-gradient(135deg, #9a6bff, #7a4ff0); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
/* Switch */
.switch {
display: inline-flex;
align-items: center;
gap: 9px;
cursor: pointer;
user-select: none;
font-size: 12.5px;
color: var(--muted);
}
.switch input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.switch__track {
width: 38px;
height: 22px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line-2);
position: relative;
transition: background 0.2s ease, border-color 0.2s ease;
flex: 0 0 auto;
}
.switch__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--muted);
transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1), background 0.2s ease;
}
.switch input:checked + .switch__track {
background: rgba(139, 92, 246, 0.35);
border-color: var(--accent);
}
.switch input:checked + .switch__track .switch__thumb {
transform: translateX(16px);
background: var(--accent);
}
.switch input:focus-visible + .switch__track {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.switch:hover .switch__text { color: var(--ink); }
.hint {
margin-left: auto;
font-size: 12px;
color: var(--muted);
font-family: "JetBrains Mono", monospace;
}
@media (max-width: 520px) {
body { padding: 20px 12px; }
.bubble { max-width: 100%; }
.bubble--ai { max-width: 100%; }
.msg { gap: 9px; }
.avatar { width: 30px; height: 30px; }
.hint { margin-left: 0; width: 100%; }
.controls { gap: 12px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}(function () {
"use strict";
// Simulated chain-of-thought. Each step streams in with a small delay.
var STEPS = [
{
head: "Parse the rule",
text: "Autocomplete resolves a word once its third letter is typed, then one accept keystroke commits the suggestion.",
delay: 700
},
{
head: "Type the prefix",
text: "Enter b, a, n — that is 3 keystrokes for the first three letters.",
delay: 650
},
{
head: "Check ambiguity",
text: "After “ban” the intended target is “banana”. Assume it is the sole/top completion offered.",
delay: 700
},
{
head: "Count the accept",
text: "Accepting the suggestion (Tab or Enter) is 1 more keystroke.",
delay: 600
},
{
head: "Sum keystrokes",
text: "3 letters + 1 accept = 4 total keystrokes.",
delay: 650
},
{
head: "Sanity check",
text: "Typing all 6 letters manually would be 6, so autocomplete saves 2 — consistent.",
delay: 600
}
];
var trace = document.querySelector(".trace");
var head = trace.querySelector(".trace__head");
var labelEl = trace.querySelector('[data-role="label"]');
var timerEl = trace.querySelector('[data-role="timer"]');
var stepCountEl = trace.querySelector('[data-role="stepcount"]');
var stepsList = trace.querySelector('[data-role="steps"]');
var answerEl = document.querySelector('[data-role="answer"]');
var runBtn = document.querySelector('[data-role="run"]');
var autoCollapseEl = document.querySelector('[data-role="autocollapse"]');
var statusEl = document.querySelector('[data-role="status"]');
var rafId = null;
var timeouts = [];
var startTime = 0;
var finalDuration = 0;
var isThinking = false;
var userToggledDuringRun = false;
function fmt(sec) {
return sec.toFixed(1) + "s";
}
function clearTimers() {
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
timeouts.forEach(clearTimeout);
timeouts = [];
}
// rAF-driven, drift-free elapsed timer.
function tick() {
var elapsed = (performance.now() - startTime) / 1000;
timerEl.textContent = fmt(elapsed);
rafId = requestAnimationFrame(tick);
}
function setOpen(open) {
trace.setAttribute("data-open", open ? "true" : "false");
head.setAttribute("aria-expanded", open ? "true" : "false");
}
function addStep(step, index) {
var li = document.createElement("li");
li.className = "step";
var num = document.createElement("span");
num.className = "step__num";
num.textContent = index + 1;
var body = document.createElement("div");
body.className = "step__body";
var h = document.createElement("div");
h.className = "step__head";
h.textContent = step.head;
var t = document.createElement("div");
t.className = "step__text";
t.textContent = step.text;
body.appendChild(h);
body.appendChild(t);
li.appendChild(num);
li.appendChild(body);
stepsList.appendChild(li);
}
function finish() {
isThinking = false;
clearTimers();
finalDuration = (performance.now() - startTime) / 1000;
trace.setAttribute("data-state", "done");
labelEl.textContent = "Thought for " + fmt(finalDuration);
timerEl.textContent = fmt(finalDuration);
timerEl.hidden = true;
stepCountEl.hidden = false;
stepCountEl.textContent = STEPS.length + " steps";
answerEl.hidden = false;
statusEl.textContent = "Done · " + fmt(finalDuration);
runBtn.disabled = false;
// Auto-collapse honors the switch, unless the user manually opened it mid-run.
if (autoCollapseEl.checked && !userToggledDuringRun) {
setOpen(false);
} else {
setOpen(true);
}
}
function run() {
clearTimers();
userToggledDuringRun = false;
isThinking = true;
finalDuration = 0;
// Reset UI
stepsList.innerHTML = "";
answerEl.hidden = true;
stepCountEl.hidden = true;
timerEl.hidden = false;
timerEl.textContent = "0.0s";
labelEl.textContent = "Thinking";
trace.setAttribute("data-state", "thinking");
setOpen(true); // expanded while streaming so you can watch steps arrive
runBtn.disabled = true;
statusEl.textContent = "Streaming reasoning…";
startTime = performance.now();
rafId = requestAnimationFrame(tick);
// Stream steps sequentially using accumulated delays.
var acc = 0;
STEPS.forEach(function (step, i) {
acc += step.delay;
timeouts.push(
setTimeout(function () {
addStep(step, i);
}, acc)
);
});
// Finish shortly after the last step lands.
timeouts.push(setTimeout(finish, acc + 500));
}
// Header toggle (expand/collapse). Disabled while actively thinking is allowed —
// user can peek, but we record the manual intent so auto-collapse won't override.
head.addEventListener("click", function () {
var open = trace.getAttribute("data-open") === "true";
setOpen(!open);
if (isThinking) userToggledDuringRun = true;
});
runBtn.addEventListener("click", run);
// If the user flips auto-collapse after a finished run, apply it immediately.
autoCollapseEl.addEventListener("change", function () {
if (!isThinking && finalDuration > 0) {
setOpen(!autoCollapseEl.checked);
userToggledDuringRun = false;
}
});
// Kick off the first run on load.
run();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Thinking Trace</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="stage">
<div class="chat" aria-label="Assistant conversation">
<!-- User message -->
<div class="msg msg--user">
<div class="avatar avatar--user" aria-hidden="true">You</div>
<div class="bubble bubble--user">
How many keystrokes does it take to type "banana" if autocomplete
finishes any word after its third letter?
</div>
</div>
<!-- Assistant message with thinking trace -->
<div class="msg msg--ai">
<div class="avatar avatar--ai" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3a3 3 0 0 0-3 3 3 3 0 0 0-2 5.2A3 3 0 0 0 9 17a3 3 0 0 0 6 0 3 3 0 0 0 2-5.8A3 3 0 0 0 15 6a3 3 0 0 0-3-3Z"/>
<path d="M12 6v11M9 9.5h6M8.5 13h7"/>
</svg>
</div>
<div class="bubble bubble--ai">
<!-- ===== Thinking trace ===== -->
<section class="trace" data-state="idle" aria-label="Model reasoning">
<button class="trace__head" type="button"
aria-expanded="false" aria-controls="trace-body">
<span class="trace__icon" aria-hidden="true">
<svg class="trace__brain" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3a3 3 0 0 0-3 3 3 3 0 0 0-2 5.2A3 3 0 0 0 9 17a3 3 0 0 0 6 0 3 3 0 0 0 2-5.8A3 3 0 0 0 15 6a3 3 0 0 0-3-3Z"/>
</svg>
</span>
<span class="trace__title">
<span class="trace__label" data-role="label">Thinking</span>
<span class="trace__shimmer" data-role="shimmer" aria-hidden="true"></span>
</span>
<span class="trace__meta">
<time class="trace__timer" data-role="timer" aria-hidden="true">0.0s</time>
<span class="trace__steps" data-role="stepcount" hidden></span>
<svg class="trace__chev" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="m6 9 6 6 6-6"/>
</svg>
</span>
</button>
<div class="trace__collapser" id="trace-body">
<div class="trace__inner">
<ol class="trace__steps-list" data-role="steps" aria-live="polite"></ol>
</div>
</div>
</section>
<!-- Final answer -->
<div class="answer" data-role="answer" hidden>
<p><strong>4 keystrokes.</strong> You type <code>b-a-n</code>, and after the
third letter autocomplete resolves the only match — <em>banana</em> — so one
accept keystroke (Tab/Enter) completes it. That's 3 letters + 1 accept.</p>
</div>
</div>
</div>
</div>
<!-- Controls -->
<div class="controls" role="group" aria-label="Demo controls">
<button class="btn btn--primary" data-role="run" type="button">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 3 14 9-14 9V3Z"/></svg>
Run again
</button>
<label class="switch">
<input type="checkbox" data-role="autocollapse" checked />
<span class="switch__track" aria-hidden="true"><span class="switch__thumb"></span></span>
<span class="switch__text">Auto-collapse on finish</span>
</label>
<span class="hint" data-role="status" aria-live="polite">Streaming reasoning…</span>
</div>
</main>
<script src="script.js"></script>
</body>
</html>Thinking Trace recreates the collapsible reasoning block you see in modern reasoning-model chat UIs. While the model is “thinking,” the header shows a pulsing brain icon, a shimmering skeleton line, and a live timer that ticks up in real time. Reasoning steps stream in one at a time with a soft fade, and once the model settles on an answer the block collapses to a compact summary such as Thought for 4.2s · 6 steps.
Click the header (or press Enter/Space) to expand and replay the entire chain of thought — each numbered step with its own label — then collapse it again to reclaim space. A toggle switch controls whether the trace auto-collapses the instant it finishes, mirroring the behavior of assistants that hide their scratchpad by default. A “Run again” control restarts the stream so you can watch the whole lifecycle repeat.
The demo is driven by requestAnimationFrame for the smooth, drift-free elapsed timer and by setTimeout-scheduled streaming to simulate token latency, with an animated grid-template-rows transition (and a max-height fallback) powering the collapse. Everything is vanilla JS and CSS — no libraries, no build step — and the whole widget is keyboard-operable with proper aria-expanded / aria-live semantics.