Skip to content
Star on GitHub
Review and approveLaunch checklist
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

Launch checklist

The agent checks a design before it ships, and fixes what fails. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.

$npx designme add launch-checklist
Ready to ship pricing?6 checks
01

Install

Add it to your project with the CLI.

HTML, CSS and JavaScript

$npx designme add launch-checklist

React

$npx designme add launch-checklist --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.

Launch checklist runs a list of pre-flight checks on a design before it ships, such as contrast, token use and alt text, and shows each result as it comes in. Failed checks get a Fix button so the agent can repair them, and the header button moves from running the checks to shipping.

  • When an agent should verify a page or component against your rules before it goes live.
  • When people need a quick summary of what still blocks a release.
  • When the agent can fix some failures by itself and people should trigger each fix.
  • When the final Ship it step should only be offered once every check passes.
03

Usage

Import it and drive it from your own state.

Import

import { LaunchChecklist } from '@/components/designme/launch-checklist/LaunchChecklist';

Example

The demo above as a React component, 61 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 { useEffect, useRef, useState } from 'react';
import { LaunchChecklist, type CheckState, type LaunchChecklistProps } from './LaunchChecklist';
import { useScript, useTakeover } from '../lib/useScript';

/* ---------- Launch checklist: six checks run in turn, two fail and get fixed, then it ships ---------- */
const CHECKS: [label: string, problem: string | null, result: string][] = [
  ['Contrast meets AA', null, 'All 14 text pairs pass'],
  ['Uses library tokens', '3 raw hex values in the FAQ', 'Swapped to color.text.muted'],
  ['Responsive at 375px', null, 'No overflow on 3 breakpoints'],
  ['Alt text on images', '2 icons have no label', 'Added labels from the layer names'],
  ['Links resolve', null, '12 of 12 links return 200'],
  ['Copy matches the brief', null, 'Prices and plan names match']
];
type Row = { state: CheckState; note?: string };
const fresh = (): Row[] => CHECKS.map(() => ({ state: 'wait' }));

export default function LaunchChecklistDemo() {
  const [rows, setRows] = useState<Row[]>(fresh);
  const [status, setStatus] = useState(`${CHECKS.length} checks`);
  const [phase, setPhase] = useState<'idle' | 'running' | 'checked' | 'shipped'>('idle');
  const [runs, setRuns] = useState(0);
  const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
  useEffect(() => () => timers.current.forEach(clearTimeout), []);
  // the checks run once on its own, unless you take over first; the fixes are left for you
  const ref = useRef<HTMLDivElement>(null), mine = useTakeover(ref);
  useScript(async ({ wait }) => { await wait(1300); if (!mine()) setRuns(r => r || 1); });
  const set = (i: number, row: Row) => setRows(rs => rs.map((r, k) => k === i ? row : r));
  const tally = (rs: Row[]) => { const bad = rs.filter(r => r.state === 'bad').length; setStatus(bad ? `${bad} to fix before shipping` : 'All clear, ready to ship'); };

  useScript(async ({ wait }) => {
    if (!runs) return;
    setStatus('Checking'); setPhase('running');
    const out = fresh();
    for (let i = 0; i < CHECKS.length; i++) {
      set(i, { state: 'run', note: 'Checking' }); await wait(520 + Math.random() * 300);
      out[i] = CHECKS[i][1] ? { state: 'bad', note: CHECKS[i][1]! } : { state: 'ok', note: CHECKS[i][2] }; set(i, out[i]);
    }
    setPhase('checked'); tally(out);
  }, [runs]);

  const fix = (i: number) => {
    set(i, { state: 'run', note: 'Fixing' });
    timers.current.push(setTimeout(() => setRows(rs => { const next = rs.map((r, k) => k === i ? { state: 'ok' as const, note: CHECKS[i][2] } : r); tally(next); return next; }), 900));
  };
  const clear = phase === 'checked' && rows.every(r => r.state === 'ok');
  const action: LaunchChecklistProps['action'] =
    phase === 'shipped' ? { label: 'Ship it', icon: 'rocket', accent: true, disabled: true }
    : clear ? { label: 'Ship it', icon: 'rocket', accent: true, onClick: () => { setStatus('Shipped to production'); setPhase('shipped'); } }
    : phase === 'checked' ? { label: 'Run again', icon: 'refresh', onClick: () => { timers.current.forEach(clearTimeout); setRows(fresh()); setStatus(`${CHECKS.length} checks`); setPhase('idle'); setRuns(0); } }
    : { label: 'Run checks', icon: 'play', disabled: phase === 'running', onClick: () => setRuns(r => r + 1) };

  return (
    <div ref={ref} style={{ width: '100%' }}>
      <LaunchChecklist title="Ready to ship pricing?" status={status} action={action}
        checks={rows.map((r, i) => ({ label: CHECKS[i][0], state: r.state, note: r.note, onFix: r.state === 'bad' ? () => fix(i) : undefined }))} />
    </div>
  );
}
04

