Process as a live checklist
Phases with real completion states, including one honestly incomplete.
Live preview
Source
This exact file renders the preview above.
import { Container } from '@/components/ui/layout'
import { Progress } from '@/components/ui/progress'
import { Status } from '@/components/ui/status'
/**
* Process as a live checklist
*
* A rollout tracker rather than a sales section: phases with real completion
* states. Suited to a public roadmap or an internal adoption page, where the
* honest answer includes what is not done.
*/
const phases = [
{
name: 'Foundations',
progress: 100,
status: 'operational' as const,
statusLabel: 'Complete',
tasks: ['Token set', 'Light and dark schemes', 'Density axis', 'Focus treatment'],
},
{
name: 'Primitives',
progress: 100,
status: 'operational' as const,
statusLabel: 'Complete',
tasks: ['45 components', 'States and keyboard contracts', 'Usage documentation'],
},
{
name: 'Composition',
progress: 100,
status: 'operational' as const,
statusLabel: 'Complete',
tasks: ['Sections', 'Page patterns', 'Composition playground'],
},
{
name: 'Verification',
progress: 62,
status: 'pending' as const,
statusLabel: 'In progress',
tasks: ['Automated route audit', 'Manual screen-reader passes', 'Reduced-motion review'],
},
]
export default function ChecklistPhasesProcess() {
return (
<section className="border-b border-line bg-canvas py-section">
<Container>
<div className="max-w-2xl">
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
Where the work actually is.
</h2>
<p className="mt-3 text-md text-ink-muted">
Three phases finished, one honestly in progress.
</p>
</div>
<div className="mt-10 grid gap-4 sm:grid-cols-2">
{phases.map((phase) => (
<div key={phase.name} className="rounded-xl border border-line bg-surface p-5">
<div className="flex items-center justify-between gap-3">
<h3 className="text-md font-semibold text-ink-strong">{phase.name}</h3>
<Status appearance="pill" kind={phase.status} label={phase.statusLabel} />
</div>
<Progress
value={phase.progress}
tone={phase.progress === 100 ? 'success' : 'accent'}
className="mt-4"
showValue
/>
<ul className="mt-4 flex flex-col gap-1.5">
{phase.tasks.map((task) => (
<li key={task} className="text-sm text-ink-muted">
· {task}
</li>
))}
</ul>
</div>
))}
</div>
</Container>
</section>
)
}
components/blocks/sections/process/checklist-phases.tsx
import { Container } from '@/components/ui/layout'
import { Progress } from '@/components/ui/progress'
import { Status } from '@/components/ui/status'
/**
* Process as a live checklist
*
* A rollout tracker rather than a sales section: phases with real completion
* states. Suited to a public roadmap or an internal adoption page, where the
* honest answer includes what is not done.
*/
const phases = [
{
name: 'Foundations',
progress: 100,
status: 'operational' as const,
statusLabel: 'Complete',
tasks: ['Token set', 'Light and dark schemes', 'Density axis', 'Focus treatment'],
},
{
name: 'Primitives',
progress: 100,
status: 'operational' as const,
statusLabel: 'Complete',
tasks: ['45 components', 'States and keyboard contracts', 'Usage documentation'],
},
{
name: 'Composition',
progress: 100,
status: 'operational' as const,
statusLabel: 'Complete',
tasks: ['Sections', 'Page patterns', 'Composition playground'],
},
{
name: 'Verification',
progress: 62,
status: 'pending' as const,
statusLabel: 'In progress',
tasks: ['Automated route audit', 'Manual screen-reader passes', 'Reduced-motion review'],
},
]
export default function ChecklistPhasesProcess() {
return (
<section className="border-b border-line bg-canvas py-section">
<Container>
<div className="max-w-2xl">
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
Where the work actually is.
</h2>
<p className="mt-3 text-md text-ink-muted">
Three phases finished, one honestly in progress.
</p>
</div>
<div className="mt-10 grid gap-4 sm:grid-cols-2">
{phases.map((phase) => (
<div key={phase.name} className="rounded-xl border border-line bg-surface p-5">
<div className="flex items-center justify-between gap-3">
<h3 className="text-md font-semibold text-ink-strong">{phase.name}</h3>
<Status appearance="pill" kind={phase.status} label={phase.statusLabel} />
</div>
<Progress
value={phase.progress}
tone={phase.progress === 100 ? 'success' : 'accent'}
className="mt-4"
showValue
/>
<ul className="mt-4 flex flex-col gap-1.5">
{phase.tasks.map((task) => (
<li key={task} className="text-sm text-ink-muted">
· {task}
</li>
))}
</ul>
</div>
))}
</div>
</Container>
</section>
)
}
components/ui/progress.tsx
import { cn } from '@/lib/cn'
/**
* Progress
*
* Determinate by default. When `value` is omitted the bar renders as
* indeterminate and drops `aria-valuenow`, which is what tells assistive tech
* "in progress, duration unknown" rather than "0%".
*/
export interface ProgressProps {
/** 0–100. Omit for an indeterminate bar. */
value?: number
label?: string
/** Renders the numeric value beside the label. */
showValue?: boolean
/** Accessible name when no visible label is wanted. */
srLabel?: string
tone?: 'accent' | 'success' | 'warning' | 'danger'
size?: 'sm' | 'md'
className?: string
}
const tones = {
accent: 'bg-accent',
success: 'bg-success',
warning: 'bg-warning',
danger: 'bg-danger',
} as const
export function Progress({
value,
label,
showValue = false,
srLabel,
tone = 'accent',
size = 'md',
className,
}: ProgressProps) {
const clamped = value === undefined ? undefined : Math.max(0, Math.min(100, Math.round(value)))
return (
<div className={cn('w-full', className)}>
{(label || showValue) && (
<div className="mb-1.5 flex items-baseline justify-between gap-3">
{label ? <span className="text-xs font-medium text-ink">{label}</span> : <span />}
{showValue && clamped !== undefined ? (
<span className="font-mono text-xs text-ink-muted tabular-nums">{clamped}%</span>
) : null}
</div>
)}
<div
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={clamped}
aria-label={label ? undefined : (srLabel ?? 'Progress')}
aria-valuetext={clamped === undefined ? 'In progress' : `${clamped}%`}
className={cn(
'w-full overflow-hidden rounded-full bg-surface-sunken',
size === 'sm' ? 'h-1' : 'h-2',
)}
>
{clamped === undefined ? (
<div className={cn('h-full w-2/5 animate-pulse-token rounded-full', tones[tone])} />
) : (
<div
className={cn(
'h-full rounded-full transition-[width] duration-300 ease-standard',
tones[tone],
)}
style={{ width: `${clamped}%` }}
/>
)}
</div>
</div>
)
}
export interface ProgressRingProps {
value: number
size?: number
label?: string
tone?: keyof typeof tones
className?: string
}
/** Circular variant, for dashboard tiles where a bar would waste width. */
export function ProgressRing({
value,
size = 56,
label,
tone = 'accent',
className,
}: ProgressRingProps) {
const clamped = Math.max(0, Math.min(100, Math.round(value)))
const stroke = 5
const radius = (size - stroke) / 2
const circumference = 2 * Math.PI * radius
const offset = circumference - (clamped / 100) * circumference
const strokeColor = {
accent: 'var(--color-accent)',
success: 'var(--color-success)',
warning: 'var(--color-warning)',
danger: 'var(--color-danger)',
}[tone]
return (
<div
className={cn('relative inline-flex items-center justify-center', className)}
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={clamped}
aria-label={label ?? 'Progress'}
>
<svg width={size} height={size} className="-rotate-90" aria-hidden="true">
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="var(--color-surface-sunken)"
strokeWidth={stroke}
/>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={strokeColor}
strokeWidth={stroke}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
/>
</svg>
<span className="absolute font-mono text-xs font-medium tabular-nums">{clamped}%</span>
</div>
)
}
components/ui/status.tsx
import { CheckCircle2, AlertTriangle, XCircle, Info, CircleDashed, Clock } from 'lucide-react'
import { cn } from '@/lib/cn'
/**
* Status
*
* Deliberately separate from Badge. A status describes the *state of a thing*
* (a deployment, an invoice, a service) and therefore always pairs an icon
* with a label — colour is the third signal, never the only one. That rule is
* what makes the library legible to colour-blind users without a theme switch.
*/
export type StatusKind = 'operational' | 'degraded' | 'down' | 'pending' | 'info' | 'idle'
export interface StatusProps {
kind: StatusKind
label: string
/** `dot` for dense tables, `pill` for standalone display. */
appearance?: 'dot' | 'pill' | 'inline'
className?: string
}
const config = {
operational: {
icon: CheckCircle2,
text: 'text-success',
bg: 'bg-success-soft border-success-line',
dot: 'bg-success',
},
degraded: {
icon: AlertTriangle,
text: 'text-warning',
bg: 'bg-warning-soft border-warning-line',
dot: 'bg-warning',
},
down: {
icon: XCircle,
text: 'text-danger',
bg: 'bg-danger-soft border-danger-line',
dot: 'bg-danger',
},
pending: { icon: Clock, text: 'text-info', bg: 'bg-info-soft border-info-line', dot: 'bg-info' },
info: { icon: Info, text: 'text-info', bg: 'bg-info-soft border-info-line', dot: 'bg-info' },
idle: {
icon: CircleDashed,
text: 'text-ink-subtle',
bg: 'bg-surface-sunken border-line',
dot: 'bg-ink-subtle',
},
} as const
export function Status({ kind, label, appearance = 'inline', className }: StatusProps) {
const entry = config[kind]
const Icon = entry.icon
if (appearance === 'dot') {
return (
<span className={cn('inline-flex items-center gap-2 text-sm text-ink', className)}>
<span className={cn('size-2 shrink-0 rounded-full', entry.dot)} aria-hidden="true" />
{label}
</span>
)
}
if (appearance === 'pill') {
return (
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium',
entry.bg,
entry.text,
className,
)}
>
<Icon className="size-3.5 shrink-0" aria-hidden="true" />
{label}
</span>
)
}
return (
<span
className={cn('inline-flex items-center gap-1.5 text-sm font-medium', entry.text, className)}
>
<Icon className="size-4 shrink-0" aria-hidden="true" />
{label}
</span>
)
}
Demo source — adapt to your project. Foundry is not published as a package.
Usage
A rollout tracker rather than a sales section. The honest answer includes what is not done, which is why one phase is deliberately in progress.
- Never show a roadmap where everything is complete; nobody believes it.
- Pair every progress bar with a status word.
Variants and states
Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.
- Four phases
- Per-phase progress
- Status pills
Accessibility
- Progressbar roles
- Each phase exposes a real progress value.
- Status pills
- State is carried by icon and word, not colour alone.
Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.
Related
All sectionsProgress toward goals
Ratios rather than totals — a ring for the headline, bars for the breakdown.
starter3 variantsHorizontal milestone rail
A roadmap that scrolls sideways rather than wrapping.
starter3 variantsSetup checklist
In-product onboarding whose progress is computed from the items.
starter3 variants