Skeleton
Loading placeholder that matches the shape of the content it's replacing. Settld skeletons use a warm-tinted shimmer — never gray — so they feel native to the cream palette. Shapes: text line, avatar circle, card rect, amount rect, button pill, chip pill. Shimmer sweeps left to right at 1.5s with a subtle butter-tinted peak.
Variants
Text
Heading
Avatar
Amount
Card
Button & chip
Text — use varying widths (50–90%) for lines in a text block. Never use the same width — it reads as a single word, not a natural line break pattern.
Avatar — circle shapes (50% border-radius) for member avatars and profile pictures.
Amount — wider and taller rects with a slightly larger border-radius. Used when replacing the primary amount display in expense cards and the settle-up header.
Card — full-width rect for expense rows, group cards, and summary cards. Height matches the real card's height.
Button / chip — pill shape for loading CTA buttons and person chips.
Sizes
| Variant | Modifier | Dimensions | Matches |
|---|---|---|---|
| Text | .skeleton--text-sm | 11px tall | Caption / footnote text |
| Text | .skeleton--text | 14px tall | Body text (default) |
| Text | .skeleton--text-lg | 18px tall | Large body / subheadings |
| Heading | .skeleton--heading | 24px tall | Section headings, card titles |
| Avatar | .skeleton--avatar-sm | 28×28px | Person chip avatar |
| Avatar | .skeleton--avatar | 40×40px | List item avatar |
| Avatar | .skeleton--avatar-lg | 56×56px | Profile screen avatar |
| Amount | .skeleton--amount | 32px tall, 120px wide | Expense card amount |
| Amount | .skeleton--amount-lg | 44px tall, 160px wide | Settle-up header amount |
| Card | .skeleton--card | 80px tall, full width | Expense row card |
| Card | .skeleton--card-lg | 120px tall, full width | Group summary card |
| Chip | .skeleton--chip | 36px tall, 80px wide | Person chip |
| Button | .skeleton--btn | 44px tall, 120px wide | CTA button |
| Button | .skeleton--btn-block | 44px tall, 100% wide | Full-width action button |
States
Loading (shimmer active)
Reduced motion (static, no shimmer)
| State | Visual | When |
|---|---|---|
| Loading | Warm shimmer sweeping left to right | Data fetch in progress |
| Reduced motion | Static warm tint, no animation | prefers-reduced-motion: reduce |
| Resolved | Skeleton removed, real content fades in | Data loaded |
On dark surfaces
On dark surfaces, the skeleton base uses --dark-border and the shimmer is a lighter translucent white wash. The warm butter tint in the shimmer peak is more subtle on dark — just a hint of warmth, not full butter yellow.
Composition — expense list
Compose shape primitives to match the real layout. The expense list row uses an avatar circle, two text lines (name + date/group), and an amount rect. Vary widths on text lines — never repeat the same width twice in a row.
Composition — group header
Anatomy
| Part | Element | Notes |
|---|---|---|
| Base | div.skeleton | --s-deep background, overflow: hidden, shape via modifier class. |
| Shimmer | ::after pseudo-element | Absolute inset. Warm gradient sweep. 1.5s ease infinite. Paused on prefers-reduced-motion. |
Usage
Accessibility
aria-busy="true" while loading. Once data loads and skeletons are replaced, set aria-busy="false". Screen readers will announce "busy" and then "not busy." Add an aria-label like "Loading expenses" to the container.
prefers-reduced-motion override — users with vestibular disorders find continuously sweeping animations distressing. The static fallback (background: rgba(251,248,240,0.2) on ::after) still communicates "loading" without motion.
aria-hidden="true" on individual skeleton elements if they're inside a container that isn't aria-busy.
Code
HTML
<!-- Loading container --> <div aria-busy="true" aria-label="Loading expenses"> <!-- Expense row skeleton --> <div class="skeleton-row" aria-hidden="true"> <div class="skeleton skeleton--avatar"></div> <div class="skeleton-stack" style="flex:1"> <div class="skeleton skeleton--text" style="width:65%"></div> <div class="skeleton skeleton--text skeleton--text-sm" style="width:40%"></div> </div> <div class="skeleton skeleton--amount"></div> </div> <!-- Repeat for expected number of rows --> </div> <!-- Full-width button skeleton --> <div class="skeleton skeleton--btn-block" aria-hidden="true"></div>
CSS classes
| Class | Purpose |
|---|---|
.skeleton | Base — warm bg, overflow hidden, shimmer ::after |
.skeleton--text | 14px tall text line |
.skeleton--text-sm | 11px tall small text line |
.skeleton--text-lg | 18px tall large text line |
.skeleton--heading | 24px tall heading |
.skeleton--avatar | 40px circle |
.skeleton--avatar-sm | 28px circle |
.skeleton--avatar-lg | 56px circle |
.skeleton--amount | 32×120px wider rect |
.skeleton--amount-lg | 44×160px large amount |
.skeleton--card | 80px tall full-width card |
.skeleton--card-lg | 120px tall full-width card |
.skeleton--chip | 36×80px pill chip |
.skeleton--btn | 44×120px pill button |
.skeleton--btn-block | 44px tall, full-width pill |
.skeleton-row | Flex row with 12px gap |
.skeleton-stack | Flex column with 8px gap |
Design tokens used
| Token | Value | Role |
|---|---|---|
--s-deep | #E8DEC5 | Skeleton base color (warm, not gray) |
--dark-border | #3D3630 | Dark surface skeleton base |
--butter | #F4C94E | Warm tint in shimmer peak |
--r-xs | 6px | Text/heading border radius |
--r-sm | 10px | Amount border radius |
--r-md | 14px | Card border radius |
--r-pill | 999px | Chip/button border radius |