Props

Everything LaunchChecklist takes.

PropTypeWhat it does
titlestringLike "Ready to ship pricing?"
checksrequiredLaunchCheck[]
statusstringThe line under the title. Defaults to a count of the checks, "Checking" while one runs, then what is left to fix
action{ label: string; icon?: IconName; /** The accent colour, for the step that ships */ accent?: boolean; disabled?: boolean; onClick?: () => void }The button beside the title, like Run checks, Run again or Ship it
barebooleanSits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own
classNamestring
05

Code

The full source, as the CLI installs it.

LaunchChecklist.tsxReact · 78 lines
'use client';
import { cx } from '../lib/cx';
import { Icon, type IconName } from '../lib/icons';
import { enter, Spin, Swap } from '../lib/motion';
import '../designme.css';
import './launch-checklist.css';

export type CheckState = 'wait' | 'run' | 'ok' | 'bad';

export interface LaunchCheck {
  /** What is checked, like "Contrast meets AA" */
  label: string;
  /** wait (not run yet), run (checking or fixing now), ok or bad */
  state: CheckState;
  /** The quiet line under the label, like "All 14 text pairs pass" or what is wrong */
  note?: string;
  /** Shows a Fix button on a failed check */
  onFix?: () => void;
}

export interface LaunchChecklistProps {
  /** Like "Ready to ship pricing?" */
  title?: string;
  checks: LaunchCheck[];
  /** The line under the title. Defaults to a count of the checks, "Checking" while one runs, then what is left to fix */
  status?: string;
  /** The button beside the title, like Run checks, Run again or Ship it */
  action?: { label: string; icon?: IconName; /** The accent colour, for the step that ships */ accent?: boolean; disabled?: boolean; onClick?: () => void };
  /** Sits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own */
  bare?: boolean;
  className?: string;
}

function Glyph({ state }: { state: CheckState }) {
  // a fresh glyph comes in each time the state changes; the wrapper takes the entrance so a spinner inside keeps turning
  const g = state === 'run' ? <Spin /> : state === 'ok' ? <span style={{ color: 'var(--dm-ok)', display: 'grid' }}><Icon name="ok" /></span>
    : state === 'bad' ? <span style={{ color: 'var(--dm-bad)', display: 'grid' }}><Icon name="alert" /></span> : <span className="dm-lcx-ring" />;
  return <span key={state} className={state === 'wait' ? undefined : 'dm-enter'} style={{ display: 'grid' }}>{g}</span>;
}

const summary = (checks: LaunchCheck[]) => {
  if (checks.some(c => c.state === 'run')) return 'Checking';
  if (checks.every(c => c.state === 'wait')) return `${checks.length} checks`;
  const bad = checks.filter(c => c.state === 'bad').length;
  return bad ? `${bad} to fix before shipping` : 'All clear, ready to ship';
};

