Agent statusThinking states
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises, from Astro sites to React apps and AI products.
Book a callThinking states
Six thinking indicators to pick from, each with a status line. Free and MIT licensed, as HTML, a typed React component or an Astro component, in two styles with light and dark mode.
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add thinking-statesReact
npx designme add thinking-states --reactAstro
npx designme add thinking-states --astroThe CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro islands. The Astro version lands in src/components/designme too, with the same markup, CSS and script as the HTML version. See Astro.
shadcn CLI, React
npx shadcn@latest add https://designme.dev/r/shadcn/thinking-states.json shadcn CLI, Astro
npx shadcn@latest add https://designme.dev/r/shadcn/astro/thinking-states.json In a project set up for shadcn/ui, its CLI adds the same files to your components folder, under designme/, with the shared core. To give the components your shadcn or Tailwind v4 theme, add the theme bridge.
About
What it is for and what it does.
Thinking states is a set of six indicators for the moment an agent is thinking, to pick from: Orbit, Morph, Scan, Stream, Matrix and Stack. Each pairs a small animated mark on a tile with one status line, like "Reading 3 files", that steps through what the agent is doing, and a quiet count of the seconds so far.
- While an agent works out an answer and there is nothing to show yet, in a chat thread or beside a panel.
- When you want one line that says what the agent is doing now, not a full trace of every step.
- When your product needs a thinking indicator of its own: pick the mark that suits it, from calm (Morph) to busy (Matrix).
- When the answer will be text, use Scan or Stream, whose lines of dots and typed line hint at what is coming.
variantpicks the indicator. Five of them are grids of dots, each at its own density:orbit(5 by 5, a light running round the edge with a fading tail and a dot breathing in the middle, the line shimmering),morph(7 by 7, the dots drawing a rectangle, then a circle, then a triangle),scan(6 by 6, a column of light sweeping three lines of text in dots),stream(three full lines of six dots that light up one by one, line by line, then fade out together, the line typed out behind a caret),matrix(a four by four grid of dots, each lighting up on its own beat) orstack(pages shuffling to the front).- Pass
steps, a list of status lines. Leavecurrentout and they take turns everyintervalmilliseconds (2600 by default), or setcurrentto show the step your agent reports. - A new line crossfades in, except in Stream, where it types out a letter at a time.
secondsshows the time so far at the end of the line. Left out, it counts by itself;falsehides it.- Marks use the accent and everything else stays grey. In Soft the tile and marks are squircles; in Blueprint the tile is square with a hairline edge, marks tick in steps, Stream's caret is a block.
- On designme.dev the switch under the demo picks the variant, and the HTML install ships both the switch and every variant.
- The status line is read from a polite live region (
role="status"), so a screen reader hears each new step once, never each frame or each typed letter. - The marks, the visible line and the seconds are hidden from screen readers, so nothing is read twice.
- With reduced motion turned on, every mark holds still in a pose that still reads (Matrix shows a lit pattern, Stack its three pages), the caret stays lit, the sweep goes and the line changes without typing or fading.
Usage
Import it in React and drive it from your own state, or place it in an Astro page.
React
import { ThinkingStates } from '@/components/designme/thinking-states/ThinkingStates';React example
The demo above as a React component, 27 lines you can copy.
'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useState } from 'react';
import { ThinkingStates, type ThinkingVariant } from './ThinkingStates';
import { Seg } from '../lib/motion';
import { Demo } from '../lib/Demo';
import { useScript } from '../lib/useScript';
/* ---------- Thinking states: the agent's steps taking turns, the clock starting again with each new question ---------- */
const STEPS = ['Thinking', 'Reading 3 files', 'Searching the docs', 'Comparing 2 options', 'Drafting the answer'];
export default function ThinkingStatesDemo() {
const [variant, setVariant] = useState<ThinkingVariant>('orbit');
const [current, setCurrent] = useState(0);
const [seconds, setSeconds] = useState(0);
useScript(async ({ wait }) => {
for (;;) {
setSeconds(0);
for (let i = 0; i < STEPS.length; i++) { setCurrent(i); for (let t = 0; t < 26; t++) { await wait(100); if (t % 10 === 9) setSeconds(s => s + 1); } }
}
});
return (
<Demo switcher={<Seg label="Thinking state" value={variant} onChange={setVariant} options={[{ value: 'orbit', label: 'Orbit' }, { value: 'morph', label: 'Morph' }, { value: 'scan', label: 'Scan' }, { value: 'stream', label: 'Stream' }, { value: 'matrix', label: 'Matrix' }, { value: 'stack', label: 'Stack' }]} />}>
<ThinkingStates variant={variant} steps={STEPS} current={current} seconds={seconds} />
</Demo>
);
}
Astro
In a page in src/pages. It plays the demo above; change its thinking-states.js to show your own data. Attributes like class go on its root element.
---
import ThinkingStates from '../components/designme/thinking-states/ThinkingStates.astro';
---
<ThinkingStates />
<ThinkingStates data-style="soft" class="my-class" />Props
Everything ThinkingStates takes.
| Prop | Type | What it does |
|---|---|---|
variant | ThinkingVariant | Which indicator: dots circling a core, a shape that morphs, a line scanning skeleton text, tokens with a typed line, a twinkling dot grid or shuffling pages |
steps | string[] | The status lines, like "Reading 3 files". With more than one, they take turns every `interval` unless `current` is set |
current | number | Index of the line to show. Leave it out to let the lines take turns by themselves |
interval | number | Milliseconds each line shows for when they take turns by themselves |
seconds | number | false | Seconds shown at the end of the line. Counts by itself when left out; false hides it |
className | string |
Code
The full source, as the CLI installs it.
ThinkingStates.tsxReact · 107 lines
'use client';
import { useEffect, useRef, useState } from 'react';
import { cx } from '../lib/cx';
import { useReducedMotion, useSeconds } from '../lib/hooks';
import { Swap } from '../lib/motion';
import '../designme.css';
import './thinking-states.css';
export type ThinkingVariant = 'orbit' | 'morph' | 'scan' | 'stream' | 'matrix' | 'stack';
export interface ThinkingStatesProps {
/** Which indicator: dots circling a core, a shape that morphs, a line scanning skeleton text, tokens with a typed line,
a twinkling dot grid or shuffling pages */
variant?: ThinkingVariant;
/** The status lines, like "Reading 3 files". With more than one, they take turns every `interval` unless `current` is set */
steps?: string[];
/** Index of the line to show. Leave it out to let the lines take turns by themselves */
current?: number;
/** Milliseconds each line shows for when they take turns by themselves */
interval?: number;
/** Seconds shown at the end of the line. Counts by itself when left out; false hides it */
seconds?: number | false;
className?: string;
}
type GridKind = 'orbit' | 'morph' | 'scan' | 'stream';
/* Orbit, Morph, Scan and Stream are drawn on a grid of dots, each at its own density (Orbit 5 by 5, Scan 6 by 6, Morph 7 by 7,
Stream 6 by 3): which part each dot plays, and its place in the run as an animation delay */
const grid = (k: GridKind) => {
const n = { orbit: 5, scan: 6, morph: 7, stream: 6 }[k], rows = k === 'stream' ? 3 : n, at = (x: number, y: number) => y * n + x;
const cls: string[] = Array(n * rows).fill(''), dly: (number | null)[] = Array(n * rows).fill(null), pose = new Set<number>();
const run = (order: number[], dur: number) => order.forEach((c, i) => { dly[c] = (i / order.length - 1) * dur; });
if (k === 'orbit') {
const ring: number[] = [];
for (let x = 0; x < n; x++) ring.push(at(x, 0));
for (let y = 1; y < n; y++) ring.push(at(n - 1, y));
for (let x = n - 2; x >= 0; x--) ring.push(at(x, n - 1));
for (let y = n - 2; y > 0; y--) ring.push(at(0, y));
ring.forEach(c => { cls[c] = 'r'; }); run(ring, 1.6); cls[at(2, 2)] = 'c';
[0, ring.length - 1, ring.length - 2].forEach(i => pose.add(ring[i]));
} else if (k === 'scan') {
const len = [6, 0, 5, 0, 3, 0];
for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) { const c = at(x, y); cls[c] = x < len[y] ? 'l' : 'b'; dly[c] = (x / n - 1) * 1.6; if (x === 2) pose.add(c); }
} else if (k === 'morph') {
for (let y = 0; y < n; y++) for (let x = 0; x < n; x++) {
const rect = y >= 1 && y <= 5, circ = (x - 3) ** 2 + (y - 3) ** 2 <= 10, tri = Math.abs(x - 3) <= y / 2;
const c = at(x, y); cls[c] = 'm' + ((rect ? 1 : 0) | (circ ? 2 : 0) | (tri ? 4 : 0)); if (circ) pose.add(c);
dly[c] = -(((c * 37 + 11) % 17) / 17) * .32;
}
} else {
for (let y = 0; y < rows; y++) for (let x = 0; x < n; x++) { const c = at(x, y); cls[c] = 's' + c; if (y < 2) pose.add(c); }
}
return cls.map((x, c) => ({ cls: [x, pose.has(c) && 'p'].filter(Boolean).map(y => 'dm-' + y).join(' '), delay: dly[c] }));
};
const GRIDS = { orbit: grid('orbit'), morph: grid('morph'), scan: grid('scan'), stream: grid('stream') };
const Dots = ({ k }: { k: GridKind }) => (
<span className="dm-ths-dg" data-k={k}>{GRIDS[k].map((d, i) => <i key={i} className={d.cls} style={d.delay != null ? { animationDelay: d.delay.toFixed(3) + 's' } : undefined} />)}</span>
);
const Mark = ({ variant }: { variant: ThinkingVariant }) => {
switch (variant) {
case 'morph': case 'scan': case 'stream': return <Dots k={variant} />;
case 'matrix': return <span className="dm-ths-mtx">{Array.from({ length: 16 }, (_, i) => <i key={i} />)}</span>;
case 'stack': return <span className="dm-ths-stk"><i /><i /><i /></span>;
default: return <Dots k="orbit" />;
}
};
/** Six thinking indicators to pick from, each a small mark beside a status line that steps through what the agent is doing. */
export function ThinkingStates({ variant = 'orbit', steps = ['Thinking'], current, interval = 2600, seconds, className }: ThinkingStatesProps) {
const reduce = useReducedMotion();
const [auto, setAuto] = useState(0);
useEffect(() => {
if (current !== undefined || steps.length < 2) return;
const id = setInterval(() => setAuto(i => (i + 1) % steps.length), interval);
return () => clearInterval(id);
}, [current, steps.length, interval]);
const text = steps[Math.max(0, Math.min(current ?? auto, steps.length - 1))] ?? '';
const counted = useSeconds(seconds === undefined);
const secs = seconds === false ? null : seconds ?? counted;
// Stream types each new line out behind its caret; switching to Stream mid-line shows the line whole
const stream = variant === 'stream';
const [typed, setTyped] = useState(text.length);
const last = useRef(text);
useEffect(() => {
const fresh = last.current !== text; last.current = text;
if (!stream || reduce || !fresh) { setTyped(text.length); return; }
let n = 0; setTyped(0);
const id = setInterval(() => { n++; setTyped(n); if (n >= text.length) clearInterval(id); }, 32);
return () => clearInterval(id);
}, [text, stream, reduce]);
return (
<div data-dm="" data-v={variant} className={cx('dm-ths', className)}>
<div className="dm-ths-row">
<span className="dm-ths-mk" aria-hidden="true"><Mark variant={variant} /></span>
<span className="dm-ths-l" aria-hidden="true">
{stream ? <span className="dm-ths-tx">{text.slice(0, typed)}</span> : <Swap className={cx('dm-ths-tx', variant === 'orbit' && 'dm-shim')}>{text}</Swap>}
{stream && <span className="dm-ths-car" />}
</span>
{secs !== null && <span className="dm-ths-t dm-num" aria-hidden="true">{secs}s</span>}
</div>
{/* a screen reader hears each new line once, never the letters as they type */}
<span className="dm-ths-sr" role="status" aria-live="polite">{text}</span>
</div>
);
}
thinking-states.cssReact · 98 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Thinking states ---------- */
/* six ways to show that the agent is thinking, to pick from: each is a small mark on a tile beside one status line that
steps through what the agent is doing, and a screen reader hears each step once (the polite status), never each frame.
Orbit, Morph, Scan and Stream are dot grids like Matrix, each at its own density (owner, 5 Nov). Orbit: a light round the edge,
the line shimmering. Morph: a rectangle, a circle, a triangle. Scan: a column sweeping lines of dots. Stream: three lines of dots
filling one by one, then fading, the line typed out behind a caret. Matrix: a grid
of dots lighting up at random. Stack: pages shuffling to the front. Marks are the accent and everything else is grey;
with reduced motion every mark holds still in a legible pose and only the line changes */
.dm-ths{width:100%;max-width:264px;margin-inline:auto;min-height:84px;display:flex;flex-direction:column;justify-content:center;gap:var(--dm-s-3);position:relative}
.dm-ths-row{display:flex;align-items:center;gap:10px;min-width:0}
.dm-ths-mk{position:relative;width:28px;height:28px;flex:none;display:grid;place-items:center;border-radius:calc(9px * var(--dm-rk));background:var(--dm-chip);color:var(--dm-accent-mark,var(--dm-accent))}
.dm-ths-l{flex:0 1 auto;min-width:0;font-size:14px;line-height:20px;color:var(--dm-fg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dm-ths-car{display:inline-block;width:2px;height:15px;margin-left:2px;vertical-align:-3px;border-radius:1px;background:var(--dm-accent-mark,var(--dm-accent));animation:dm-thsblink 1s steps(1,end) infinite}
@keyframes dm-thsblink{50%{opacity:0}}
.dm-ths-t{flex:none;font-size:13px;color:var(--dm-muted)}
.dm-ths-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
/* Orbit, Morph, Scan and Stream are drawn on a grid of dots like Matrix, each at its own density (owner, 5 Nov):
Orbit 5 by 5, Scan 6 by 6, Morph 7 by 7 and Stream 6 by 3. The grid (.ths-dg) is written by thsGrid(); each dot's part
in the pattern is a class, and its place in the run an inline animation-delay */
.dm-ths-dg{display:grid;grid-template-columns:repeat(var(--dm-n),var(--dm-ds));gap:var(--dm-dg);--dm-ds:2px;--dm-dg:1px}
.dm-ths-dg i{display:block;width:var(--dm-ds);height:var(--dm-ds);border-radius:50%;background:currentColor;opacity:.14}
/* Orbit: a light runs round the ring with a fading tail, the dot in the middle breathing */
.dm-ths-dg[data-k="orbit"]{--dm-n:5;--dm-ds:3px;--dm-dg:1px}
.dm-ths-dg .dm-r{animation:dm-thsor 1.6s linear infinite}
.dm-ths-dg .dm-c{opacity:.85;animation:dm-thscore 1.6s var(--dm-ease-io) infinite}
@keyframes dm-thsor{0%{opacity:1}30%{opacity:.45}60%,100%{opacity:.14}}
@keyframes dm-thscore{50%{transform:scale(.7)}}
/* Scan: three lines of text in dots, a column of light sweeping across them */
.dm-ths-dg[data-k="scan"]{--dm-n:6;--dm-ds:2px;--dm-dg:1px}
.dm-ths-dg .dm-l{animation:dm-thsscl 1.6s linear infinite}
.dm-ths-dg .dm-b{opacity:.06;animation:dm-thsscb 1.6s linear infinite}
@keyframes dm-thsscl{0%{opacity:1}35%,100%{opacity:.3}}
@keyframes dm-thsscb{0%{opacity:.38}25%,100%{opacity:.06}}
/* Morph: the dots draw a rectangle, then a circle, then a triangle, and back; between shapes every dot flickers like
Matrix's, each a little out of step (its own small delay), before it settles into the next shape (owner, 5 Nov) */
.dm-ths-dg[data-k="morph"]{--dm-n:7;--dm-ds:2px;--dm-dg:1px}
.dm-ths-dg[data-k="morph"] i{animation-duration:3.2s;animation-timing-function:var(--dm-ease-io);animation-iteration-count:infinite}
.dm-ths-dg .dm-m0{animation-name:dm-thsm0}.dm-ths-dg .dm-m1{animation-name:dm-thsm1}.dm-ths-dg .dm-m2{animation-name:dm-thsm2}.dm-ths-dg .dm-m3{animation-name:dm-thsm3}.dm-ths-dg .dm-m4{animation-name:dm-thsm4}.dm-ths-dg .dm-m5{animation-name:dm-thsm5}.dm-ths-dg .dm-m6{animation-name:dm-thsm6}.dm-ths-dg .dm-m7{animation-name:dm-thsm7}
@keyframes dm-thsm0{0%{opacity:.12}20%{opacity:.12}22.6%{opacity:.5}25.2%{opacity:.12}27.8%{opacity:.4}30.4%{opacity:.12}33%{opacity:.12}53%{opacity:.12}55.6%{opacity:.5}58.2%{opacity:.12}60.8%{opacity:.4}63.4%{opacity:.12}66%{opacity:.12}86%{opacity:.12}88.8%{opacity:.5}91.6%{opacity:.12}94.4%{opacity:.4}97.2%{opacity:.12}100%{opacity:.12}}
@keyframes dm-thsm1{0%{opacity:1}20%{opacity:1}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:.12}53%{opacity:.12}55.6%{opacity:.5}58.2%{opacity:.12}60.8%{opacity:.4}63.4%{opacity:.12}66%{opacity:.12}86%{opacity:.12}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:1}}
@keyframes dm-thsm2{0%{opacity:.12}20%{opacity:.12}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:1}53%{opacity:1}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:.12}86%{opacity:.12}88.8%{opacity:.5}91.6%{opacity:.12}94.4%{opacity:.4}97.2%{opacity:.12}100%{opacity:.12}}
@keyframes dm-thsm3{0%{opacity:1}20%{opacity:1}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:1}53%{opacity:1}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:.12}86%{opacity:.12}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:1}}
@keyframes dm-thsm4{0%{opacity:.12}20%{opacity:.12}22.6%{opacity:.5}25.2%{opacity:.12}27.8%{opacity:.4}30.4%{opacity:.12}33%{opacity:.12}53%{opacity:.12}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:1}86%{opacity:1}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:.12}}
@keyframes dm-thsm5{0%{opacity:1}20%{opacity:1}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:.12}53%{opacity:.12}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:1}86%{opacity:1}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:1}}
@keyframes dm-thsm6{0%{opacity:.12}20%{opacity:.12}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:1}53%{opacity:1}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:1}86%{opacity:1}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:.12}}
@keyframes dm-thsm7{0%{opacity:1}20%{opacity:1}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:1}53%{opacity:1}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:1}86%{opacity:1}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:1}}
/* Stream: three full lines of dots fill the tile evenly (owner, 5 Nov); they light up one by one, line by line, hold, then
all fade out together. Each dot's moment is its own keyframe (thss0 to thss17, in reading order) */
.dm-ths-dg[data-k="stream"]{--dm-n:6;--dm-ds:3px;--dm-dg:1px;row-gap:6px}
.dm-ths-dg[data-k="stream"] i{animation-duration:2.4s;animation-timing-function:linear;animation-iteration-count:infinite}
.dm-ths-dg .dm-s0{animation-name:dm-thss0}.dm-ths-dg .dm-s1{animation-name:dm-thss1}.dm-ths-dg .dm-s2{animation-name:dm-thss2}.dm-ths-dg .dm-s3{animation-name:dm-thss3}.dm-ths-dg .dm-s4{animation-name:dm-thss4}.dm-ths-dg .dm-s5{animation-name:dm-thss5}.dm-ths-dg .dm-s6{animation-name:dm-thss6}.dm-ths-dg .dm-s7{animation-name:dm-thss7}.dm-ths-dg .dm-s8{animation-name:dm-thss8}.dm-ths-dg .dm-s9{animation-name:dm-thss9}.dm-ths-dg .dm-s10{animation-name:dm-thss10}.dm-ths-dg .dm-s11{animation-name:dm-thss11}.dm-ths-dg .dm-s12{animation-name:dm-thss12}.dm-ths-dg .dm-s13{animation-name:dm-thss13}.dm-ths-dg .dm-s14{animation-name:dm-thss14}.dm-ths-dg .dm-s15{animation-name:dm-thss15}.dm-ths-dg .dm-s16{animation-name:dm-thss16}.dm-ths-dg .dm-s17{animation-name:dm-thss17}
@keyframes dm-thss0{0%,4.0%{opacity:.14}6.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss1{0%,7.7%{opacity:.14}10.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss2{0%,11.3%{opacity:.14}13.9%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss3{0%,15.0%{opacity:.14}17.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss4{0%,18.7%{opacity:.14}21.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss5{0%,22.3%{opacity:.14}24.9%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss6{0%,26.0%{opacity:.14}28.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss7{0%,29.7%{opacity:.14}32.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss8{0%,33.3%{opacity:.14}35.9%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss9{0%,37.0%{opacity:.14}39.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss10{0%,40.7%{opacity:.14}43.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss11{0%,44.3%{opacity:.14}46.9%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss12{0%,48.0%{opacity:.14}50.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss13{0%,51.7%{opacity:.14}54.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss14{0%,55.3%{opacity:.14}57.9%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss15{0%,59.0%{opacity:.14}61.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss16{0%,62.7%{opacity:.14}65.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes dm-thss17{0%,66.3%{opacity:.14}68.9%,82%{opacity:1}94%,100%{opacity:.14}}
/* Matrix: each dot keeps its own beat, so the grid twinkles instead of sweeping */
.dm-ths-mtx{display:grid;grid-template-columns:repeat(4,3px);gap:2px}
.dm-ths-mtx i{display:block;width:3px;height:3px;border-radius:50%;background:currentColor;opacity:.2;animation:dm-thstw 2s var(--dm-ease-io) infinite}
.dm-ths-mtx i:nth-child(3n){animation-duration:2.6s}.dm-ths-mtx i:nth-child(3n+1){animation-duration:1.7s}
.dm-ths-mtx i:nth-child(1){animation-delay:-.1s}.dm-ths-mtx i:nth-child(2){animation-delay:-.9s}.dm-ths-mtx i:nth-child(3){animation-delay:-1.9s}.dm-ths-mtx i:nth-child(4){animation-delay:-.4s}
.dm-ths-mtx i:nth-child(5){animation-delay:-1.2s}.dm-ths-mtx i:nth-child(6){animation-delay:-.2s}.dm-ths-mtx i:nth-child(7){animation-delay:-1.6s}.dm-ths-mtx i:nth-child(8){animation-delay:-.7s}
.dm-ths-mtx i:nth-child(9){animation-delay:-.5s}.dm-ths-mtx i:nth-child(10){animation-delay:-1.4s}.dm-ths-mtx i:nth-child(11){animation-delay:-.05s}.dm-ths-mtx i:nth-child(12){animation-delay:-1s}
.dm-ths-mtx i:nth-child(13){animation-delay:-2.1s}.dm-ths-mtx i:nth-child(14){animation-delay:-.8s}.dm-ths-mtx i:nth-child(15){animation-delay:-1.3s}.dm-ths-mtx i:nth-child(16){animation-delay:-.3s}
@keyframes dm-thstw{0%,100%{opacity:.2}45%,60%{opacity:1}}
/* Stack: three pages; the front one leaves and the others move up, each a third of the loop behind the next */
.dm-ths-stk{position:relative;width:20px;height:20px}
.dm-ths-stk i{position:absolute;left:3px;top:3px;width:11px;height:13px;border-radius:calc(2.5px * var(--dm-rk));background:var(--dm-surface);box-shadow:inset 0 0 0 1.25px currentColor;color:color-mix(in srgb,var(--dm-fg) 34%,transparent);animation:dm-thsstk 2.4s var(--dm-ease-io) infinite}
.dm-ths-stk i:nth-child(1){translate:6px -3px;opacity:.6;z-index:1}
.dm-ths-stk i:nth-child(2){translate:3px 0;opacity:.85;z-index:2;animation-delay:-.8s}
.dm-ths-stk i:nth-child(3){translate:0 3px;z-index:3;color:var(--dm-accent-mark,var(--dm-accent));animation-delay:-1.6s}
@keyframes dm-thsstk{0%{translate:6px -3px;opacity:.6;z-index:1;color:color-mix(in srgb,var(--dm-fg) 34%,transparent)}25%{translate:3px 0;opacity:.85;z-index:2;color:color-mix(in srgb,var(--dm-fg) 34%,transparent)}50%,66%{translate:0 3px;opacity:1;z-index:3;color:var(--dm-accent-mark,var(--dm-accent))}80%{translate:-5px 6px;opacity:0;z-index:3;color:var(--dm-accent-mark,var(--dm-accent))}81%{translate:6px -3px;opacity:0;z-index:1;color:color-mix(in srgb,var(--dm-fg) 34%,transparent)}100%{translate:6px -3px;opacity:.6;z-index:1;color:color-mix(in srgb,var(--dm-fg) 34%,transparent)}}
/* six choices fit under the demo on a phone */
@container (max-width:380px){.dm-ths-dm>.dm-demo .dm-seg2 button{padding:0 7px;font-size:12px}}
/* reduced motion: every mark holds a pose that still reads (the orbit's light and tail, a circle, a lit column, the first
words, a lit diamond, a stack), the caret stays lit and the sweep goes */
@media (prefers-reduced-motion:reduce){.dm-ths-mk *,.dm-ths-car{animation:none!important}.dm-ths-dg .dm-p{opacity:1}.dm-ths-mtx i:is(:nth-child(2),:nth-child(5),:nth-child(7),:nth-child(10),:nth-child(12),:nth-child(15)){opacity:1}}
/* Blueprint: a square tile with a hairline, square dots, marks that tick in steps and a block caret */
:root:not([data-style="soft"]) .dm-ths-mk:not(:where([data-style="soft"],[data-style="soft"] *)){background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-rail) 50%,transparent)}
:root:not([data-style="soft"]) .dm-ths-dg i:not(:where([data-style="soft"],[data-style="soft"] *)){border-radius:0}
:root:not([data-style="soft"]) .dm-ths-car:not(:where([data-style="soft"],[data-style="soft"] *)){width:7px;vertical-align:-3px}
:root:not([data-style="soft"]) .dm-ths-mtx i:not(:where([data-style="soft"],[data-style="soft"] *)){animation-timing-function:steps(1,end)}
:root:not([data-style="soft"]) .dm-ths-stk i:not(:where([data-style="soft"],[data-style="soft"] *)){animation-timing-function:steps(3)}
ThinkingStates.astroAstro · 35 lines
---
// Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync.
// Thinking states: Six thinking indicators to pick from, each with a status line.
// The markup, CSS and script are designme.dev's own, run by the shared core in ../core, which mounts the component when it scrolls
// into view and plays its demo. To show your own data, change thinking-states.js. The markup is kept exactly as it is (is:raw).
import type { HTMLAttributes } from 'astro/types';
import '../core/designme.css';
import './thinking-states.css';
export interface Props extends HTMLAttributes<'div'> {
/** "soft" draws it in the Soft style. Leave it out for Blueprint, or for the style set on <html> (data-style="soft") */
'data-style'?: 'soft';
}
const { class: className, ...rest } = Astro.props;
---
<div class:list={['card', 'wide', 'bare', className]} data-c="thinking-states" {...rest}>
<div class="stage-in"><Fragment is:raw>
<div class="ths-dm dm"><div class="dm-main">
<div class="ths" data-v="orbit">
<div class="ths-row">
<span class="ths-mk" aria-hidden="true"></span>
<span class="ths-l" aria-hidden="true"><span class="ths-tx shim">Thinking</span><span class="ths-car" hidden></span></span>
<span class="ths-t num" aria-hidden="true">0s</span>
</div>
<span class="ths-sr" role="status" aria-live="polite">Thinking</span>
</div>
</div>
<div class="demo"><div class="seg2" role="group" aria-label="Thinking state"><button aria-pressed="true" data-v="orbit">Orbit</button><button aria-pressed="false" data-v="morph">Morph</button><button aria-pressed="false" data-v="scan">Scan</button><button aria-pressed="false" data-v="stream">Stream</button><button aria-pressed="false" data-v="matrix">Matrix</button><button aria-pressed="false" data-v="stack">Stack</button></div></div>
</div>
</Fragment></div>
</div>
<script>
import './thinking-states.js';
</script>
thinking-states.htmlHTML · 17 lines
<div class="card wide bare" data-c="thinking-states">
<div class="stage-in">
<div class="ths-dm dm"><div class="dm-main">
<div class="ths" data-v="orbit">
<div class="ths-row">
<span class="ths-mk" aria-hidden="true"></span>
<span class="ths-l" aria-hidden="true"><span class="ths-tx shim">Thinking</span><span class="ths-car" hidden></span></span>
<span class="ths-t num" aria-hidden="true">0s</span>
</div>
<span class="ths-sr" role="status" aria-live="polite">Thinking</span>
</div>
</div>
<div class="demo"><div class="seg2" role="group" aria-label="Thinking state"><button aria-pressed="true" data-v="orbit">Orbit</button><button aria-pressed="false" data-v="morph">Morph</button><button aria-pressed="false" data-v="scan">Scan</button><button aria-pressed="false" data-v="stream">Stream</button><button aria-pressed="false" data-v="matrix">Matrix</button><button aria-pressed="false" data-v="stack">Stack</button></div></div>
</div>
</div>
</div>
thinking-states.cssHTML · 97 lines
/* ---------- Thinking states ---------- */
/* six ways to show that the agent is thinking, to pick from: each is a small mark on a tile beside one status line that
steps through what the agent is doing, and a screen reader hears each step once (the polite status), never each frame.
Orbit, Morph, Scan and Stream are dot grids like Matrix, each at its own density (owner, 5 Nov). Orbit: a light round the edge,
the line shimmering. Morph: a rectangle, a circle, a triangle. Scan: a column sweeping lines of dots. Stream: three lines of dots
filling one by one, then fading, the line typed out behind a caret. Matrix: a grid
of dots lighting up at random. Stack: pages shuffling to the front. Marks are the accent and everything else is grey;
with reduced motion every mark holds still in a legible pose and only the line changes */
.ths{width:100%;max-width:264px;margin-inline:auto;min-height:84px;display:flex;flex-direction:column;justify-content:center;gap:var(--dm-s-3);position:relative}
.ths-row{display:flex;align-items:center;gap:10px;min-width:0}
.ths-mk{position:relative;width:28px;height:28px;flex:none;display:grid;place-items:center;border-radius:calc(9px * var(--dm-rk));background:var(--dm-chip);color:var(--dm-accent-mark,var(--dm-accent))}
.ths-l{flex:0 1 auto;min-width:0;font-size:14px;line-height:20px;color:var(--dm-fg);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ths-car{display:inline-block;width:2px;height:15px;margin-left:2px;vertical-align:-3px;border-radius:1px;background:var(--dm-accent-mark,var(--dm-accent));animation:thsblink 1s steps(1,end) infinite}
@keyframes thsblink{50%{opacity:0}}
.ths-t{flex:none;font-size:13px;color:var(--dm-muted)}
.ths-sr{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
/* Orbit, Morph, Scan and Stream are drawn on a grid of dots like Matrix, each at its own density (owner, 5 Nov):
Orbit 5 by 5, Scan 6 by 6, Morph 7 by 7 and Stream 6 by 3. The grid (.ths-dg) is written by thsGrid(); each dot's part
in the pattern is a class, and its place in the run an inline animation-delay */
.ths-dg{display:grid;grid-template-columns:repeat(var(--dm-n),var(--dm-ds));gap:var(--dm-dg);--dm-ds:2px;--dm-dg:1px}
.ths-dg i{display:block;width:var(--dm-ds);height:var(--dm-ds);border-radius:50%;background:currentColor;opacity:.14}
/* Orbit: a light runs round the ring with a fading tail, the dot in the middle breathing */
.ths-dg[data-k="orbit"]{--dm-n:5;--dm-ds:3px;--dm-dg:1px}
.ths-dg .r{animation:thsor 1.6s linear infinite}
.ths-dg .c{opacity:.85;animation:thscore 1.6s var(--dm-ease-io) infinite}
@keyframes thsor{0%{opacity:1}30%{opacity:.45}60%,100%{opacity:.14}}
@keyframes thscore{50%{transform:scale(.7)}}
/* Scan: three lines of text in dots, a column of light sweeping across them */
.ths-dg[data-k="scan"]{--dm-n:6;--dm-ds:2px;--dm-dg:1px}
.ths-dg .l{animation:thsscl 1.6s linear infinite}
.ths-dg .b{opacity:.06;animation:thsscb 1.6s linear infinite}
@keyframes thsscl{0%{opacity:1}35%,100%{opacity:.3}}
@keyframes thsscb{0%{opacity:.38}25%,100%{opacity:.06}}
/* Morph: the dots draw a rectangle, then a circle, then a triangle, and back; between shapes every dot flickers like
Matrix's, each a little out of step (its own small delay), before it settles into the next shape (owner, 5 Nov) */
.ths-dg[data-k="morph"]{--dm-n:7;--dm-ds:2px;--dm-dg:1px}
.ths-dg[data-k="morph"] i{animation-duration:3.2s;animation-timing-function:var(--dm-ease-io);animation-iteration-count:infinite}
.ths-dg .m0{animation-name:thsm0}.ths-dg .m1{animation-name:thsm1}.ths-dg .m2{animation-name:thsm2}.ths-dg .m3{animation-name:thsm3}.ths-dg .m4{animation-name:thsm4}.ths-dg .m5{animation-name:thsm5}.ths-dg .m6{animation-name:thsm6}.ths-dg .m7{animation-name:thsm7}
@keyframes thsm0{0%{opacity:.12}20%{opacity:.12}22.6%{opacity:.5}25.2%{opacity:.12}27.8%{opacity:.4}30.4%{opacity:.12}33%{opacity:.12}53%{opacity:.12}55.6%{opacity:.5}58.2%{opacity:.12}60.8%{opacity:.4}63.4%{opacity:.12}66%{opacity:.12}86%{opacity:.12}88.8%{opacity:.5}91.6%{opacity:.12}94.4%{opacity:.4}97.2%{opacity:.12}100%{opacity:.12}}
@keyframes thsm1{0%{opacity:1}20%{opacity:1}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:.12}53%{opacity:.12}55.6%{opacity:.5}58.2%{opacity:.12}60.8%{opacity:.4}63.4%{opacity:.12}66%{opacity:.12}86%{opacity:.12}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:1}}
@keyframes thsm2{0%{opacity:.12}20%{opacity:.12}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:1}53%{opacity:1}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:.12}86%{opacity:.12}88.8%{opacity:.5}91.6%{opacity:.12}94.4%{opacity:.4}97.2%{opacity:.12}100%{opacity:.12}}
@keyframes thsm3{0%{opacity:1}20%{opacity:1}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:1}53%{opacity:1}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:.12}86%{opacity:.12}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:1}}
@keyframes thsm4{0%{opacity:.12}20%{opacity:.12}22.6%{opacity:.5}25.2%{opacity:.12}27.8%{opacity:.4}30.4%{opacity:.12}33%{opacity:.12}53%{opacity:.12}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:1}86%{opacity:1}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:.12}}
@keyframes thsm5{0%{opacity:1}20%{opacity:1}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:.12}53%{opacity:.12}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:1}86%{opacity:1}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:1}}
@keyframes thsm6{0%{opacity:.12}20%{opacity:.12}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:1}53%{opacity:1}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:1}86%{opacity:1}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:.12}}
@keyframes thsm7{0%{opacity:1}20%{opacity:1}22.6%{opacity:.85}25.2%{opacity:.2}27.8%{opacity:.95}30.4%{opacity:.3}33%{opacity:1}53%{opacity:1}55.6%{opacity:.85}58.2%{opacity:.2}60.8%{opacity:.95}63.4%{opacity:.3}66%{opacity:1}86%{opacity:1}88.8%{opacity:.85}91.6%{opacity:.2}94.4%{opacity:.95}97.2%{opacity:.3}100%{opacity:1}}
/* Stream: three full lines of dots fill the tile evenly (owner, 5 Nov); they light up one by one, line by line, hold, then
all fade out together. Each dot's moment is its own keyframe (thss0 to thss17, in reading order) */
.ths-dg[data-k="stream"]{--dm-n:6;--dm-ds:3px;--dm-dg:1px;row-gap:6px}
.ths-dg[data-k="stream"] i{animation-duration:2.4s;animation-timing-function:linear;animation-iteration-count:infinite}
.ths-dg .s0{animation-name:thss0}.ths-dg .s1{animation-name:thss1}.ths-dg .s2{animation-name:thss2}.ths-dg .s3{animation-name:thss3}.ths-dg .s4{animation-name:thss4}.ths-dg .s5{animation-name:thss5}.ths-dg .s6{animation-name:thss6}.ths-dg .s7{animation-name:thss7}.ths-dg .s8{animation-name:thss8}.ths-dg .s9{animation-name:thss9}.ths-dg .s10{animation-name:thss10}.ths-dg .s11{animation-name:thss11}.ths-dg .s12{animation-name:thss12}.ths-dg .s13{animation-name:thss13}.ths-dg .s14{animation-name:thss14}.ths-dg .s15{animation-name:thss15}.ths-dg .s16{animation-name:thss16}.ths-dg .s17{animation-name:thss17}
@keyframes thss0{0%,4.0%{opacity:.14}6.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss1{0%,7.7%{opacity:.14}10.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss2{0%,11.3%{opacity:.14}13.9%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss3{0%,15.0%{opacity:.14}17.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss4{0%,18.7%{opacity:.14}21.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss5{0%,22.3%{opacity:.14}24.9%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss6{0%,26.0%{opacity:.14}28.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss7{0%,29.7%{opacity:.14}32.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss8{0%,33.3%{opacity:.14}35.9%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss9{0%,37.0%{opacity:.14}39.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss10{0%,40.7%{opacity:.14}43.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss11{0%,44.3%{opacity:.14}46.9%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss12{0%,48.0%{opacity:.14}50.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss13{0%,51.7%{opacity:.14}54.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss14{0%,55.3%{opacity:.14}57.9%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss15{0%,59.0%{opacity:.14}61.6%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss16{0%,62.7%{opacity:.14}65.3%,82%{opacity:1}94%,100%{opacity:.14}}
@keyframes thss17{0%,66.3%{opacity:.14}68.9%,82%{opacity:1}94%,100%{opacity:.14}}
/* Matrix: each dot keeps its own beat, so the grid twinkles instead of sweeping */
.ths-mtx{display:grid;grid-template-columns:repeat(4,3px);gap:2px}
.ths-mtx i{display:block;width:3px;height:3px;border-radius:50%;background:currentColor;opacity:.2;animation:thstw 2s var(--dm-ease-io) infinite}
.ths-mtx i:nth-child(3n){animation-duration:2.6s}.ths-mtx i:nth-child(3n+1){animation-duration:1.7s}
.ths-mtx i:nth-child(1){animation-delay:-.1s}.ths-mtx i:nth-child(2){animation-delay:-.9s}.ths-mtx i:nth-child(3){animation-delay:-1.9s}.ths-mtx i:nth-child(4){animation-delay:-.4s}
.ths-mtx i:nth-child(5){animation-delay:-1.2s}.ths-mtx i:nth-child(6){animation-delay:-.2s}.ths-mtx i:nth-child(7){animation-delay:-1.6s}.ths-mtx i:nth-child(8){animation-delay:-.7s}
.ths-mtx i:nth-child(9){animation-delay:-.5s}.ths-mtx i:nth-child(10){animation-delay:-1.4s}.ths-mtx i:nth-child(11){animation-delay:-.05s}.ths-mtx i:nth-child(12){animation-delay:-1s}
.ths-mtx i:nth-child(13){animation-delay:-2.1s}.ths-mtx i:nth-child(14){animation-delay:-.8s}.ths-mtx i:nth-child(15){animation-delay:-1.3s}.ths-mtx i:nth-child(16){animation-delay:-.3s}
@keyframes thstw{0%,100%{opacity:.2}45%,60%{opacity:1}}
/* Stack: three pages; the front one leaves and the others move up, each a third of the loop behind the next */
.ths-stk{position:relative;width:20px;height:20px}
.ths-stk i{position:absolute;left:3px;top:3px;width:11px;height:13px;border-radius:calc(2.5px * var(--dm-rk));background:var(--dm-surface);box-shadow:inset 0 0 0 1.25px currentColor;color:color-mix(in srgb,var(--dm-fg) 34%,transparent);animation:thsstk 2.4s var(--dm-ease-io) infinite}
.ths-stk i:nth-child(1){translate:6px -3px;opacity:.6;z-index:1}
.ths-stk i:nth-child(2){translate:3px 0;opacity:.85;z-index:2;animation-delay:-.8s}
.ths-stk i:nth-child(3){translate:0 3px;z-index:3;color:var(--dm-accent-mark,var(--dm-accent));animation-delay:-1.6s}
@keyframes thsstk{0%{translate:6px -3px;opacity:.6;z-index:1;color:color-mix(in srgb,var(--dm-fg) 34%,transparent)}25%{translate:3px 0;opacity:.85;z-index:2;color:color-mix(in srgb,var(--dm-fg) 34%,transparent)}50%,66%{translate:0 3px;opacity:1;z-index:3;color:var(--dm-accent-mark,var(--dm-accent))}80%{translate:-5px 6px;opacity:0;z-index:3;color:var(--dm-accent-mark,var(--dm-accent))}81%{translate:6px -3px;opacity:0;z-index:1;color:color-mix(in srgb,var(--dm-fg) 34%,transparent)}100%{translate:6px -3px;opacity:.6;z-index:1;color:color-mix(in srgb,var(--dm-fg) 34%,transparent)}}
/* six choices fit under the demo on a phone */
@container (max-width:380px){.ths-dm>.demo .seg2 button{padding:0 7px;font-size:12px}}
/* reduced motion: every mark holds a pose that still reads (the orbit's light and tail, a circle, a lit column, the first
words, a lit diamond, a stack), the caret stays lit and the sweep goes */
@media (prefers-reduced-motion:reduce){.ths-mk *,.ths-car{animation:none!important}.ths-dg .p{opacity:1}.ths-mtx i:is(:nth-child(2),:nth-child(5),:nth-child(7),:nth-child(10),:nth-child(12),:nth-child(15)){opacity:1}}
/* Blueprint: a square tile with a hairline, square dots, marks that tick in steps and a block caret */
:root:not([data-style="soft"]) .ths-mk:not(:where([data-style="soft"],[data-style="soft"] *)){background:var(--dm-surface);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--dm-rail) 50%,transparent)}
:root:not([data-style="soft"]) .ths-dg i:not(:where([data-style="soft"],[data-style="soft"] *)){border-radius:0}
:root:not([data-style="soft"]) .ths-car:not(:where([data-style="soft"],[data-style="soft"] *)){width:7px;vertical-align:-3px}
:root:not([data-style="soft"]) .ths-mtx i:not(:where([data-style="soft"],[data-style="soft"] *)){animation-timing-function:steps(1,end)}
:root:not([data-style="soft"]) .ths-stk i:not(:where([data-style="soft"],[data-style="soft"] *)){animation-timing-function:steps(3)}
thinking-states.jsHTML · 68 lines
DM.def('thinking-states', (r, c) => {
const steps = ['Thinking', 'Reading 3 files', 'Searching the docs', 'Comparing 2 options', 'Drafting the answer'];
const box = $('.ths', r), mk = $('.ths-mk', r), tx = $('.ths-tx', r), car = $('.ths-car', r), time = $('.ths-t', r), sr = $('.ths-sr', r);
// Orbit, Morph, Scan and Stream as a grid of dots, each at its own density: which part each dot plays, and when
const grid = k => {
const n = { orbit:5, scan:6, morph:7, stream:6 }[k], rows = k === 'stream' ? 3 : n, at = (x, y) => y * n + x;
const cls = Array(n * rows).fill(''), dly = Array(n * rows).fill(null), pose = new Set();
const run = (order, dur) => order.forEach((c, i) => { dly[c] = ((i / order.length) - 1) * dur; });
if (k === 'orbit'){
const ring = [];
for (let x = 0; x < n; x++) ring.push(at(x, 0));
for (let y = 1; y < n; y++) ring.push(at(n - 1, y));
for (let x = n - 2; x >= 0; x--) ring.push(at(x, n - 1));
for (let y = n - 2; y > 0; y--) ring.push(at(0, y));
ring.forEach(c => { cls[c] = 'r'; }); run(ring, 1.6); cls[at(2, 2)] = 'c';
[0, ring.length - 1, ring.length - 2].forEach(i => pose.add(ring[i]));
} else if (k === 'scan'){
const len = [6, 0, 5, 0, 3, 0];
for (let y = 0; y < n; y++) for (let x = 0; x < n; x++){ const c = at(x, y); cls[c] = x < len[y] ? 'l' : 'b'; dly[c] = (x / n - 1) * 1.6; if (x === 2) pose.add(c); }
} else if (k === 'morph'){
for (let y = 0; y < n; y++) for (let x = 0; x < n; x++){
const rect = y >= 1 && y <= 5, circ = (x - 3) ** 2 + (y - 3) ** 2 <= 10, tri = Math.abs(x - 3) <= y / 2;
const c = at(x, y); cls[c] = 'm' + ((rect ? 1 : 0) | (circ ? 2 : 0) | (tri ? 4 : 0)); if (circ) pose.add(c);
dly[c] = -(((c * 37 + 11) % 17) / 17) * .32;
}
} else {
for (let y = 0; y < rows; y++) for (let x = 0; x < n; x++){ const c = at(x, y); cls[c] = 's' + c; if (y < 2) pose.add(c); }
}
return `<span class="ths-dg" data-k="${k}">` + cls.map((x, c) => `<i class="${x}${pose.has(c) ? ' p' : ''}"${dly[c] != null ? ` style="animation-delay:${dly[c].toFixed(3)}s"` : ''}></i>`).join('') + '</span>';
};
// the mark each state draws on its tile
const MARKS = {
orbit:grid('orbit'),
morph:grid('morph'),
scan:grid('scan'),
stream:grid('stream'),
matrix:'<span class="ths-mtx">' + '<i></i>'.repeat(16) + '</span>',
stack:'<span class="ths-stk"><i></i><i></i><i></i></span>'
};
let v = box.dataset.v || 'orbit', at = 0, secs = 0, typed = 0;
// the agent's own pace: these waits are not shortened under reduced motion, so the line still changes every few seconds
const pause = ms => new Promise(res => c.later(ms, res));
// Stream types the line out behind its caret; the others crossfade it (Orbit shimmering). A newer line cancels the typing
const type = async t => {
const k = ++typed; if (tx._sw){ clearTimeout(tx._sw.t); tx._sw = null; } tx.classList.remove('out'); tx.textContent = '';
for (let i = 1; i <= t.length; i++){ await pause(32); if (k !== typed) return; tx.textContent = t.slice(0, i); }
};
const show = (t, fresh) => {
tx.classList.toggle('shim', v === 'orbit'); car.hidden = v !== 'stream';
if (v === 'stream' && fresh && !REDUCE) type(t);
else { typed++; if (fresh) swap(tx, t); else { if (tx._sw){ clearTimeout(tx._sw.t); tx._sw = null; } tx.classList.remove('out'); tx.textContent = t; } }
// a screen reader hears each new step once, never the letters as they type
if (fresh) sr.textContent = t;
};
const pick = x => { v = x; box.dataset.v = x; mk.innerHTML = MARKS[x]; show(steps[at], false); };
seg($('.seg2', r), pick);
pick(v);
c.every(1000, () => { secs++; time.textContent = secs + 's'; });
let first = true;
c.run(async () => {
for (;;){
for (at = 0; at < steps.length; at++){ show(steps[at], !first); first = false; await pause(2600); }
// a fresh question: the clock starts again
at = 0; secs = 0; time.textContent = '0s';
}
});
});
Questions
What people ask about it.
- How do I show the step my agent is on?
- Pass your steps and set
currentto the index of the step the agent reports. Withoutcurrent, the lines take turns by themselves everyintervalmilliseconds. - Which variant should I use?
- Orbit and Morph are calm and work anywhere. Scan and Stream suit answers that will be text. Matrix reads as busy computing, and Stack as reading files or sources.
- Can I hide the timer?
- Yes. Pass
seconds={false}. Pass a number to show your own time instead of the count the component keeps.
More in Agent status
Show what the agent is doing, how far along it is, and what it is working with.