Content States
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.
| Situation | Use | Why |
|---|---|---|
| Data is loading — request in flight, ≥ 300ms expected | buildSkeleton() | Content is coming |
| Data loaded, API returned zero results | buildEmptyState({ cause: "no-results" }) | Filters reduced to zero — reset scope |
| Data loaded, user hasn't created anything yet | buildEmptyState({ cause: "no-data-yet" }) | First-run — prompt creation |
| Data loaded, category/scope is legitimately empty | buildEmptyState({ 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
buildSkeleton() 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
.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.
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.
no-content → folder-open; no-results → filter-circle-xmark; no-data-yet → no preset, pass a domain-specific icon (diagram-project, envelope, etc.).
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).
icon, title, description, and action always win — cause only changes the defaults.
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.