Skeleton
Holds the shape of content that hasn't arrived yet, so the layout doesn't jump when it does.
When to Use
Variants
Seven variants. text, avatar, button, input, and image are single primitives; card and workshop are pre-composed multi-part placeholders.
Text
Passing lines greater than 1 wraps the run in .ws-skeleton-text and shortens the last line, mimicking a ragged paragraph edge.
<?= ws_skeleton(['variant' => 'text']) ?> <?= ws_skeleton(['variant' => 'text', 'lines' => 3]) ?>
Avatar
<?= ws_skeleton(['variant' => 'avatar', 'size' => 'lg']) ?>
Button
<?= ws_skeleton(['variant' => 'button', 'size' => 'md']) ?>
Input
<?= ws_skeleton(['variant' => 'input', 'size' => 'md']) ?>
Image
<?= ws_skeleton(['variant' => 'image']) ?>
Card (composite)
Emits image + title + two body lines in one call. lines, width, and height are ignored — the composite owns its own proportions.
<?= ws_skeleton(['variant' => 'card']) ?>
Custom dimensions
Reach for this only when no variant fits. width and height are emitted as inline styles, so they bypass the token scale — prefer a named variant where one exists.
<?= ws_skeleton(['width' => '100px', 'height' => '100px', 'rounded' => true]) ?> <?= ws_skeleton(['width' => '60px', 'height' => '60px', 'circle' => true]) ?>
States
Skeleton has one visual state — shimmering — and one environmental override.
| State | Behavior |
|---|---|
| Default | A 1.5s ws-skeleton-shimmer loop sweeps a lighter band across the fill, left to right, forever. |
| Reduced motion | Under prefers-reduced-motion: reduce the animation is dropped entirely and the fill becomes flat --gray-100. Handled in CSS — you don't branch for it. |
| Removal | There is no exit transition. The caller swaps the skeleton for real content; keep the replacement's dimensions identical or the swap will visibly jump. |
Real-World Usage
Workshop card — the three app homes
The placeholder Planner, Facilitator, and Synthesize show while the plan list loads. It mirrors the saved-workshop card exactly — name, meta row, lifecycle strip, footer action — so nothing shifts when the real cards arrive. count emits sibling cards that drop straight into the app's own grid; grid adds the component's own responsive wrapper for pages that don't have one.
<?php // inside an app's own grid: echo ws_skeleton(['variant' => 'workshop', 'count' => 3]); // standalone, with the component's grid: echo ws_skeleton(['variant' => 'workshop', 'count' => 3, 'grid' => true, 'id' => 'fachLoading']); ?>
grid is set, id and attrs move to the wrapper — that's the node your page toggles. Without it they land on the first card.
Composed user card
Primitives compose. Build the placeholder from the same box model as the real component rather than reaching for a custom width and height.
<?= ws_skeleton(['variant' => 'avatar', 'size' => 'md']) ?> <?= ws_skeleton(['variant' => 'text', 'width' => '80%']) ?> <?= ws_skeleton(['variant' => 'text', 'lines' => 2]) ?> <?= ws_skeleton(['variant' => 'button', 'size' => 'md']) ?>
Options
Every option is passed in a single array — ws_skeleton() takes no positional arguments.
| Option | Type | Default | Purpose |
|---|---|---|---|
variant | string | 'text' | text | avatar | image | card | workshop | button | input |
size | string | 'md' | sm | md | lg. Applies to avatar, button, and input; md emits no modifier class. |
lines | int | 1 | text only. Above 1, wraps in .ws-skeleton-text and shortens the final line. |
count | int | 1 | workshop only. Number of sibling cards. Floored at 1. |
grid | bool | false | workshop only. Wraps the cards in .ws-skeleton-grid and moves id/attrs to that wrapper. |
width | string | null | Inline width override, e.g. '80%', '200px'. Ignored by card and workshop. |
height | string | null | Inline height override. Same exclusions as width. |
rounded | bool | null | Forces .ws-skeleton--rounded. Only true adds the class; the per-variant default applies otherwise. |
circle | bool | false | Forces a circular shape via .ws-skeleton--circle. |
id | string | null | Element ID — usually the handle your JS uses to hide the placeholder. |
class | string | '' | Additional CSS classes. |
attrs | array | [] | Extra HTML attributes as key/value pairs. |
Accessibility
| Concern | Behavior |
|---|---|
| ARIA | Every node the component emits carries aria-hidden="true", including each sibling in a count run. Assistive tech skips the placeholder entirely rather than announcing meaningless boxes. |
| Keyboard | None — skeletons are non-interactive and never focusable. Nothing enters the tab order. |
| Focus | If you replace a focused element with a skeleton, focus is lost to <body>. Restore it to the replacement content once loaded. |
| Reduced motion | Honored in CSS. prefers-reduced-motion: reduce removes the shimmer and renders a flat --gray-100 fill. |
| Announcing the wait | Not handled here — because the markup is aria-hidden, screen-reader users get no signal from it. Pair a live region (aria-live="polite") with the loading state when the wait is long enough to matter. |
Tokens
| Token | Used for |
|---|---|
--gray-100 | Shimmer base and the flat reduced-motion fill |
--gray-200 | Shimmer highlight band (the 50% gradient stop) |
--bg-surface | Composite card background (card, workshop) |
--radius-sm / --radius-md / --radius-lg / --radius-xl | Corner radii per variant |
--radius-full | Circular shapes (avatar, circle) |
--space-2 / --space-3 / --space-4 | Internal gaps in the composite variants |
CSS Classes
| Class | Purpose |
|---|---|
.ws-skeleton | Base shimmer fill — on every primitive and every part of a composite |
.ws-skeleton--text | Text-line proportions |
.ws-skeleton--text-title | Taller, wider first line inside composites |
.ws-skeleton--text-short | Shortened final line in a multi-line run |
.ws-skeleton--avatar | Avatar variant |
.ws-skeleton--button | Button variant |
.ws-skeleton--input | Input variant |
.ws-skeleton--image | Image variant |
.ws-skeleton--sm / --md / --lg | Size modifiers (md is implicit and emits nothing) |
.ws-skeleton--circle | Circular shape override |
.ws-skeleton--rounded | Rounded-corner override |
.ws-skeleton-text | Wrapper for a multi-line text run |
.ws-skeleton-card / .ws-skeleton-card__body | Composite card shell and its text block |
.ws-skeleton-grid | Responsive grid wrapper (grid: true) |
.ws-skeleton-workshop | Workshop card shell |
.ws-skeleton-workshop__meta | Meta chip row |
.ws-skeleton-workshop__chip / __chip--wide | Individual meta chips |
.ws-skeleton-workshop__life | Lifecycle strip row |
.ws-skeleton-workshop__dot / __bar | Lifecycle nodes and connectors |
.ws-skeleton-workshop__foot | Footer action row |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_skeleton() helper function |
includes/components/skeleton.php | Template — variant switch and class assembly |
includes/components/components.css | Styles and the ws-skeleton-shimmer keyframes (.ws-skeleton rules) |