Skip to content
Star on GitHub
Agent statusThinking states
Style
Accent
Sound

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 call

Thinking 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.

$npx designme add thinking-states
Thinking
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add thinking-states

React

$npx designme add thinking-states --react

Astro

$npx designme add thinking-states --astro

The 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.

02

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.
03

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" />
04

Props

Everything ThinkingStates takes.

PropTypeWhat it does
variantThinkingVariantWhich 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
stepsstring[]The status lines, like "Reading 3 files". With more than one, they take turns every `interval` unless `current` is set
currentnumberIndex of the line to show. Leave it out to let the lines take turns by themselves
intervalnumberMilliseconds each line shows for when they take turns by themselves
secondsnumber | falseSeconds shown at the end of the line. Counts by itself when left out; false hides it
classNamestring
05

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';
    }
  });
});
06

Questions

What people ask about it.

How do I show the step my agent is on?
Pass your steps and set current to the index of the step the agent reports. Without current, the lines take turns by themselves every interval milliseconds.
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.
07

More in Agent status

Show what the agent is doing, how far along it is, and what it is working with.

ThinkingThe live trace, written as the agent works, then folded away.
Reasoning trailThe agent's plan, notes, sources and edits, after it thinks.
Agent lanesSeveral agents working in parallel, each with its own lane and hand-offs.
Agent treeAn orchestrator and its sub-agents, with cost rolled up per branch.
Agent inboxEvery run that needs you, is running or is done, in one list.
Trace viewOne agent run as a waterfall of spans, with latency and tokens.
Browser sessionThe agent browsing for you, step by step, with Pause and Take over.
Web searchThe agent's searches as a trail: what it asked, what came back, what it read or skipped, and the answer with its sources.
All 21 in Agent status →