Attract Phase
The opening screen of an onboarding flow. Plays a staged reveal storyboard on mount — atmospheric glow blobs, two waves of hero orbs, a layer of dim backdrop orbs, a periodic firefly pulse, then the headline / subtitle / CTA — so the user has something to watch before being asked to do anything.
Content slots are fully external. Pass title, subtitle, and cta as ReactNode. The phase owns no progression state; wire the CTA's onClick to your onboarding flow's "advance" handler (or use onAdvance as the canonical hook).
Patterns hide in every problem.
Can you see them?
Customize
Slots
Installation
npx shadcn@latest add https://craftbits.dev/r/attract-phase.jsonUsage
import { AttractPhase } from "@craft-bits/core";
function FirstScreen({ onStart }: { onStart: () => void }) {
return (
<AttractPhase
title="Patterns hide in every problem."
subtitle="Can you see them?"
cta={
<button
onClick={onStart}
className="rounded-cb-md bg-cb-accent px-10 py-3 text-cb-accent-fg"
>
Show me
</button>
}
onAdvance={onStart}
/>
);
}Compose with the top-level Onboarding shell so the phase swap is animated for free:
import { Onboarding, AttractPhase, type OnboardingPhase } from "@craft-bits/core";
const PHASES: OnboardingPhase[] = [
{
id: "attract",
label: "Welcome",
content: (ctx) => (
<AttractPhase
title="Patterns hide in every problem."
subtitle="Can you see them?"
onAdvance={ctx.advance}
cta={<button onClick={ctx.advance}>Show me</button>}
/>
),
},
];Understanding the component
- Storyboard timing. The reveal runs through six stages over 1.6s — glow blobs fade in immediately, hero orbs arrive in two staggered waves (300ms / 500ms), dim backdrop orbs at 700ms, the firefly pulse starts at 900ms, the title at 1100ms, the subtitle at 1300ms, and the CTA at 1600ms. Each stage uses
SPRINGS.*from the motion tokens — never an inline transition. - Hero vs dim orbs. The first six positions render as larger
cb-accent-tinted orbs with a soft glow; the remaining six render as smaller, lower-opacity placeholders. The contrast is intentional — the heroes pull the eye, the dims fill the canvas. - Firefly cycling. Once the dim orbs land, one hero pulses to ~1.18x scale on a fixed sequence (
[0, 3, 1, 5, 2, 4]) at 2.2s intervals. It's a signal — patterns hide everywhere; one lights up now and then to remind you. - External content. Every text slot is a
ReactNodeso the phase makes no assumptions about copy length, weight, or wrapping. Wrap the CTA in a fragment if you need both a primary button and a secondary skip link (see the demo). - Reduced motion. Under
prefers-reduced-motion: reduce, the phase short-circuits to the final stage on mount — no scale-in, no firefly pulse, no staggered reveal — and renders the same static composition.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Headline shown after the orb field. Rendered inside <h2>. |
subtitle | ReactNode | — | Supporting copy. Rendered inside <p>. |
cta | ReactNode | — | Call-to-action node anchored at the bottom (typically a <button>). |
onAdvance | () => void | — | Fires when the user wants to advance. Wire to your CTA's onClick. |
regionAriaLabel | string | "Attract phase" | Accessible name for the outer role="region" landmark. |
className | string | — | Merged onto the outer <div> via cn(). |
Accessibility
- The root renders as a
role="region"landmark with a configurablearia-labelso assistive tech can jump straight to the screen. - The orb field is
aria-hidden="true"— it's atmosphere, not content. Screen-reader users hear thetitle/subtitle/ctaonly. - The staged reveal respects
prefers-reduced-motion: reduce: the storyboard collapses to a static final-state render with no entry animation, scale-in, or firefly pulse. - The phase does NOT auto-advance. Progression is the consumer's job — wire
cta'sonClicktoonAdvance(or to whatever your flow needs) so users can take the action with the keyboard alone.
Credits
- Extracted from:
AlgoFlashcards(src/platform/ui/onboarding/AttractPhase.tsx). The source baked in track-color theming viaTRACKS.*, aGlowNodeSVG primitive, an internal stage reducer keyed onlessonId, hardcoded headline copy, and anonShowMe/onSkipdual-button contract. craft-bits' version strips all of that: orbs are token-driven (cb-accent+cb-border), every text slot becomes aReactNode, and the CTA composition is the consumer's call.