Empty State Loop
An empty state that teaches by looping. Three chips cycle through your demoLabels, and the middle chip "stamps" with the accent color once per tick — a tiny demo of what tagging, picking, or naming feels like in your product. Sits well at the top of an empty list, an empty board, or the first launch of a new feature.
No entries yet
Tag what you see, type, and save.
Customize
Labels
mechanics / content…
Timing
2400ms
Content
Installation
npx shadcn@latest add https://craftbits.dev/r/empty-state-loop.jsonUsage
import { EmptyStateLoop } from "@craft-bits/core";
<EmptyStateLoop
title="No entries yet"
description="Tag what you see, type, and save."
/>With your own labels and a CTA:
<EmptyStateLoop
title="No tags yet"
description="Tap a chip to start."
demoLabels={["bug", "copy", "design", "pedagogy"]}
cta={<button onClick={onAdd}>Add the first one</button>}
/>Understanding the component
- Three chips, in-place cycle. The strip always renders three chips. On every tick the labels shift one position to the left; the leftmost label rotates back in on the right. Each tick reuses the same DOM nodes — only the text content changes.
- The middle chip stamps. Once per tick the middle chip animates its scale in a quick down-then-overshoot curve while its background morphs from
bg-cb-bg-mutedtobg-cb-accentand back. The text colour matches. The spring isSPRINGS.snapwith a 0.7s outer duration — crisp, not sluggish. - Reduced motion.
usePrefersReducedMotion()short-circuits the cycle and the stamp — the chips render statically with the first three labels. No JS branching beyond the hook itself; the same DOM, just no animation. - What the title and description carry. The chip strip is purely decorative (
aria-hidden="true"). All meaning lives in thetitleanddescriptionprops — those are what assistive tech reads aloud. The optionalctaslot is where you put the action the empty state should funnel the user toward.
Variants
- Default —
title+description, no CTA. - With CTA — pass a button or link node via the
ctaprop. - Custom labels — pass any short word array via
demoLabels. - Slower cycle —
periodMs={3600}for a calmer feel.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | ReactNode | — | Main heading line. Required. |
description | ReactNode | — | Supporting copy under the heading. |
cta | ReactNode | — | Optional CTA node rendered below the description. |
demoLabels | readonly string[] | 6 generic labels | Labels that cycle through the chips. |
periodMs | number | 2400 | Cycle period in ms. Floored at 400ms. |
className | string | — | Merged onto the root <div> via cn(). |
Accessibility
- The chip strip is
aria-hidden="true"— assistive tech reads thetitleanddescriptioninstead. Put the meaningful "what's missing" text there. prefers-reduced-motion: reducestops the cycle and the stamp. No flash, no late JS branching — the hook returns synchronously after first paint and the effect simply never schedules an interval.- The component is non-interactive by default. Pass a focusable element (button, link) via
ctaif the empty state should funnel toward an action. - Color contrast: title uses
text-cb-fg, description usestext-cb-fg-muted; both meet WCAG AA on the default surface.
Credits
- Extracted from:
algoflashcards(src/platform/ui/EmptyStateLoop.tsx). The source was wired to the project's sapphire-by-default semantic palette and accepted asubtitle+accentHex. The library version dropsaccentHex(the stamp consumes--cb-accentfrom theme), renamessubtitletodescription, adds an explicitctaslot, and routes the interval throughusePrefersReducedMotioninstead of a bespoke hook.