Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .specs/skeleton.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ spec_version: 1
figma:
url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=479-881
node_id: 479:881
checksum: a3ba11ff0d67cde98427376c7f4f5e8d958933bcaced577819fc26e721e03fe5
checksum: 5ebe624103f06c4dcdc3fac32789b74cc7fd48142bd277515151e8c6d44e5423
created: 2026-06-23
last_updated: 2026-06-24
last_updated: 2026-06-26
---

# Skeleton — Component Spec
Expand Down Expand Up @@ -38,7 +38,7 @@ import Skeleton from '@aziontech/webkit/skeleton'
| `kind` | `'shape' \| 'circle'` | `'shape'` | no | Geometry: a rounded rectangular block (`shape`) or a circle. |
| `width` | `string` | `'100%'` | no | CSS width (any length). |
| `height` | `string` | `'1rem'` | no | CSS height (any length); for a circle, set equal to `width`. |
| `animated` | `boolean` | `true` | no | Pulse while loading; suppressed under reduced motion. |
| `animated` | `boolean` | `true` | no | Shimmer while loading; suppressed under reduced motion. |

## Events

Expand All @@ -52,13 +52,13 @@ import Skeleton from '@aziontech/webkit/skeleton'

- Visual states: `default` (a single decorative placeholder; no hover/focus/active — it is not interactive)
- `data-kind` mirrors the `kind` prop (`shape` | `circle`)
- `data-animated` is present when `animated` is true (drives the pulse)
- `data-animated` is present when `animated` is true (drives the shimmer)

## Motion & Animations

| Trigger | Animation / Transition | Token (see `.claude/docs/DESIGN.md` § Animations) | Reduced-motion fallback |
|---|---|---|---|
| while loading (`animated`) | `animate-pulse` | theme gap (see Theme gaps) — closest looping opacity primitive | `motion-reduce:animate-none` (static) |
| while loading (`animated`) | `animate-shimmer` (linear gradient sweep over the fill) | `--animate-shimmer` (`animate.js`) + `@keyframes shimmer` (`semantic/animations.js`) | gated behind `motion-safe:` (no sweep) + `motion-reduce:animate-none` (static) |

## Tokens

Expand All @@ -71,21 +71,21 @@ import Skeleton from '@aziontech/webkit/skeleton'
| Figma variable | Temporary primitive | Follow-up |
|---|---|---|
| `--bg-surface-overlay` (skeleton fill, #4D4D4D dark / #FAFAFA light) | `bg-[var(--bg-surface-overlay)]` (real token, not yet in DESIGN.md) | `TODO: document --bg-surface-overlay in DESIGN.md` |
| pulse animation | `animate-pulse` (Tailwind primitive, present in the compiled theme) | `TODO: add a semantic skeleton/pulse animation to semantic/animations.js` |
| shimmer animation | `animate-shimmer` (`--animate-shimmer` preset in `animate.js` + `@keyframes shimmer` in `semantic/animations.js`) | `TODO: document --animate-shimmer in DESIGN.md § Animations` |

## Accessibility (WCAG 2.1 AA)

- The skeleton is decorative: `aria-hidden="true"` so assistive tech skips it. The loading status is conveyed by the surrounding region (e.g. `aria-busy="true"` on the container the consumer owns), not by the placeholder itself.
- Not focusable, not interactive: no keyboard map, no focus ring.
- `motion-reduce:animate-none` suppresses the pulse for users who prefer reduced motion.
- The shimmer is gated behind `motion-safe:` and `motion-reduce:animate-none` suppresses it for users who prefer reduced motion (static flat fill).

## Stories (Storybook)

This component has no `size` axis, so the canonical Sizes story does not apply.

- Default
- Types — composite story rendering both `kind` values (`shape`, `circle`) side-by-side.
- Static — `animated: false`; demonstrates the non-pulsing placeholder. Justified because `animated` is a distinct state of the component and the pulse cannot be seen in a static screenshot.
- Static — `animated: false`; demonstrates the non-shimmering placeholder. Justified because `animated` is a distinct state of the component and the shimmer cannot be seen in a static screenshot.

## Constraints — DO NOT

Expand Down
1 change: 1 addition & 0 deletions packages/theme/dist/v3/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@
--animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-bounce: bounce 1s infinite;
--animate-shimmer: shimmer 1.6s linear infinite;
--animate-popup-scale-in: popupScaleIn 150ms cubic-bezier(0.39, 0.57, 0.56, 1);
--animate-popup-scale-out: popupScaleOut 110ms cubic-bezier(0.55, 0.09, 0.68, 0.53);
--ring-offset: 0.5px;
Expand Down
1 change: 1 addition & 0 deletions packages/theme/dist/v3/globals.scss
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@
--animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-bounce: bounce 1s infinite;
--animate-shimmer: shimmer 1.6s linear infinite;
--animate-popup-scale-in: popupScaleIn 150ms cubic-bezier(0.39, 0.57, 0.56, 1);
--animate-popup-scale-out: popupScaleOut 110ms cubic-bezier(0.55, 0.09, 0.68, 0.53);
--ring-offset: 0.5px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const animate = {
ping: 'ping 1s cubic-bezier(0, 0, 0.2, 1) infinite',
pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
bounce: 'bounce 1s infinite',
shimmer: 'shimmer 1.6s linear infinite',
'popup-scale-in': `popupScaleIn ${duration['moderate-01']} ${curve['productive-entrance']}`,
'popup-scale-out': `popupScaleOut ${duration['fast-02']} ${curve['productive-exit']}`,
};
Expand Down
4 changes: 4 additions & 0 deletions packages/theme/src/tokens/semantic/animations.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export const animations = () => {
'0%': { backgroundColor: 'var(--surface-hover)', fontWeight: '500' },
'100%': { backgroundColor: 'var(--surface-hover)', fontWeight: '500' },
},
'@keyframes shimmer': {
'0%': { backgroundPosition: '200% 0' },
'100%': { backgroundPosition: '-200% 0' },
},
'@keyframes popupScaleIn': {
'0%': { opacity: '0', transform: 'scale(0.95)' },
'100%': { opacity: '1', transform: 'scale(1)' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
width?: string
/** CSS height (any length); for a circle, set equal to `width`. */
height?: string
/** Pulse while loading; suppressed under reduced motion. */
/** Shimmer while loading; suppressed under reduced motion. */
animated?: boolean
}

Expand All @@ -40,6 +40,6 @@
:data-kind="kind"
:data-animated="animated || null"
:style="{ width, height }"
class="block bg-[var(--bg-surface-overlay)] data-[kind=shape]:rounded-[var(--shape-elements)] data-[kind=circle]:rounded-full data-[animated]:animate-pulse motion-reduce:animate-none"
class="block bg-[var(--bg-surface-overlay)] data-[kind=shape]:rounded-[var(--shape-elements)] data-[kind=circle]:rounded-full data-[animated]:motion-safe:bg-[linear-gradient(90deg,var(--bg-surface-raised)_0%,var(--bg-surface)_35%,var(--bg-surface-raised)_50%,var(--bg-surface-raised)_65%,var(--bg-surface-raised)_100%)] data-[animated]:motion-safe:bg-[length:200%_100%] data-[animated]:motion-safe:animate-[var(--animate-shimmer)] motion-reduce:animate-none"
/>
</template>
Loading