Review and approveLaunch checklist
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callLaunch 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.
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add launch-checklistReact
npx designme add launch-checklist --reactThe 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.
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.
- Each check has a
label, an optionalnoteand astate:wait,run,okorbad, each with its own glyph. - A segmented bar at the top fills in with green and red as checks finish.
- The status line counts the checks, says Checking while one runs, then reports how many are left to fix or that all is clear.
statusoverrides it. - A failed check with
onFixshows a Fix button. Set the check back torunwhile the agent fixes it. actionadds a header button with a label, icon,disabledstate and anaccentcolour for the step that ships.- Notes crossfade when they change, so "Checking" can turn into the result in place.
- The bar has
role="progressbar", labelled with the title, and itsaria-valuenowcounts the checks that have finished, passed or failed. - Fix and the header action are real buttons, and the action can be disabled.
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>
);
}
Props
Everything LaunchChecklist takes.
| Prop | Type | What it does |
|---|---|---|
title | string | Like "Ready to ship pricing?" |
checksrequired | LaunchCheck[] | |
status | string | The 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 |
bare | boolean | Sits flat in a chat thread (the default, as on designme.dev). Set false to give it a card of its own |
className | string |
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(); });
});
Questions
What people ask about it.
- Does the checklist run the checks itself?
- No. It shows the
checksyou pass. Your code or agent runs each check and updates itsstateandnoteas results arrive. - How do I offer a fix for a failed check?
- Give that check an
onFixhandler while its state isbad. The Fix button calls it, and you can set the state torunwith a note like "Fixing" until it passes. - How do I switch the button to Ship it once everything passes?
- Change the
actionprop when all checks areok, for example{ label: 'Ship it', icon: 'rocket', accent: true }. The demo does this.
More in Review and approve
Moments where a person checks, steers or signs off on the agent's work.