Success states
The page after a form. It says what happened, what comes next and offers one useful thing to do. A reference with next steps, a receipt to print, what was sent with minutes left to change it, and where a request stands now.
One of 3 components in Form states
Variants
Details
What Success states brings along, read from the registry and the source.
- Category
- Form states
- Area
- Content
- Variants
- 4
- Version
- 1.0.0 · since 1.1.0
- File
- components/ui/success-states.tsx
- npm packages
- None
- Languages
- English and German, 21 texts of its own
- Studio
- All 4 variants editable
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The SuccessStatesProps interface from components/ui/success-states.tsx, with the comments from the code.
variantoptionalSuccessStatesVariantnext-stepsreceiptsubmitted-summarystatus-tracker
titlerequiredReact.ReactNodedescriptionoptionalReact.ReactNodereferenceoptionalstringA reference people can quote, e.g. an order or ticket number
stepsoptionalSuccessStep[]linesoptional{ title: string; amount: number }[]receipt: lines, total, how it was paid, who paid and a note
totaloptionalnumbercurrencyoptionalstringpaidWithoptionalstringdateoptionalstringissueroptional{ name: string; address?: string; taxId?: string }noteoptionalstringfieldsoptional{ label: string; value: string }[]submitted-summary: what was sent, as label and value
editMinutesoptionalnumbersubmitted-summary: minutes left to change it
etaoptionalstringstatus-tracker: an estimate, e.g. "by Friday"
primaryoptional{ label: string; href?: string; icon?: IconName }The main action and a quiet second one
secondaryoptional{ label: string; href?: string; icon?: IconName }onEditoptional() => voidonPrintoptional() => void
Built-in texts
Success states writes these texts itself. The language follows the KitProvider, the labels prop replaces single ones.
Show all 21 texts
| Key | English | German |
|---|---|---|
| reference | English Reference | German Vorgangsnummer |
| copy | English Copy reference | German Nummer kopieren |
| copied | English Copied | German Kopiert |
| next | English What happens next | German So geht es weiter |
| receipt | English Receipt | German Beleg |
| paidWith | English Paid with {p} | German Bezahlt mit {p} |
| total | English Total | German Gesamt |
| English Print | German Drucken | |
| save | English Save as PDF | German Als PDF speichern |
| sentTitle | English What you sent | German Das hast du geschickt |
| editFor | English You can change it for {n} more minutes | German Du kannst es noch {n} Minuten ändern |
| editClosed | English Changes are closed, we are already on it | German Änderungen sind geschlossen, wir sind schon dran |
| edit | English Change my answers | German Antworten ändern |
| now | English Now | German Jetzt |
| eta | English Expected {e} | German Voraussichtlich {e} |
| sms | English Send me updates by text | German Updates per SMS schicken |
| smsText | English One message per step, nothing else | German Eine Nachricht je Schritt, sonst nichts |
| smsOn | English You get a text at every step. | German Du bekommst bei jedem Schritt eine SMS. |
| dateLabel | English Date | German Datum |
| from | English From | German Von |
| tax | English Tax ID | German Steuernummer |
Accessibility
Read from the source of the component.
- Sets aria-current, aria-label and aria-live, so screen readers announce purpose and state.
- Announces changes through a live region without moving focus.
- Adds text for screen readers where icons and graphics stand alone.
- Shows a visible focus ring when used with the keyboard.
- The server renders the starting state in full, JavaScript only adds the interaction.
Motion
- Rises in briefly the first time it comes into view (kit-rise, useInView).
- Brings animations of its own, calmly timed and without wobble.
- With reduced motion (prefers-reduced-motion) it stays still. 1 places in the code switch motion off then.
Source
The file aiw add puts into your project. From then on it is yours.
Show the file, 462 lines
'use client';
import { useEffect, useRef, useState } from 'react';
import { cn, round, useLocale, useTexts } from '@/lib/kit';
import { Button } from '@/components/ui/button';
import { Icon, type IconName } from '@/components/ui/icon';
import { Switch } from '@/components/ui/switch';
import { Reveal } from '@/components/ui/reveal';
import { Section } from '@/components/ui/section';
import { Stagger } from '@/components/ui/motion';
import { DoneMark } from '@/components/ui/form-flow';
/*
* The page after a form. It confirms what happened, says what comes next and
* gives one useful thing to do. Focus lands on the title, so screen readers
* announce the result at once.
*
* next-steps a reference to copy and the steps ahead, the first one ticked
* receipt what was paid, ready to print or save, with the tax note
* submitted-summary what was sent, with a few minutes to change it on a ring that runs down
* status-tracker where the request stands now, an estimate, and updates by text on request
*/
export type SuccessStatesVariant = 'next-steps' | 'receipt' | 'submitted-summary' | 'status-tracker';
export interface SuccessStep {
title: string;
text?: string;
icon?: IconName;
/** status-tracker: done, current or later */
state?: 'done' | 'current' | 'later';
/** status-tracker: a time or date shown at the side */
when?: string;
}
export interface SuccessStatesProps {
variant?: SuccessStatesVariant;
title: React.ReactNode;
description?: React.ReactNode;
/** A reference people can quote, e.g. an order or ticket number */
reference?: string;
steps?: SuccessStep[];
/** receipt: lines, total, how it was paid, who paid and a note */
lines?: { title: string; amount: number }[];
total?: number;
currency?: string;
paidWith?: string;
date?: string;
issuer?: { name: string; address?: string; taxId?: string };
note?: string;
/** submitted-summary: what was sent, as label and value */
fields?: { label: string; value: string }[];
/** submitted-summary: minutes left to change it */
editMinutes?: number;
/** status-tracker: an estimate, e.g. "by Friday" */
eta?: string;
/** The main action and a quiet second one */
primary?: { label: string; href?: string; icon?: IconName };
secondary?: { label: string; href?: string; icon?: IconName };
onEdit?: () => void;
onPrint?: () => void;
labels?: Partial<Texts>;
className?: string;
}
const TEXTS = {
en: {
reference: 'Reference',
copy: 'Copy reference',
copied: 'Copied',
next: 'What happens next',
receipt: 'Receipt',
paidWith: 'Paid with {p}',
total: 'Total',
print: 'Print',
save: 'Save as PDF',
sentTitle: 'What you sent',
editFor: 'You can change it for {n} more minutes',
editClosed: 'Changes are closed, we are already on it',
edit: 'Change my answers',
now: 'Now',
eta: 'Expected {e}',
sms: 'Send me updates by text',
smsText: 'One message per step, nothing else',
smsOn: 'You get a text at every step.',
dateLabel: 'Date',
from: 'From',
tax: 'Tax ID',
},
de: {
reference: 'Vorgangsnummer',
copy: 'Nummer kopieren',
copied: 'Kopiert',
next: 'So geht es weiter',
receipt: 'Beleg',
paidWith: 'Bezahlt mit {p}',
total: 'Gesamt',
print: 'Drucken',
save: 'Als PDF speichern',
sentTitle: 'Das hast du geschickt',
editFor: 'Du kannst es noch {n} Minuten ändern',
editClosed: 'Änderungen sind geschlossen, wir sind schon dran',
edit: 'Antworten ändern',
now: 'Jetzt',
eta: 'Voraussichtlich {e}',
sms: 'Updates per SMS schicken',
smsText: 'Eine Nachricht je Schritt, sonst nichts',
smsOn: 'Du bekommst bei jedem Schritt eine SMS.',
dateLabel: 'Datum',
from: 'Von',
tax: 'Steuernummer',
},
};
type Texts = (typeof TEXTS)['en'];
/** Title that takes the focus once, so the result is announced */
function Title({ children, className }: { children: React.ReactNode; className?: string }) {
const ref = useRef<HTMLHeadingElement>(null);
useEffect(() => ref.current?.focus({ preventScroll: true }), []);
return (
<h2 ref={ref} tabIndex={-1} className={cn('kit-rise text-balance text-h2 font-semibold text-foreground outline-none', className)} style={{ '--kit-delay': '120ms' } as React.CSSProperties}>
{children}
</h2>
);
}
function Actions({ p, center }: { p: SuccessStatesProps; center?: boolean }) {
if (!p.primary && !p.secondary) return null;
return (
<div className={cn('kit-rise mt-8 flex flex-col gap-3 sm:flex-row', center && 'sm:justify-center')} style={{ '--kit-delay': '360ms' } as React.CSSProperties}>
{p.primary && (
<Button href={p.primary.href ?? '#'} icon={p.primary.icon ?? 'arrow-right'} className="max-sm:w-full">
{p.primary.label}
</Button>
)}
{p.secondary && (
<Button href={p.secondary.href ?? '#'} variant="outline" iconLeft={p.secondary.icon} className="max-sm:w-full">
{p.secondary.label}
</Button>
)}
</div>
);
}
function Reference({ value, t }: { value: string; t: Texts }) {
const [copied, setCopied] = useState(false);
return (
<div className="kit-rise inline-flex items-center gap-1 rounded-xl border border-border bg-surface py-1 pe-1 ps-4" style={{ '--kit-delay': '240ms' } as React.CSSProperties}>
<span className="text-[13px] text-muted-foreground">{t.reference}</span>
<span className="ms-2 text-[15px] font-semibold tabular-nums tracking-[0.02em] text-foreground">{value}</span>
<button
type="button"
onClick={() => {
navigator.clipboard?.writeText(value).catch(() => {});
setCopied(true);
window.setTimeout(() => setCopied(false), 1800);
}}
aria-label={t.copy}
className="ms-2 grid size-9 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-[color-mix(in_oklab,var(--foreground)_6%,transparent)] hover:text-foreground focus-visible:shadow-[0_0_0_2px_color-mix(in_oklab,var(--primary)_45%,transparent)] focus-visible:outline-none max-sm:size-11"
>
<Icon name={copied ? 'check' : 'copy'} size={16} className={cn('transition-transform duration-300', copied && 'scale-110 text-success')} />
</button>
<span aria-live="polite" className="sr-only">
{copied ? t.copied : ''}
</span>
</div>
);
}
/* ------------------------------------------------------------------ */
/* next-steps */
/* ------------------------------------------------------------------ */
function NextSteps({ p, t }: { p: SuccessStatesProps; t: Texts }) {
const steps = p.steps ?? [];
return (
<div className="mx-auto max-w-[44rem] text-center">
<div className="flex justify-center">
<span className="relative">
{/* A soft ring of light breathes out once behind the check */}
<span aria-hidden className="absolute inset-0 rounded-full bg-[color-mix(in_oklab,var(--success)_30%,transparent)] motion-safe:animate-[ping_1.4s_var(--ease-kit)_1]" />
<DoneMark size={72} />
</span>
</div>
<Title className="mt-7">{p.title}</Title>
{p.description && (
<p className="kit-rise mx-auto mt-4 max-w-[34rem] text-pretty text-lead text-muted-foreground" style={{ '--kit-delay': '180ms' } as React.CSSProperties}>
{p.description}
</p>
)}
{p.reference && (
<div className="mt-7">
<Reference value={p.reference} t={t} />
</div>
)}
{steps.length > 0 && (
<div className="mt-12 text-start">
<p className="text-center text-[13px] font-medium text-muted-foreground">{t.next}</p>
{/* The steps on a rail that fills to the first while it scrolls in */}
<Stagger step={120} as="ol" className="mt-6 grid gap-3 sm:grid-cols-3 sm:gap-4">
{steps.map((s, i) => (
<li key={s.title} className={cn('relative flex gap-4 rounded-2xl border p-4 sm:block sm:p-5', i === 0 ? 'border-[color-mix(in_oklab,var(--success)_40%,var(--border))] bg-[color-mix(in_oklab,var(--success)_6%,var(--card))]' : 'border-border bg-card')}>
<span className="flex shrink-0 items-center justify-between">
<span className={cn('grid size-9 place-items-center rounded-xl', i === 0 ? 'bg-success text-background' : 'bg-surface-2 text-muted-foreground')}>{i === 0 ? <Icon name="check" size={17} strokeWidth={2.4} /> : <Icon name={s.icon ?? 'circle'} size={17} />}</span>
<span className="hidden text-[12px] font-medium tabular-nums text-subtle-foreground sm:inline">{i + 1}</span>
</span>
<span className="min-w-0 sm:block">
<span className="block text-[15px] font-semibold text-foreground sm:mt-4">{s.title}</span>
{s.text && <span className="mt-1 block text-pretty text-[13.5px] leading-relaxed text-muted-foreground">{s.text}</span>}
</span>
</li>
))}
</Stagger>
</div>
)}
<Actions p={p} center />
</div>
);
}
/* ------------------------------------------------------------------ */
/* receipt */
/* ------------------------------------------------------------------ */
function Receipt({ p, t }: { p: SuccessStatesProps; t: Texts }) {
const locale = useLocale();
const nf = new Intl.NumberFormat(locale === 'de' ? 'de-DE' : 'en-US', { style: 'currency', currency: p.currency ?? 'EUR' });
const money = (n: number) => nf.format(round(n, 2));
const total = p.total ?? (p.lines ?? []).reduce((s, l) => s + l.amount, 0);
return (
<div className="mx-auto grid max-w-[60rem] grid-cols-1 items-start gap-10 lg:grid-cols-[minmax(0,1fr)_25rem] lg:gap-14">
<div className="lg:pt-6">
<DoneMark size={60} />
<Title className="mt-6">{p.title}</Title>
{p.description && (
<p className="kit-rise mt-4 max-w-[30rem] text-pretty text-lead text-muted-foreground" style={{ '--kit-delay': '180ms' } as React.CSSProperties}>
{p.description}
</p>
)}
<div className="kit-rise mt-8 flex flex-wrap gap-3 print:hidden" style={{ '--kit-delay': '300ms' } as React.CSSProperties}>
<Button variant="outline" iconLeft="printer" onClick={() => (p.onPrint ? p.onPrint() : window.print())}>
{t.print}
</Button>
<Button variant="ghost" iconLeft="download" onClick={() => (p.onPrint ? p.onPrint() : window.print())}>
{t.save}
</Button>
</div>
<Actions p={p} />
</div>
{/* The receipt as a paper slip with a torn edge, it rises up while scrolling in */}
<Reveal effect="rise">
<div className="relative rounded-t-2xl border border-b-0 border-border bg-card px-6 pb-8 pt-6 shadow-[var(--highlight),var(--shadow-lg)] sm:px-8">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-[12px] font-medium uppercase tracking-[0.08em] text-subtle-foreground">{t.receipt}</p>
{p.reference && <p className="mt-1 text-[15px] font-semibold tabular-nums text-foreground">{p.reference}</p>}
</div>
{p.date && (
<div className="text-end">
<p className="text-[12px] text-subtle-foreground">{t.dateLabel}</p>
<p className="mt-1 text-[14px] tabular-nums text-foreground">{p.date}</p>
</div>
)}
</div>
{p.issuer && (
<p className="mt-5 text-[13px] leading-relaxed text-muted-foreground">
<span className="font-medium text-foreground">{p.issuer.name}</span>
{p.issuer.address && (
<>
<br />
{p.issuer.address}
</>
)}
{p.issuer.taxId && (
<>
<br />
{t.tax} {p.issuer.taxId}
</>
)}
</p>
)}
<dl className="mt-6 grid gap-2.5 border-t border-dashed border-border-strong pt-5 text-[14px]">
{(p.lines ?? []).map((l) => (
<div key={l.title} className="flex justify-between gap-4">
<dt className="text-muted-foreground">{l.title}</dt>
<dd className="tabular-nums text-foreground">{money(l.amount)}</dd>
</div>
))}
</dl>
<div className="mt-5 flex items-baseline justify-between border-t border-dashed border-border-strong pt-5">
<span className="text-[15px] font-semibold text-foreground">{t.total}</span>
<span className="text-[24px] font-semibold tabular-nums tracking-[-0.02em] text-foreground">{money(total)}</span>
</div>
{p.paidWith && (
<p className="mt-3 flex items-center gap-2 text-[13px] text-muted-foreground">
<Icon name="check-circle" size={15} className="text-success" />
{t.paidWith.replace('{p}', p.paidWith)}
</p>
)}
{p.note && <p className="mt-5 rounded-lg bg-surface px-3.5 py-3 text-pretty text-[12.5px] leading-relaxed text-muted-foreground">{p.note}</p>}
</div>
{/* Torn edge drawn as a row of small triangles in the card colour */}
<svg aria-hidden viewBox="0 0 400 12" preserveAspectRatio="none" className="block h-3 w-full">
<path d={`M0 0 ${Array.from({ length: 20 }, (_, i) => `L${i * 20 + 10} 12 L${i * 20 + 20} 0`).join(' ')} Z`} className="fill-card stroke-border" strokeWidth="1" vectorEffect="non-scaling-stroke" />
</svg>
</Reveal>
</div>
);
}
/* ------------------------------------------------------------------ */
/* submitted-summary */
/* ------------------------------------------------------------------ */
function SubmittedSummary({ p, t }: { p: SuccessStatesProps; t: Texts }) {
const total = (p.editMinutes ?? 10) * 60;
const [left, setLeft] = useState(total);
useEffect(() => {
const timer = window.setInterval(() => setLeft((s) => Math.max(0, s - 1)), 1000);
return () => window.clearInterval(timer);
}, []);
const share = left / total;
const minutes = Math.ceil(left / 60);
const r = 18;
const circ = round(2 * Math.PI * r, 2);
return (
<div className="mx-auto max-w-[42rem]">
<div className="flex items-start gap-4">
<DoneMark size={52} />
<div className="min-w-0">
<Title className="text-h3">{p.title}</Title>
{p.description && (
<p className="kit-rise mt-2 text-pretty text-[15px] leading-relaxed text-muted-foreground" style={{ '--kit-delay': '180ms' } as React.CSSProperties}>
{p.description}
</p>
)}
</div>
</div>
<Reveal effect="rise" className="mt-8">
<div className="kit-card overflow-hidden">
<p className="border-b border-border bg-surface px-5 py-3 text-[13px] font-medium text-muted-foreground sm:px-6">{t.sentTitle}</p>
<dl>
<Stagger step={60} as="div" className="divide-y divide-border">
{(p.fields ?? []).map((f) => (
<div key={f.label} className="grid grid-cols-1 gap-0.5 px-5 py-3 sm:grid-cols-[11rem_minmax(0,1fr)] sm:gap-4 sm:px-6">
<dt className="text-[13px] text-muted-foreground">{f.label}</dt>
<dd className="whitespace-pre-line text-pretty text-[14.5px] text-foreground">{f.value}</dd>
</div>
))}
</Stagger>
</dl>
{/* A ring runs down the minutes left to change the answers */}
<div className="flex flex-col gap-4 border-t border-border px-5 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-6">
<p className="flex items-center gap-3 text-[13.5px] text-muted-foreground">
<svg viewBox="0 0 44 44" width={32} height={32} aria-hidden className="shrink-0 -rotate-90">
<circle cx="22" cy="22" r={r} fill="none" stroke="color-mix(in oklab, var(--foreground) 12%, transparent)" strokeWidth="4" />
<circle cx="22" cy="22" r={r} fill="none" stroke="var(--primary)" strokeWidth="4" strokeLinecap="round" strokeDasharray={circ} strokeDashoffset={round(circ * (1 - share), 2)} className="transition-[stroke-dashoffset] duration-1000 ease-linear" />
</svg>
<span aria-live="polite">{left > 0 ? t.editFor.replace('{n}', String(minutes)) : t.editClosed}</span>
</p>
{left > 0 && (
<Button variant="outline" size="sm" iconLeft="edit" onClick={p.onEdit} className="max-sm:w-full">
{t.edit}
</Button>
)}
</div>
</div>
</Reveal>
<Actions p={p} />
</div>
);
}
/* ------------------------------------------------------------------ */
/* status-tracker */
/* ------------------------------------------------------------------ */
function StatusTracker({ p, t }: { p: SuccessStatesProps; t: Texts }) {
const steps = p.steps ?? [];
const [sms, setSms] = useState(false);
const current = Math.max(0, steps.findIndex((s) => s.state === 'current'));
const fill = steps.length > 1 ? current / (steps.length - 1) : 1;
return (
<div className="mx-auto grid max-w-[60rem] grid-cols-1 gap-8 lg:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] lg:gap-14">
<div className="lg:pt-4">
<DoneMark size={56} />
<Title className="mt-6 text-h3 sm:text-h2">{p.title}</Title>
{p.description && (
<p className="kit-rise mt-3 text-pretty text-[15.5px] leading-relaxed text-muted-foreground" style={{ '--kit-delay': '180ms' } as React.CSSProperties}>
{p.description}
</p>
)}
{p.reference && (
<div className="mt-6">
<Reference value={p.reference} t={t} />
</div>
)}
<Actions p={p} />
</div>
<Reveal effect="rise" className="kit-card p-6 sm:p-8">
{p.eta && (
<p className="mb-6 flex items-center gap-2.5 text-[14px] font-medium text-foreground">
<Icon name="clock" size={16} className="text-muted-foreground" />
{t.eta.replace('{e}', p.eta)}
</p>
)}
<ol className="relative grid gap-0">
<span aria-hidden className="absolute bottom-6 start-[15px] top-6 w-0.5 rounded-full bg-border" />
<span aria-hidden className="absolute start-[15px] top-6 w-0.5 origin-top rounded-full bg-success transition-transform duration-1000 ease-[var(--ease-kit)] motion-reduce:transition-none" style={{ height: 'calc(100% - 3rem)', transform: `scaleY(${round(fill, 3)})` }} />
{steps.map((s) => {
const state = s.state ?? 'later';
return (
<li key={s.title} aria-current={state === 'current' ? 'step' : undefined} className="relative flex gap-4 pb-6 last:pb-0">
<span className={cn('relative z-10 grid size-8 shrink-0 place-items-center rounded-full border-2', state === 'done' ? 'border-success bg-success text-background' : state === 'current' ? 'border-primary bg-card' : 'border-border bg-card')}>
{state === 'done' && <Icon name="check" size={15} strokeWidth={2.6} />}
{state === 'current' && (
<>
<span className="absolute inset-0 rounded-full bg-[color-mix(in_oklab,var(--primary)_25%,transparent)] motion-safe:animate-ping" />
<span className="relative size-2.5 rounded-full bg-primary" />
</>
)}
</span>
<span className="min-w-0 flex-1 pt-1">
<span className="flex flex-wrap items-baseline justify-between gap-x-3">
<span className={cn('text-[15px] font-medium', state === 'later' ? 'text-muted-foreground' : 'text-foreground')}>{s.title}</span>
{(s.when || state === 'current') && <span className={cn('text-[12.5px] tabular-nums', state === 'current' ? 'font-medium text-[color-mix(in_oklab,var(--primary)_85%,var(--foreground))]' : 'text-subtle-foreground')}>{state === 'current' ? (s.when ?? t.now) : s.when}</span>}
</span>
{s.text && <span className="mt-0.5 block text-pretty text-[13.5px] leading-snug text-muted-foreground">{s.text}</span>}
</span>
</li>
);
})}
</ol>
<div className="mt-6 border-t border-border pt-5">
<Switch label={t.sms} description={sms ? t.smsOn : t.smsText} checked={sms} onCheckedChange={setSms} />
</div>
</Reveal>
</div>
);
}
/* ------------------------------------------------------------------ */
/* Section */
/* ------------------------------------------------------------------ */
/**
* The page after a form.
* <SuccessStates variant="next-steps" title="Your request has arrived" reference="AT-2041" steps={[{ title, text, icon }]} />
*/
export function SuccessStates(props: SuccessStatesProps) {
const { variant = 'next-steps', labels, className } = props;
const t = useTexts(TEXTS, labels);
return (
<Section className={cn('overflow-hidden', className)} background={variant === 'next-steps' ? 'glow' : 'default'}>
{variant === 'next-steps' && <NextSteps p={props} t={t} />}
{variant === 'receipt' && <Receipt p={props} t={t} />}
{variant === 'submitted-summary' && <SubmittedSummary p={props} t={t} />}
{variant === 'status-tracker' && <StatusTracker p={props} t={t} />}
</Section>
);
}Frequent questions
How do I install Success states?
With aiw add @ai-website/success-states. The command puts components/ui/success-states.tsx into your project, the code is yours from then on. It brings along button, icon, reveal, section, motion, form-flow and switch. It needs no further npm packages.
Which variants does Success states have?
4 variants, Next steps, Receipt, What you sent and Status tracker. You pick one with the variant prop, for example variant="receipt".
Can I translate the texts of Success states?
Yes. Success states carries 21 texts of its own in English and German, such as “Reference”, “Copy reference” and “Copied”. The language follows the KitProvider, single texts change through the labels prop.
Can an AI assistant build with Success states?
Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Success states with aiw_components and install it with aiw_add @ai-website/success-states. Name the variant in your prompt, for example variant="next-steps", and the assistant writes your content straight into the props.
Can I adjust Success states without code?
Yes, in the Studio. All 4 variants are editable there, texts, lists and switches. The Studio writes the matching SuccessStates code to copy.