Stepper
Where a reader is in a task of a few steps. A done step carries a tick, not just a colour, so it survives a greyscale screenshot.
server - 0 KB
Stepper
server - 0 KBWhere a reader is in a task of a few steps. A done step carries a tick, not just a colour, so it survives a greyscale screenshot.
- AccountCompleted
- Profile
- TeamNot started
- DoneNot started
Try it
size
The code
stepper.tsx
import { Stepper, type StepperDesignProps } from 'topclassui/stepper'
<Stepper items={STEPS} value="profile" aria-label="Sign-up" />Stepper renders on the server: pass value, the id of the current step. It shows progress; it does not navigate.
Every state
4 states- Cart
- AddressNot started
- PaymentNot started
- CartCompleted
- Address
- PaymentNot started
- CartCompleted
- AddressNeeds attention
- PaymentNot started
- CartCompleted
- AddressCompleted
- PaymentCompleted
Import from the package
topclassui/stepper
import { Stepper } from 'topclassui/stepper'Install once:npm i topclassui@beta zod
Examples
- A step is
{ id, title, status? }, withstatusone ofdone,current,todoanderror. - Steps before
valueare done, steps after it todo. With novaluethe first step is current; avalueno step has leaves every step todo. - A step's own
statuswins overvalue:errordraws an x and is read as "Needs attention". sizesets the circles, from 24 to 48 px, and the titles. Withcustomthe circle reads--tc-stepper-mark, 32 px unless you set it.- A long title wraps inside its column and is never cut. Steps that do not fit across wrap onto another row, so the page never scrolls sideways.
basic
- AccountCompleted
- Profile
- TeamNot started
- DoneNot started
basic.tsx
import { Stepper } from 'topclassui/stepper'
const STEPS = [
{ id: 'account', title: 'Account' },
{ id: 'profile', title: 'Profile' },
{ id: 'team', title: 'Team' },
{ id: 'done', title: 'Done' },
]
/** Four steps, the second one current, as drawn: a tick, a number with a fill, two with a line. */
export default function Demo() {
return <Stepper items={STEPS} value="profile" aria-label="Sign-up" />
}
statuses
- UploadCompleted
- CheckNeeds attention
- Publish
- ShareNot started
statuses.tsx
import { Stepper } from 'topclassui/stepper'
const STEPS = [
{ id: 'upload', title: 'Upload' },
{ id: 'check', title: 'Check', status: 'error' },
{ id: 'publish', title: 'Publish' },
{ id: 'share', title: 'Share' },
] as const
/** All four statuses: a step's own status wins over the one worked out from value. */
export default function Demo() {
return <Stepper items={STEPS} value="publish" aria-label="Publishing" />
}
sizes
- CartCompleted
- Address
- PaymentNot started
- CartCompleted
- Address
- PaymentNot started
- CartCompleted
- Address
- PaymentNot started
- CartCompleted
- Address
- PaymentNot started
- CartCompleted
- Address
- PaymentNot started
sizes.tsx
import { Stepper } from 'topclassui/stepper'
const STEPS = [
{ id: 'cart', title: 'Cart' },
{ id: 'address', title: 'Address' },
{ id: 'payment', title: 'Payment' },
]
/** Circles of 24, 28, 32, 40 and 48 px; the connector stays level with the centres at each. */
export default function Demo() {
return (
<div className="grid gap-8">
<Stepper size="xs" items={STEPS} value="address" aria-label="Extra small" />
<Stepper size="sm" items={STEPS} value="address" aria-label="Small" />
<Stepper size="md" items={STEPS} value="address" aria-label="Medium" />
<Stepper size="lg" items={STEPS} value="address" aria-label="Large" />
<Stepper size="xl" items={STEPS} value="address" aria-label="Extra large" />
</div>
)
}
custom-size
- UploadCompleted
- CheckNeeds attention
- Publish
- ShareNot started
- UploadCompleted
- CheckNeeds attention
- Publish
- ShareNot started
custom-size.tsx
import { Stepper } from 'topclassui/stepper'
const STEPS = [
{ id: 'upload', title: 'Upload' },
{ id: 'check', title: 'Check', status: 'error' },
{ id: 'publish', title: 'Publish' },
{ id: 'share', title: 'Share' },
] as const
/** custom sets no circle size: the circle reads --tc-stepper-mark, 32 px unless the caller sets it. */
export default function Demo() {
return (
<div className="grid gap-8">
<Stepper size="custom" items={STEPS} value="publish" aria-label="The default circle" />
<Stepper size="custom" items={STEPS} value="publish" className="text-lg" style={{ '--tc-stepper-mark': '3.5rem' }} aria-label="Your own circle" />
</div>
)
}
long-content
- Confirm the billing address on the orderCompleted
- Choose a delivery slot for the big itemsCompleted
- Add the gift note and the wrapping paper
- Review every line of the order, then payNot started
- Pay with a saved card or a bank transferNot started
- Download the receipt and tracking numberNot started
long-content.tsx
import { Stepper } from 'topclassui/stepper'
const STEPS = [
{ id: 'one', title: 'Confirm the billing address on the order' },
{ id: 'two', title: 'Choose a delivery slot for the big items' },
{ id: 'three', title: 'Add the gift note and the wrapping paper' },
{ id: 'four', title: 'Review every line of the order, then pay' },
{ id: 'five', title: 'Pay with a saved card or a bank transfer' },
{ id: 'six', title: 'Download the receipt and tracking number' },
]
/** Six steps of forty characters: each title wraps inside its own column and the page never scrolls sideways. */
export default function Demo() {
return <Stepper items={STEPS} value="three" aria-label="Checkout" />
}
arabic-digits
- السلةمكتمل
- العنوانمكتمل
- الدفع
- التأكيدلم يبدأ
- السلةمكتمل
- العنوانمكتمل
- الدفع
- التأكيدلم يبدأ
arabic-digits.tsx
import { getLabels } from 'topclassui/labels/get-labels'
import { Stepper } from 'topclassui/stepper'
const STEPS = [
{ id: 'cart', title: 'السلة' },
{ id: 'address', title: 'العنوان' },
{ id: 'payment', title: 'الدفع' },
{ id: 'confirm', title: 'التأكيد' },
]
/** The locale decides the digits: ar-u-nu-arab writes Arabic-Indic numbers, plain ar Latin ones. */
export default function Demo() {
return (
<div dir="rtl" lang="ar" className="grid gap-8">
<Stepper items={STEPS} value="payment" aria-label="أرقام عربية هندية" {...getLabels('ar-u-nu-arab')} />
<Stepper items={STEPS} value="payment" aria-label="أرقام لاتينية" {...getLabels('ar')} />
</div>
)
}
In Arabic, right to left
- الحسابمكتمل
- الملف الشخصي
- الفريقلم يبدأ
- تملم يبدأ
rtl.tsx
import { getLabels } from 'topclassui/labels/get-labels'
import { TopClassProvider } from 'topclassui/provider'
import { Stepper } from 'topclassui/stepper'
const STEPS = [
{ id: 'account', title: 'الحساب' },
{ id: 'profile', title: 'الملف الشخصي' },
{ id: 'team', title: 'الفريق' },
{ id: 'done', title: 'تم' },
]
/** Arabic, inside TopClassProvider as the root layout would mount it. The first step is at
* the right and each connector runs leftward, with no class of its own for it. */
export default function Demo() {
return (
<TopClassProvider direction="rtl" locale="ar">
<div dir="rtl" lang="ar">
<Stepper items={STEPS} value="profile" aria-label="التسجيل" {...getLabels('ar')} />
</div>
</TopClassProvider>
)
}
Props
Keyboard and accessibility
Nothing to operate: it is not interactive on its own.
- An ordered list, which gives "2 of 4", with
role="list"written so Safari with VoiceOver keeps it. - The current step carries
aria-current="step"; every other step has a hidden word for its status, fromlabels. - The circles are hidden from screen readers, and nothing takes focus.
Right to left
The steps run from the right by themselves. locale decides the digits: ar-u-nu-arab writes the numbers in Arabic-Indic digits.
Server and client
Renders on the server and ships no JavaScript.
Files: index.tsx.
On the chip: server - 0 KB.