App Loading Screen

A full-screen loading shell — four rows of six segmented bars that pulse in a staggered, repeating wave under an uppercase, letter-spaced label. Built for the moment right before your app boots, when a generic spinner would feel low-rent and the bars need to read as "your product is loading" rather than "any spinner".

Two modes: an indeterminate cycling wave (the default), or a determinate fill driven by a progress percentage when you know the ETA.

Customize
Mode
indeterminate
Progress
50%

Installation

npx shadcn@latest add https://craftbits.dev/r/app-loading-screen.json

Usage

import { AppLoadingScreen } from "@craft-bits/core";
 
<AppLoadingScreen label="Loading" />

With a known progress percentage:

<AppLoadingScreen label="Loading session" progress={42} />

Understanding the component

  1. Four rows of six segments. A 4 × 6 grid of thin bars (h-1.5) inside a fixed-width column (w-48). The grid is the same DNA as the source's lesson-progress bars, but flattened to a single accent tone so the loader reads as a single visual signature instead of a track-by-track recap.
  2. Staggered wave. Each segment has a local delay of rowIndex × 0.06 + segmentIndex × 0.06 so the wave reads left-to-right, top-to-bottom. The cycle duration is 2.4s with a repeat delay matching the total stagger window, so the wave wraps cleanly.
  3. Glow pulse. Each active segment carries a 4px blur layer that ramps to 0.3 opacity at mid-cycle, then fades. The glow is suppressed under reduced motion and in determinate mode (where there's nothing to "pulse").
  4. Determinate fill. When progress is provided, the cycling animation stops and a proportional number of cells (rounded) is lit and held. Filled left-to-right, top-to-bottom across all 24 cells.
  5. Reduced motion. When prefers-reduced-motion: reduce is set, the cycling wave is suppressed and the bars render as a static muted grid (indeterminate) or a static filled subset (determinate). No JS branching beyond the early-return guard — the entire animated layer is skipped.
  6. Label is the wordmark slot. The label sits above the bars in uppercase, letter-spaced muted typography. Default is "Loading" — pass your product name to make the screen feel like part of your product instead of a generic spinner.

Props

PropTypeDefaultDescription
labelstring'Loading'Uppercase, letter-spaced caption rendered above the bars. Used as the accessible name for screen readers.
progressnumberundefinedCompletion percentage in [0, 100]. When provided, the loader switches to determinate mode (proportional fill, no cycling).
classNamestringMerged onto the root <div> via cn().

Accessibility

  • Renders as a <div role="status" aria-busy="true" aria-live="polite"> with aria-label derived from the label prop, so screen readers announce both the loading state and its caption.
  • In determinate mode the root additionally exposes aria-valuemin={0} / aria-valuemax={100} / aria-valuenow={Math.round(progress)} for fine-grained progress reporting.
  • All segment elements are aria-hidden="true" — assistive tech reads the status's accessible name, not the bar count.
  • prefers-reduced-motion: reduce short-circuits the cycling wave and glow pulse — the bars render as a static muted grid (or a static filled subset in determinate mode).
  • Color contrast: bars use --cb-accent over --cb-bg-muted. Both pass WCAG AA against the default surface in light and dark themes.

Credits

  • Extracted from: algoflashcards (src/platform/ui/AppLoadingScreen.tsx). Generalised: the source rendered four track-colored rows (sapphire / emerald / amber / amethyst) keyed to the project's skill-tree taxonomy and an "AlgoRecall" wordmark; the library version replaces both with a single accent tone and a label slot so the loader is reusable across products.