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.json

Usage

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

  1. 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.
  2. 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-muted to bg-cb-accent and back. The text colour matches. The spring is SPRINGS.snap with a 0.7s outer duration — crisp, not sluggish.
  3. 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.
  4. What the title and description carry. The chip strip is purely decorative (aria-hidden="true"). All meaning lives in the title and description props — those are what assistive tech reads aloud. The optional cta slot is where you put the action the empty state should funnel the user toward.

Variants

  • Defaulttitle + description, no CTA.
  • With CTA — pass a button or link node via the cta prop.
  • Custom labels — pass any short word array via demoLabels.
  • Slower cycleperiodMs={3600} for a calmer feel.

Props

PropTypeDefaultDescription
titleReactNodeMain heading line. Required.
descriptionReactNodeSupporting copy under the heading.
ctaReactNodeOptional CTA node rendered below the description.
demoLabelsreadonly string[]6 generic labelsLabels that cycle through the chips.
periodMsnumber2400Cycle period in ms. Floored at 400ms.
classNamestringMerged onto the root <div> via cn().

Accessibility

  • The chip strip is aria-hidden="true" — assistive tech reads the title and description instead. Put the meaningful "what's missing" text there.
  • prefers-reduced-motion: reduce stops 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 cta if the empty state should funnel toward an action.
  • Color contrast: title uses text-cb-fg, description uses text-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 a subtitle + accentHex. The library version drops accentHex (the stamp consumes --cb-accent from theme), renames subtitle to description, adds an explicit cta slot, and routes the interval through usePrefersReducedMotion instead of a bespoke hook.