Canvas Design System
Main Site Tokens

Skeleton

Holds the shape of content that hasn't arrived yet, so the layout doesn't jump when it does.

When to Use

Use when: You know the shape of what's loading and the wait is likely to be noticeable — a plan list, a card grid, a profile block. Match the real content's dimensions so nothing shifts on arrival.
Don't use when: The wait is under ~300ms (the flash is worse than the pause), the shape is unknown, or the region is empty rather than loading — use Empty State. For determinate work with a known percentage, use Progress.

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.

StateBehavior
DefaultA 1.5s ws-skeleton-shimmer loop sweeps a lighter band across the fill, left to right, forever.
Reduced motionUnder 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.
RemovalThere 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']); ?>
info When 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.

OptionTypeDefaultPurpose
variantstring'text'text | avatar | image | card | workshop | button | input
sizestring'md'sm | md | lg. Applies to avatar, button, and input; md emits no modifier class.
linesint1text only. Above 1, wraps in .ws-skeleton-text and shortens the final line.
countint1workshop only. Number of sibling cards. Floored at 1.
gridboolfalseworkshop only. Wraps the cards in .ws-skeleton-grid and moves id/attrs to that wrapper.
widthstringnullInline width override, e.g. '80%', '200px'. Ignored by card and workshop.
heightstringnullInline height override. Same exclusions as width.
roundedboolnullForces .ws-skeleton--rounded. Only true adds the class; the per-variant default applies otherwise.
circleboolfalseForces a circular shape via .ws-skeleton--circle.
idstringnullElement ID — usually the handle your JS uses to hide the placeholder.
classstring''Additional CSS classes.
attrsarray[]Extra HTML attributes as key/value pairs.

Accessibility

ConcernBehavior
ARIAEvery 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.
KeyboardNone — skeletons are non-interactive and never focusable. Nothing enters the tab order.
FocusIf you replace a focused element with a skeleton, focus is lost to <body>. Restore it to the replacement content once loaded.
Reduced motionHonored in CSS. prefers-reduced-motion: reduce removes the shimmer and renders a flat --gray-100 fill.
Announcing the waitNot 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

TokenUsed for
--gray-100Shimmer base and the flat reduced-motion fill
--gray-200Shimmer highlight band (the 50% gradient stop)
--bg-surfaceComposite card background (card, workshop)
--radius-sm / --radius-md / --radius-lg / --radius-xlCorner radii per variant
--radius-fullCircular shapes (avatar, circle)
--space-2 / --space-3 / --space-4Internal gaps in the composite variants
info Skeleton is deliberately colorless — it consumes no phase or brand token, so it reads the same in every app.

CSS Classes

ClassPurpose
.ws-skeletonBase shimmer fill — on every primitive and every part of a composite
.ws-skeleton--textText-line proportions
.ws-skeleton--text-titleTaller, wider first line inside composites
.ws-skeleton--text-shortShortened final line in a multi-line run
.ws-skeleton--avatarAvatar variant
.ws-skeleton--buttonButton variant
.ws-skeleton--inputInput variant
.ws-skeleton--imageImage variant
.ws-skeleton--sm / --md / --lgSize modifiers (md is implicit and emits nothing)
.ws-skeleton--circleCircular shape override
.ws-skeleton--roundedRounded-corner override
.ws-skeleton-textWrapper for a multi-line text run
.ws-skeleton-card / .ws-skeleton-card__bodyComposite card shell and its text block
.ws-skeleton-gridResponsive grid wrapper (grid: true)
.ws-skeleton-workshopWorkshop card shell
.ws-skeleton-workshop__metaMeta chip row
.ws-skeleton-workshop__chip / __chip--wideIndividual meta chips
.ws-skeleton-workshop__lifeLifecycle strip row
.ws-skeleton-workshop__dot / __barLifecycle nodes and connectors
.ws-skeleton-workshop__footFooter action row

Files

FilePurpose
includes/components/helpers.phpws_skeleton() helper function
includes/components/skeleton.phpTemplate — variant switch and class assembly
includes/components/components.cssStyles and the ws-skeleton-shimmer keyframes (.ws-skeleton rules)