Skip to content
Star on GitHub
Agent statusStep loader
Style
Accent
Sound

Built by DesignMe, free to use under the MIT licence.

Expert design and dev support for startups and enterprises.

Book a call

Step loader

Named steps with a progress track and four loaders. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add step-loader
    01

    Install

    Add it to your project with the CLI.

    HTML, CSS and JavaScript

    $npx designme add step-loader

    React

    $npx designme add step-loader --react

    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.

    02

    About

    What it is for and what it does.

    Step loader shows an agent task as a short list of named steps, with the current step's label, a count like 2/4 and a segmented progress track. Click the row to open the full list and see which steps are done and which are still to come.

    • When an agent runs a known sequence, like reading frames, matching tokens and writing components.
    • When people want one compact line by default, with the full list a click away.
    • When finished steps should report what they did, such as "Read 4 frames" instead of "Reading Figma frames".
    • When the end of a run should hand off with a clear label like "Ready for review".
    03

    Usage

    Import it and drive it from your own state.

    Import

    import { StepLoader } from '@/components/designme/step-loader/StepLoader';

    Example

    The demo above as a React component, 30 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 { StepLoader, type StepLoaderGlyph } from './StepLoader';
    import { Seg } from '../lib/motion';
    import { Demo } from '../lib/Demo';
    import { useScript } from '../lib/useScript';
    
    /* ---------- Step loader: four named steps, looping ---------- */
    const STEPS = [
      { label: 'Reading Figma frames', done: 'Read 4 frames' }, { label: 'Matching tokens', done: 'Matched 18 tokens' },
      { label: 'Writing components', done: 'Wrote 3 components' }, { label: 'Checking contrast', done: 'Contrast passes' }
    ];
    export default function StepLoaderDemo() {
      const [loader, setLoader] = useState<StepLoaderGlyph>('radar');
      const [current, setCurrent] = useState(0);
      useScript(async ({ wait }) => {
        for (;;) {
          for (let i = 0; i < STEPS.length; i++) { setCurrent(i); await wait(1900); }
          setCurrent(STEPS.length); await wait(3200);
          setCurrent(0); await wait(700);
        }
      });
      return (
        <Demo switcher={<Seg label="Loader" value={loader} onChange={setLoader} options={[{ value: 'radar', label: 'Radar' }, { value: 'dots', label: 'Dots' }, { value: 'ring', label: 'Ring' }, { value: 'bars', label: 'Bars' }]} />}>
          <StepLoader steps={STEPS} current={current} loader={loader} />
        </Demo>
      );
    }
    
    04

    Props

    Everything StepLoader takes.

    PropTypeWhat it does
    stepsrequiredStep[]
    currentrequirednumberIndex of the step running now. steps.length (or more) means every step is done
    loaderStepLoaderGlyphThe loader beside the running step
    doneLabelstringLabel once every step is done
    openbooleanWhether the list of every step is open. Leave it out to let the row toggle it
    defaultOpenboolean
    onOpenChange(open: boolean) => void
    classNamestring
    05

    Code

    The full source, as the CLI installs it.

    StepLoader.tsxReact · 67 lines
    'use client';
    import { cx } from '../lib/cx';
    import { useControllable } from '../lib/hooks';
    import { Icon } from '../lib/icons';
    import { Bars, Dot, Fold, Radar, Spin, Swap, Think } from '../lib/motion';
    import '../designme.css';
    import './step-loader.css';
    
    export interface Step {
      /** Shown while the step runs, like "Reading Figma frames" */
      label: string;
      /** Shown once it is done, like "Read 4 frames". Falls back to label */
      done?: string;
    }
    
    export type StepLoaderGlyph = 'radar' | 'dots' | 'ring' | 'bars';
    
    export interface StepLoaderProps {
      steps: Step[];
      /** Index of the step running now. steps.length (or more) means every step is done */
      current: number;
      /** The loader beside the running step */
      loader?: StepLoaderGlyph;
      /** Label once every step is done */
      doneLabel?: string;
      /** Whether the list of every step is open. Leave it out to let the row toggle it */
      open?: boolean;
      defaultOpen?: boolean;
      onOpenChange?: (open: boolean) => void;
      className?: string;
    }
    
    const Loader = ({ kind }: { kind: StepLoaderGlyph }) =>
      kind === 'dots' ? <Think /> : kind === 'ring' ? <Spin /> : kind === 'bars' ? <Bars /> : <Radar />;
    
    /** Named steps with a progress track. Click the row to see every step. */
    export function StepLoader({ steps, current, loader = 'radar', doneLabel = 'Ready for review', open, defaultOpen = false, onOpenChange, className }: StepLoaderProps) {
      const [isOpen, setOpen] = useControllable(open, defaultOpen, onOpenChange);
      const n = steps.length, done = current >= n, at = Math.min(current, n - 1);
      return (
        <div data-dm="" className={cx('dm-stl', className)} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
          <button type="button" className="dm-stl-row" aria-expanded={isOpen} onClick={() => setOpen(!isOpen)}>
            <span className="dm-stl-g">{done ? <span style={{ color: 'var(--dm-ok)', display: 'grid' }}><Icon name="ok" /></span> : <Loader kind={loader} />}</span>
            <span className="dm-stl-t"><Swap className={cx('dm-stl-lab', !done && 'dm-shim')}>{done ? doneLabel : steps[at].label}</Swap></span>
            <Swap className="dm-stl-n dm-num">{`${done ? n : at + 1}/${n}`}</Swap>
            <Icon name="chev-d" size="xs" className="dm-stl-chev" />
          </button>
          <div className="dm-stl-track" role="progressbar" aria-valuemin={0} aria-valuemax={n} aria-valuenow={done ? n : at}>
            {steps.map((_, i) => <i key={i} className={cx((done || i < current) && 'dm-on', !done && i === current && 'dm-run')} />)}
          </div>
          <Fold open={isOpen} className="dm-stl-fold">
            <ol className="dm-stl-list">
              {steps.map((s, i) => {
                const finished = done || i < current, running = !done && i === current;
                return (
                  <li key={i} className={cx(finished && 'dm-done', running && 'dm-run')}>
                    <span className="dm-g">{finished ? <span style={{ color: 'var(--dm-ok)', display: 'grid' }}><Icon name="check" size="xs" /></span> : running ? <Loader kind={loader} /> : <Dot color="var(--dm-faint)" />}</span>
                    {finished ? s.done ?? s.label : s.label}
                  </li>
                );
              })}
            </ol>
          </Fold>
        </div>
      );
    }
    
    step-loader.cssReact · 26 lines
    /* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
    /* ---------- Step loader ---------- */
    .dm-stl .dm-dm-main{gap:10px}
    .dm-stl-row{display:flex;align-items:center;gap:10px;background:var(--dm-surface);border-radius:var(--dm-r-pill);box-shadow:var(--dm-shadow);height:40px;padding:0 14px 0 13px;font-size:13.5px;min-width:250px;transition:box-shadow .3s}
    @media (hover:hover) and (pointer:fine){.dm-stl-row:hover{box-shadow:var(--dm-shadow-pop)}}
    .dm-stl-g{width:16px;display:grid;place-items:center;color:var(--dm-fg)}
    .dm-stl-t{flex:1;text-align:left}
    .dm-stl-n{color:var(--dm-muted);font-size:12.5px}
    .dm-stl-chev{color:var(--dm-muted);transition:transform .4s var(--dm-ease)}
    .dm-stl-row[aria-expanded="true"] .dm-stl-chev{transform:rotate(180deg)}
    .dm-stl-track{display:flex;gap:var(--dm-s-1);width:250px}
    .dm-stl-track i{flex:1;height:3px;border-radius:2px;background:var(--dm-line);position:relative;overflow:hidden}
    .dm-stl-track i::after{content:"";position:absolute;inset:0;background:var(--dm-accent-mark,var(--dm-accent));transform:scaleX(0);transform-origin:left;transition:transform .9s var(--dm-ease)}
    .dm-stl-track i.dm-on::after{transform:scaleX(1)}
    .dm-stl-track i.dm-run::after{transform:scaleX(.45);transition-duration:2.2s}
    .dm-stl-fold{width:250px}
    .dm-stl-fold>div{margin-inline:-16px;padding-inline:var(--dm-s-4)}
    .dm-stl-list{list-style:none;margin:6px 0 18px;padding:6px;background:var(--dm-surface);border-radius:14px;box-shadow:var(--dm-shadow)}
    .dm-stl-list li{display:flex;align-items:center;gap:9px;padding:6px var(--dm-s-2);font-size:13px;color:var(--dm-muted)}
    .dm-stl-list li.dm-done{color:var(--dm-fg2)}.dm-stl-list li.dm-run{color:var(--dm-fg)}
    .dm-stl-list li .dm-g{width:14px;display:grid;place-items:center}
    .dm-bars3{display:inline-flex;gap:2px;align-items:center;height:12px}
    .dm-bars3 i{width:2.5px;height:100%;border-radius:2px;background:currentColor;animation:dm-b3 .9s var(--dm-ease-io) infinite}
    .dm-bars3 i:nth-child(2){animation-delay:.15s}.dm-bars3 i:nth-child(3){animation-delay:.3s}
    @keyframes dm-b3{0%,100%{transform:scaleY(.35)}50%{transform:scaleY(1)}}
    
    step-loader.htmlHTML · 17 lines
    <div class="card wide bare" data-c="step-loader">
      <div class="stage-in">
        <div class="stl dm"><div class="dm-main">
          <button class="stl-row" aria-expanded="false">
            <span class="stl-g"></span>
            <span class="stl-t"><span class="stl-lab shim">Reading Figma frames</span></span>
            <span class="stl-n num">1/4</span>
            <svg class="i xs stl-chev"><use href="#i-chev-d"/></svg>
          </button>
          <div class="stl-track"><i></i><i></i><i></i><i></i></div>
          <div class="fold shut stl-fold"><div><ol class="stl-list"></ol></div></div>
          </div>
          <div class="demo"><div class="seg2" role="group" aria-label="Loader"><button aria-pressed="true" data-v="radar">Radar</button><button aria-pressed="false" data-v="think">Dots</button><button aria-pressed="false" data-v="spin">Ring</button><button aria-pressed="false" data-v="bars">Bars</button></div></div>
        </div>
      </div>
    </div>
    
    step-loader.cssHTML · 25 lines
    /* ---------- Step loader ---------- */
    .stl .dm-main{gap:10px}
    .stl-row{display:flex;align-items:center;gap:10px;background:var(--surface);border-radius:var(--r-pill);box-shadow:var(--shadow);height:40px;padding:0 14px 0 13px;font-size:13.5px;min-width:250px;transition:box-shadow .3s}
    @media (hover:hover) and (pointer:fine){.stl-row:hover{box-shadow:var(--shadow-pop)}}
    .stl-g{width:16px;display:grid;place-items:center;color:var(--fg)}
    .stl-t{flex:1;text-align:left}
    .stl-n{color:var(--muted);font-size:12.5px}
    .stl-chev{color:var(--muted);transition:transform .4s var(--ease)}
    .stl-row[aria-expanded="true"] .stl-chev{transform:rotate(180deg)}
    .stl-track{display:flex;gap:var(--s-1);width:250px}
    .stl-track i{flex:1;height:3px;border-radius:2px;background:var(--line);position:relative;overflow:hidden}
    .stl-track i::after{content:"";position:absolute;inset:0;background:var(--accent-mark,var(--accent));transform:scaleX(0);transform-origin:left;transition:transform .9s var(--ease)}
    .stl-track i.on::after{transform:scaleX(1)}
    .stl-track i.run::after{transform:scaleX(.45);transition-duration:2.2s}
    .stl-fold{width:250px}
    .stl-fold>div{margin-inline:-16px;padding-inline:var(--s-4)}
    .stl-list{list-style:none;margin:6px 0 18px;padding:6px;background:var(--surface);border-radius:14px;box-shadow:var(--shadow)}
    .stl-list li{display:flex;align-items:center;gap:9px;padding:6px var(--s-2);font-size:13px;color:var(--muted)}
    .stl-list li.done{color:var(--fg2)}.stl-list li.run{color:var(--fg)}
    .stl-list li .g{width:14px;display:grid;place-items:center}
    .bars3{display:inline-flex;gap:2px;align-items:center;height:12px}
    .bars3 i{width:2.5px;height:100%;border-radius:2px;background:currentColor;animation:b3 .9s var(--ease-io) infinite}
    .bars3 i:nth-child(2){animation-delay:.15s}.bars3 i:nth-child(3){animation-delay:.3s}
    @keyframes b3{0%,100%{transform:scaleY(.35)}50%{transform:scaleY(1)}}
    
    step-loader.jsHTML · 29 lines
    DM.def('step-loader', (r, c) => {
      const steps = [['Reading Figma frames','Read 4 frames'],['Matching tokens','Matched 18 tokens'],['Writing components','Wrote 3 components'],['Checking contrast','Contrast passes']];
      const row = $('.stl-row', r), lab = $('.stl-lab', r), n = $('.stl-n', r), g = $('.stl-g', r), bars = $$('.stl-track i', r), list = $('.stl-list', r), fold = $('.stl-fold', r);
      let kind = 'radar', cur = 0, done = false;
      const glyph = () => kind === 'radar' ? '<span class="radar"></span>' : kind === 'think' ? '<span class="think on"><i></i><i></i><i></i><i></i></span>' : kind === 'spin' ? '<span class="spin"></span>' : '<span class="bars3"><i></i><i></i><i></i></span>';
      const paint = () => {
        g.innerHTML = done ? `<span style="color:var(--ok)">${I('ok')}</span>` : glyph();
        list.innerHTML = steps.map((s, i) => `<li class="${i < cur || done ? 'done' : i === cur ? 'run' : ''}"><span class="g">${i < cur || done ? `<span style="color:var(--ok)">${I('check','xs')}</span>` : i === cur ? glyph() : '<span class="dot" style="color:var(--faint)"></span>'}</span>${i < cur || done ? s[1] : s[0]}</li>`).join('');
      };
      seg($('.seg2', r), v => { kind = v; paint(); });
      row.onclick = () => { const o = fold.classList.toggle('shut'); row.setAttribute('aria-expanded', !o); };
      paint();
      c.run(async () => {
        for (;;){
          for (cur = 0; cur < steps.length; cur++){
            bars[cur].classList.add('run'); paint();
            swap(lab, steps[cur][0]); swap(n, (cur + 1) + '/4');
            await c.wait(1900);
            bars[cur].classList.remove('run'); bars[cur].classList.add('on');
          }
          done = true; lab.classList.remove('shim'); swap(lab, 'Ready for review'); swap(n, '4/4'); paint();
          /* hold the finished state, then run the steps again */
          await c.wait(3200);
          done = false; cur = 0; bars.forEach(b => b.classList.remove('on', 'run')); lab.classList.add('shim'); paint();
          await c.wait(700);
        }
      });
    });
    
    06

    Questions

    What people ask about it.

    How do I mark the whole run as finished?
    Set current to steps.length or higher. The loader becomes a green check, every step shows its done text and the row shows doneLabel.
    Can I open the step list from my own code?
    Yes. Pass open and update it from onOpenChange to control it. Leave open out and the row toggles the list by itself, starting from defaultOpen.
    Does the loader advance on its own?
    No. It shows whatever current you pass, so you move it forward as your agent reports each step.
    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.
    Run summaryWhat the agent did, what changed and what needs you.
    All 18 in Agent status →