/** Pre-flight checks before something ships, each with its result and a Fix for the ones that fail. */
export function LaunchChecklist({ title = 'Ready to ship?', checks, status, action, bare = true, className }: LaunchChecklistProps) {
  return (
    <div data-dm="" className={cx('dm-lcx', !bare && 'dm-panel', className)} style={bare ? { paddingInline: 0 } : undefined}>
      <div className="dm-lcx-h">
        <div><b>{title}</b><Swap className="dm-muted dm-lcx-st">{status ?? summary(checks)}</Swap></div>
        {action && (
          <button type="button" className={cx('dm-pill dm-sm dm-lcx-run', action.accent ? 'dm-accent' : 'dm-primary')} disabled={action.disabled} onClick={action.onClick}>
            {action.icon && <Icon name={action.icon} size="xs" />}{action.label}
          </button>
        )}
      </div>
      <div className="dm-lcx-bar" role="progressbar" aria-label={title} aria-valuemin={0} aria-valuemax={checks.length}
        aria-valuenow={checks.filter(c => c.state === 'ok' || c.state === 'bad').length}>
        {checks.map((c, i) => <i key={i} className={c.state === 'wait' ? undefined : 'dm-' + c.state} />)}
      </div>
      <div className="dm-lcx-list">
        {checks.map((c, i) => (
          <div key={i} className={cx('dm-lcx-r', 'dm-' + c.state)}>
            <span className="dm-g"><Glyph state={c.state} /></span>
            <span className="dm-t">{c.label}<Swap className="dm-d">{c.note ?? ''}</Swap></span>
            {c.state === 'bad' && c.onFix && (
              <button type="button" className="dm-pill dm-soft dm-sm dm-enter" style={enter().style} onClick={c.onFix}><Icon name="wand" size="xs" />Fix</button>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}
launch-checklist.cssReact · 20 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Launch checklist ---------- */
.dm-lcx{padding:var(--dm-s-4);display:flex;flex-direction:column;gap:var(--dm-s-3)}
.dm-lcx-h{display:flex;align-items:center;justify-content:space-between;gap:10px}
.dm-lcx-h>div{display:flex;flex-direction:column}
.dm-lcx-h b{font-weight:500;font-size:13.5px}
.dm-lcx-st{font-size:12.5px}
.dm-lcx-bar{display:flex;gap:3px}
.dm-lcx-bar i{flex:1;height:5px;border-radius:3px;background:var(--dm-line);transition:background .45s var(--dm-ease)}
.dm-lcx-bar i.dm-ok{background:var(--dm-ok)}.dm-lcx-bar i.dm-bad{background:var(--dm-bad)}.dm-lcx-bar i.dm-run{background:var(--dm-accent-mark,var(--dm-accent));opacity:.5}
.dm-lcx-list{display:flex;flex-direction:column}
.dm-lcx-r{display:flex;align-items:center;gap:10px;padding:7px var(--dm-s-1);font-size:13px;border-radius:10px;min-height:38px}
.dm-lcx-r .dm-g{width:18px;height:18px;display:grid;place-items:center;flex:none;color:var(--dm-faint)}
.dm-lcx-r .dm-t{flex:1;min-width:0}
.dm-lcx-r .dm-d{display:block;font-size:12px;color:var(--dm-muted)}
.dm-lcx-r.dm-bad .dm-d{color:var(--dm-bad)}
.dm-lcx-r.dm-wait .dm-t{color:var(--dm-muted)}
.dm-lcx-r .dm-pill{flex:none}
.dm-lcx-ring{width:14px;height:14px;border-radius:50%;box-shadow:inset 0 0 0 1.5px currentColor}
launch-checklist.htmlHTML · 10 lines
<div class="card bare" data-c="launch-checklist">
  <div class="stage-in">
    <div class="lcx panel">
      <div class="lcx-h"><div><b>Ready to ship pricing?</b><span class="muted lcx-st">6 checks</span></div><button class="pill primary sm lcx-run"><svg class="i xs"><use href="#i-play"/></svg>Run checks</button></div>
      <div class="lcx-bar"></div>
      <div class="lcx-list"></div>
    </div>
  </div>
</div>
launch-checklist.cssHTML · 19 lines
/* ---------- Launch checklist ---------- */
.lcx{padding:var(--s-4);display:flex;flex-direction:column;gap:var(--s-3)}
.lcx-h{display:flex;align-items:center;justify-content:space-between;gap:10px}
.lcx-h>div{display:flex;flex-direction:column}
.lcx-h b{font-weight:500;font-size:13.5px}
.lcx-st{font-size:12.5px}
.lcx-bar{display:flex;gap:3px}
.lcx-bar i{flex:1;height:5px;border-radius:3px;background:var(--line);transition:background .45s var(--ease)}
.lcx-bar i.ok{background:var(--ok)}.lcx-bar i.bad{background:var(--bad)}.lcx-bar i.run{background:var(--accent-mark,var(--accent));opacity:.5}
.lcx-list{display:flex;flex-direction:column}
.lcx-r{display:flex;align-items:center;gap:10px;padding:7px var(--s-1);font-size:13px;border-radius:10px;min-height:38px}
.lcx-r .g{width:18px;height:18px;display:grid;place-items:center;flex:none;color:var(--faint)}
.lcx-r .t{flex:1;min-width:0}
.lcx-r .d{display:block;font-size:12px;color:var(--muted)}
.lcx-r.bad .d{color:var(--bad)}
.lcx-r.wait .t{color:var(--muted)}
.lcx-r .pill{flex:none}
.lcx-ring{width:14px;height:14px;border-radius:50%;box-shadow:inset 0 0 0 1.5px currentColor}
launch-checklist.jsHTML · 32 lines
DM.def('launch-checklist', (r, c) => {
  r.innerHTML = r.innerHTML.replace('{play}', I('play','xs'));
  const C = [
    ['Contrast meets AA', null, 'All 14 text pairs pass'],
    ['Uses library tokens', '3 raw hex values in the FAQ', 'Swapped to color.text.muted'],
    ['Responsive at 375px', null, 'No overflow on 3 breakpoints'],
    ['Alt text on images', '2 icons have no label', 'Added labels from the layer names'],
    ['Links resolve', null, '12 of 12 links return 200'],
    ['Copy matches the brief', null, 'Prices and plan names match']
  ];
  const list = $('.lcx-list', r), bar = $('.lcx-bar', r), st = $('.lcx-st', r), run = $('.lcx-run', r);
  bar.innerHTML = C.map(() => '<i></i>').join(''); const segs = $$('i', bar);
  list.innerHTML = C.map(x => `<div class="lcx-r wait"><span class="g"><span class="lcx-ring"></span></span><span class="t">${x[0]}<span class="d"></span></span></div>`).join('');
  const rows = $$('.lcx-r', list);
  const G = { ok:`<span style="color:var(--ok)">${I('ok')}</span>`, bad:`<span style="color:var(--bad)">${I('alert')}</span>`, run:'<span class="spin"></span>' };
  function set(i, s, note){ const rw = rows[i]; rw.className = 'lcx-r ' + s; const g = $('.g', rw); g.innerHTML = G[s]; enter(g); swap($('.d', rw), note || ''); segs[i].className = s === 'run' ? 'run' : s; }
  function tally(){ const bad = rows.filter(x => x.classList.contains('bad')).length; swap(st, bad ? bad + ' to fix before shipping' : 'All clear, ready to ship'); if (!bad){ run.innerHTML = I('rocket','xs') + 'Ship it'; run.className = 'pill accent sm lcx-run'; run.onclick = () => { swap(st, 'Shipped to production'); run.disabled = true; }; } }
  run.onclick = () => c.run(async () => {
    run.disabled = true; swap(st, 'Checking');
    for (let i = 0; i < C.length; i++){
      set(i, 'run', 'Checking'); await c.wait(520 + Math.random() * 300);
      if (C[i][1]){ set(i, 'bad', C[i][1]); const f = html(`<button class="pill soft sm">${I('wand','xs')}Fix</button>`); rows[i].appendChild(f); enter(f);
        f.onclick = () => c.run(async () => { f.remove(); set(i, 'run', 'Fixing'); await c.wait(900); set(i, 'ok', C[i][2]); tally(); });
      } else set(i, 'ok', C[i][2]);
    }
    run.disabled = false; run.innerHTML = I('refresh','xs') + 'Run again'; tally();
    if (rows.some(x => x.classList.contains('bad'))) run.onclick = () => DM.mount(r.closest('.card'));
  });
  // the preview runs the checks once on its own after the entrance; the fixes are left for you
  c.auto(1300, () => { if (!run.disabled) run.click(); });
});
06

Questions

What people ask about it.

Does the checklist run the checks itself?
No. It shows the checks you pass. Your code or agent runs each check and updates its state and note as results arrive.
How do I offer a fix for a failed check?
Give that check an onFix handler while its state is bad. The Fix button calls it, and you can set the state to run with a note like "Fixing" until it passes.
How do I switch the button to Ship it once everything passes?
Change the action prop when all checks are ok, for example { label: 'Ship it', icon: 'rocket', accent: true }. The demo does this.
07

More in Review and approve

Moments where a person checks, steers or signs off on the agent's work.

Change reviewAccept or reject each change the agent made, file by file.
Plan reviewCheck the agent's plan, edit steps, then let it run.
Permission gateThe agent asks before it touches something new.
Hand-off to a personThe agent hands a conversation to a person, with what they need to pick it up.
Decision promptThe agent asks a few quick questions before it starts.
Proposal cardA suggested change with confidence, alternatives and undo.
Design tunerTweak a component by hand or let the agent tune it.
Rewrite toolbarSelect a sentence and let the agent rewrite it in place.
All 14 in Review and approve →