13

Content States

Loading & Empty States Builder Component

Content-region placeholders for the two ends of the data-availability spectrum. Skeletons stand in while content is loading (≥ 300ms). Empty states stand in when there's no content to show yet. Both communicate "this region is intentionally blank right now" — the right pattern depends on whether content is coming (skeleton) or not there (empty state). They are mutually exclusive: never show both in the same region at the same time.

Pick your component first: the two components are mutually exclusive — the choice depends entirely on whether data is arriving or absent.
SituationUseWhy
Data is loading — request in flight, ≥ 300ms expectedbuildSkeleton()Content is coming
Data loaded, API returned zero resultsbuildEmptyState({ cause: "no-results" })Filters reduced to zero — reset scope
Data loaded, user hasn't created anything yetbuildEmptyState({ cause: "no-data-yet" })First-run — prompt creation
Data loaded, category/scope is legitimately emptybuildEmptyState({ cause: "no-content" })Dead-end — navigate out

13.1 — Skeleton Component

Shimmer-animated placeholder shown while content loads. Rendered via buildSkeleton() — pass a shape, optional count, and layout to compose a placeholder that matches the geometry of the real content. NEVER hardcode skeleton HTML — always use the .bsn-skeleton-mount mount point. Use dark:true on Alpine/dark backgrounds.

KPI strip — shape:"kpi" · count:3 · layout:"flex-row"

Table rows — shape:"table-row" · count:4 · layout:"flex-col"

Card — shape:"card" — composed: image + heading + text + chips

Chart — shape:"chart" — composed: title + subtitle + bar area

Base elements — text, heading, avatar, button, chip, input

Text block

Avatar

Button & chips

Input

Dark surface — dark:true on Alpine background

CSS utility classesbuildSkeleton() generates these. Add .skeleton to any existing element to activate shimmer; use shape helpers for common sizes:

.skeleton-text · .skeleton-heading · .skeleton-avatar · .skeleton-avatar-sm · .skeleton-btn · .skeleton-chip · .skeleton-input · .skeleton-img · .skeleton-kpi · .skeleton-chart · .skeleton-table-row

Use .skeleton-dark on Alpine/dark surfaces. The card and chart shapes render full composed inner structures; all others repeat count times in the chosen layout.

Rules: Match skeleton geometry to actual content layout. Shimmer = 1.8s ease-in-out — never faster. Fade in real content at 200ms when replacing. Don't show skeleton for < 300ms loads (flash of loading state is worse than nothing). Never mix skeleton and spinner in the same view. Never show skeleton and empty state in the same region simultaneously. Use dark:true on Alpine backgrounds — the light shimmer has no contrast on dark surfaces.

13.2 — Empty State Component

Centered "no content yet" placeholder for empty list/browse views, empty table bodies, empty cards, and empty report zones. Rendered via buildEmptyState() — the builder composes the canonical iconography component (FA Sharp Solid at 3x in the lightest palette gray — faint / --border-muted) with a heading, optional description, and optional primary + secondary CTAs. The faint gray keeps the 48px solid icon from competing with the title for visual weight. NEVER hardcode empty-state HTML — always use the .bsn-empty-state-mount mount point.

Default — primary CTA only

With primary + secondary action

Compact — inside a card / constrained zone

Informational — no CTA

Causes — three-way decision tree New

Empty states answer a binary user question: "is this region empty because I did something, or because it is naturally empty?" Pass cause to pick the canonical icon and reinforce the right copy/action pattern. Mismatching cause and action — for example, Clear all filters on a category that legitimately has zero items — is worse than no empty state at all.

cause: "no-content"

Scope is valid but empty (no items tagged yet). Action: navigate OUT.

cause: "no-results"

Filters or search reduced results to zero. Action: reset the scope.

cause: "no-data-yet"

User/team hasn't created anything yet. Action: create one.

Icon defaults by cause: no-contentfolder-open; no-resultsfilter-circle-xmark; no-data-yet → no preset, pass a domain-specific icon (diagram-project, envelope, etc.).

Title patterns: "No content yet for {Scope}" · "No {domain noun} match these filters" · "No {nouns} yet".

Recovery actions: no-content navigates OUT of the dead-end (Back to Browse, Browse all). no-results recovers WITHIN the scope (Clear filters, Reset search). no-data-yet POPULATES the region (Create flow, Add asset).

Override rule: Explicit icon, title, description, and action always win — cause only changes the defaults.
Spec: Icon = FA Sharp Solid at 3x / 48px (Iconography §03), default color faint (--border-muted — the lightest gray in the approved palette, borrowed from the border scale because at 48px a solid icon needs the absolute minimum contrast that still passes "is visible" to anchor the empty zone without competing with the title). Title = --type-h3 · --font-weight-medium · --text-heading. Description = --type-body · --text-secondary · line-height:1.7 · max width 25rem. Action row uses standard .btn variants — primary first, secondary right (wraps below on narrow widths). Use the compact size inside cards or table bodies where the default padding would overwhelm the surrounding chrome.