Onboard Card
A looping onboarding progress card with done, in-progress and pending steps; the active step's bar fills over a configurable duration via a CSS transition, with role=progressbar and reduced-motion support.
Preview
-
Account Created
-
Verifying Details
-
Setting Up Workspace
<div class="w-full flex justify-center p-6">
<x-ui.onboard-card />
</div>
Installation
php artisan ui:add onboard-card
Note
This component ships an Alpine behavior module at
resources/js/ui/onboard-card.js. Import it once from your bundle so it registers on alpine:init:
import './onboard-card.js';
Registry contract
php artisan ui:add onboard-card
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/onboard-card.blade.php -
resources/js/ui/onboard-card.js
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2npm: alpinejs
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Onboard Card (`onboard-card`)
A looping onboarding progress card with done, in-progress and pending steps; the active step's bar fills over a configurable duration via a CSS transition, with role=progressbar and reduced-motion support.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add onboard-card
```
## Usage
```blade
<div class="w-full flex justify-center p-6">
<x-ui.onboard-card />
</div>
```
## Props
- `done` (string, default `Account Created`) — Label for the completed step.
- `active` (string, default `Verifying Details`) — Label for the in-progress step.
- `pending` (string, default `Setting Up Workspace`) — Label for the not-yet-started step.
- `duration` (int, default `3000`) — Fill time of the active step's progress bar, in milliseconds (clamped to a 200ms minimum).
## Use when
- Use to show state, status, notification, or lightweight social feedback near the relevant object.
- Showing a looping done, active, and pending onboarding checklist with an animated progress bar on the active step.
- Filling a sidebar or empty state with a self-contained widget that visualizes setup progress without wiring a real percentage.
## Avoid when
- Do not overload small status elements with explanations that should remain visible in text.
- Tracking a real, server-driven progress value; use progress instead.
- Showing more than three steps or arbitrary step content; this component has a fixed done/active/pending shape.
## Anti-patterns
- Using color or icon alone to communicate state
## Rules
- Use the `<brok:onboard-card>` tag (or `<x-ui.onboard-card>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/onboard-card
- Registry JSON (files, props, contract): https://brokui.dev/r/open/onboard-card.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| done | string | Account Created | Label for the completed step. |
| active | string | Verifying Details | Label for the in-progress step. |
| pending | string | Setting Up Workspace | Label for the not-yet-started step. |
| duration | int | 3000 | Fill time of the active step's progress bar, in milliseconds (clamped to a 200ms minimum). |
Slots
default— Unused; the component has no visible default slot content.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- The active step's bar fills from 0 to 100 percent via a CSS transition timed to duration; onboard-card.js drives the width and aria-valuenow.
- Reduced motion keeps the loading spinner static and disables the fill transition; the card fades its step list into its top and bottom edges.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized, reducedMotion.
Guidance
Communicate concise object or system state.
Use when
- Use to show state, status, notification, or lightweight social feedback near the relevant object.
- Showing a looping done, active, and pending onboarding checklist with an animated progress bar on the active step.
- Filling a sidebar or empty state with a self-contained widget that visualizes setup progress without wiring a real percentage.
Avoid when
- Do not overload small status elements with explanations that should remain visible in text.
- Tracking a real, server-driven progress value; use progress instead.
- Showing more than three steps or arbitrary step content; this component has a fixed done/active/pending shape.
Use instead
- Persistent explanatory text
Anti-patterns
- Using color or icon alone to communicate state
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- managed
- Focus
none
- The active step's fill carries role="progressbar" with aria-valuemin/aria-valuemax/aria-valuenow and an aria-label built from the active step's text.
- The step list uses role="list" so screen readers announce it as a group of steps.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="onboard-card-{{ $record->id }}">
<div class="w-full flex justify-center p-6">
<x-ui.onboard-card />
</div>
</div>
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
// Labels for the three steps (done → active → pending).
'done' => 'Account Created',
'active' => 'Verifying Details',
'pending' => 'Setting Up Workspace',
// Fill time of the active step's progress bar, in milliseconds.
'duration' => 3000,
])
@php
$duration = max(200, (int) $duration);
$loader = 'M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83';
@endphp
{{-- Onboard Card: a looping onboarding progress card with done / active /
pending steps. The active bar fills over `duration` via a CSS transition;
reduced motion shows it full and static. The stack fades into the card top
and bottom. --}}
<div
data-slot="onboard-card"
x-data="uiOnboardCard({ duration: {{ $duration }} })"
{{ $attributes->merge(['class' => 'relative mx-auto flex w-full max-w-xs flex-col gap-2 overflow-hidden rounded-md border border-border bg-card p-3 text-card-foreground']) }}
>
<ul role="list" class="flex flex-col gap-2">
{{-- Done --}}
<li class="flex flex-col gap-2 rounded-md border border-border bg-muted/40 px-3 py-2">
<div class="flex items-center gap-2 text-xs text-foreground">
<span class="flex size-4 items-center justify-center rounded-full bg-primary text-primary-foreground">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="size-2" aria-hidden="true">
<path d="m20 6-11 11-5-5" />
</svg>
</span>
<span>{{ __($done) }}</span>
</div>
<div class="ms-6 h-2 overflow-hidden rounded-full bg-primary"></div>
</li>
{{-- Active --}}
<li class="flex flex-col gap-2 rounded-md border border-border bg-muted/40 px-3 py-2">
<div class="flex items-center gap-2 text-xs text-foreground">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4 animate-spin text-muted-foreground motion-reduce:animate-none" aria-hidden="true">
<path d="{{ $loader }}" />
</svg>
<span>{{ __($active) }}</span>
</div>
<div class="ms-6 h-2 overflow-hidden rounded-full bg-muted">
<div
class="h-full rounded-full bg-primary transition-[width] ease-in-out motion-reduce:transition-none"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
:aria-valuenow="progress"
aria-label="{{ __($active) }}"
:style="`width:${progress}%;transition-duration:{{ $duration }}ms`"
></div>
</div>
</li>
{{-- Pending --}}
<li class="flex flex-col gap-2 rounded-md border border-border bg-muted/40 px-3 py-2">
<div class="flex items-center gap-2 text-xs text-muted-foreground">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true">
<path d="{{ $loader }}" />
</svg>
<span>{{ __($pending) }}</span>
</div>
<div class="ms-6 h-2 overflow-hidden rounded-full bg-muted"></div>
</li>
</ul>
{{-- Edge fades into the card surface. --}}
<div class="pointer-events-none absolute inset-x-0 top-0 h-6 bg-linear-to-b from-card to-transparent" aria-hidden="true"></div>
<div class="pointer-events-none absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-card to-transparent" aria-hidden="true"></div>
</div>
/**
* Onboard Card behaviour (looping onboarding progress).
*
* Three stacked steps — done, in-progress, pending — where the active step's bar
* fills from 0 to 100% over `duration`, holds, then the cycle repeats. The fill
* is a CSS width transition (we only flip `progress` between 0 and 100 and set
* the transition-duration), so there is no per-frame work.
*
* The bar exposes role="progressbar" + aria-valuenow in the Blade. Reduced motion
* paints the bar full once and never starts the loop. destroy() clears the
* timers so unmounting mid-cycle can't leak.
*
* Self-registers on `alpine:init` so import order does not matter.
*
* Note: this file is plain JS served by the registry — no Blade syntax allowed.
*/
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiOnboardCard', (config = {}) => ({
duration: Math.max(200, config.duration ?? 3000),
progress: 0,
_t1: null,
_t2: null,
_reduced: false,
init() {
this._reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (this._reduced) {
this.progress = 100;
return;
}
this.cycle();
},
cycle() {
this.progress = 0;
// Next tick so the transition runs from 0; then hold + repeat.
this._t1 = setTimeout(() => {
this.progress = 100;
}, 100);
this._t2 = setTimeout(() => this.cycle(), this.duration + 2000);
},
destroy() {
clearTimeout(this._t1);
clearTimeout(this._t2);
this._t1 = null;
this._t2 = null;
},
}));
});
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.0.4 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0