Zum Inhalt springen
UI Kit
Accent

Bestätigungsseiten

Die Seite nach einem Formular. Sie sagt, was passiert ist, was als Nächstes kommt, und bietet eine nützliche Handlung. Eine Vorgangsnummer mit nächsten Schritten, ein Beleg zum Drucken, das Geschickte mit Minuten zum Ändern und der aktuelle Stand einer Anfrage.

Eine von 3 Components in Formularzustände

4 Variants

Variants

Nächste Schritte

Ein Haken, der sich mit einem Lichtring zeichnet, die Vorgangsnummer zum Kopieren und die nächsten Schritte als Karten, die nacheinander aufsteigen, der erste schon abgehakt.

Im Studio öffnen

Beleg

Die Bestätigung neben einem Beleg als Papierstreifen mit Abrisskante. Aussteller, Posten, Summe, Zahlart und Steuerhinweis, fertig zum Drucken oder als PDF.

Im Studio öffnen

Was du geschickt hast

Die Antworten, wie sie ankamen, Zeile für Zeile. Ein Ring läuft die Minuten zum Ändern herunter, danach verschwindet der Knopf und die Zeile sagt, dass die Arbeit begonnen hat.

Im Studio öffnen

Statusverlauf

Wo die Anfrage gerade steht, auf einer Schiene, die sich bis zum aktuellen Schritt füllt, der leise pulsiert. Eine Schätzung oben, Updates per SMS mit einem Schalter.

Im Studio öffnen

Details

Was Bestätigungsseiten mitbringt, aus Registry und Quellcode gelesen.

Bereich
Content
Variants
4
Version
1.0.0 · seit 1.1.0
Datei
components/ui/success-states.tsx
Bringt mit
utilskitbuttoniconrevealsectionmotionform-flowswitch
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 21 eigene Texte

Props

Die Schnittstelle SuccessStatesProps aus components/ui/success-states.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • variantoptional
    SuccessStatesVariant

    next-stepsreceiptsubmitted-summarystatus-tracker

  • titlePflicht
    React.ReactNode
  • descriptionoptional
    React.ReactNode
  • referenceoptional
    string

    A reference people can quote, e.g. an order or ticket number

  • stepsoptional
    SuccessStep[]
  • linesoptional
    { title: string; amount: number }[]

    receipt: lines, total, how it was paid, who paid and a note

  • totaloptional
    number
  • currencyoptional
    string
  • paidWithoptional
    string
  • dateoptional
    string
  • issueroptional
    { name: string; address?: string; taxId?: string }
  • noteoptional
    string
  • fieldsoptional
    { label: string; value: string }[]

    submitted-summary: what was sent, as label and value

  • editMinutesoptional
    number

    submitted-summary: minutes left to change it

  • etaoptional
    string

    status-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
    () => void
  • onPrintoptional
    () => void

Eingebaute Texte

Diese Texte schreibt Bestätigungsseiten selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 21 Texte anzeigen
referenceEnglisch ReferenceDeutsch Vorgangsnummer
copyEnglisch Copy referenceDeutsch Nummer kopieren
copiedEnglisch CopiedDeutsch Kopiert
nextEnglisch What happens nextDeutsch So geht es weiter
receiptEnglisch ReceiptDeutsch Beleg
paidWithEnglisch Paid with {p}Deutsch Bezahlt mit {p}
totalEnglisch TotalDeutsch Gesamt
printEnglisch PrintDeutsch Drucken
saveEnglisch Save as PDFDeutsch Als PDF speichern
sentTitleEnglisch What you sentDeutsch Das hast du geschickt
editForEnglisch You can change it for {n} more minutesDeutsch Du kannst es noch {n} Minuten ändern
editClosedEnglisch Changes are closed, we are already on itDeutsch Änderungen sind geschlossen, wir sind schon dran
editEnglisch Change my answersDeutsch Antworten ändern
nowEnglisch NowDeutsch Jetzt
etaEnglisch Expected {e}Deutsch Voraussichtlich {e}
smsEnglisch Send me updates by textDeutsch Updates per SMS schicken
smsTextEnglisch One message per step, nothing elseDeutsch Eine Nachricht je Schritt, sonst nichts
smsOnEnglisch You get a text at every step.Deutsch Du bekommst bei jedem Schritt eine SMS.
dateLabelEnglisch DateDeutsch Datum
fromEnglisch FromDeutsch Von
taxEnglisch Tax IDDeutsch Steuernummer

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Setzt aria-current, aria-label und aria-live, damit Screenreader Zweck und Zustand ansagen.
  • Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
  • Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 462 Zeilen
components/ui/success-states.tsx
'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>
  );
}

Häufige Fragen

Wie installiere ich Bestätigungsseiten?

Mit aiw add @ai-website/success-states. Der Befehl legt die Datei components/ui/success-states.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, icon, reveal, section, motion, form-flow und switch. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Bestätigungsseiten?

4 Varianten, Nächste Schritte, Beleg, Was du geschickt hast und Statusverlauf. Du wählst sie mit der Prop variant, zum Beispiel variant="receipt".

Kann ich die Texte von Bestätigungsseiten übersetzen?

Ja. Bestätigungsseiten bringt 21 eigene Texte auf Deutsch und Englisch mit, etwa „Vorgangsnummer“, „Nummer kopieren“ und „Kopiert“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Bestätigungsseiten einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Bestätigungsseiten mit aiw_components und installieren sie mit aiw_add @ai-website/success-states. Nenn im Prompt die Variante, etwa variant="next-steps", dann schreibt der Assistent deine Inhalte direkt in die Props.

Kann ich Bestätigungsseiten ohne Code anpassen?

Ja, im Studio. Alle 4 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden SuccessStates-Code zum Kopieren.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert