Components · Feedback
Skeleton
A greyed placeholder that holds a component's shape while its content loads.
Examples
Variant
Active
Rows
Loading placeholder · avatar + title + paragraph (the common list-item shape)
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-skeleton> is the SAME shared custom element React and Vue consume — here in its
native form. Theming comes only from the --aha-* tokens in tokens.css. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/icons.js'; // <aha-icon> for the image glyph
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-skeleton.js'; // registers <aha-skeleton>
</script>
<!-- The common list-item shape: avatar + title + a multi-row paragraph (last row shorter) -->
<div style="display:flex; gap:16px; align-items:flex-start; max-width:420px">
<aha-skeleton variant="avatar"></aha-skeleton>
<div style="flex:1; display:flex; flex-direction:column; gap:12px">
<aha-skeleton variant="title"></aha-skeleton>
<aha-skeleton variant="paragraph" rows="3"></aha-skeleton>
<div style="display:flex; gap:12px">
<aha-skeleton variant="button"></aha-skeleton> <!-- round for a pill: variant="button" round -->
<aha-skeleton variant="image" style="width:120px"></aha-skeleton>
</div>
</div>
</div>
import '@ahaslides-product/design/icons'; // registers <aha-icon> (image variant)
import '@ahaslides-product/design/aha-skeleton'; // registers <aha-skeleton>
// Attributes only — no events — so React 18/19 can render the element directly.
function AhaSkeleton({ variant, rows, active, round }) {
return (
<aha-skeleton
variant={variant}
{...(rows != null ? { rows: String(rows) } : {})}
{...(round ? { round: '' } : {})}
{...(active === false ? { active: 'false' } : {})}
/>
);
}
// usage — hold the list-item shape while the card loads
{loading
? <AhaSkeleton variant="paragraph" rows={3} />
: <CardBody>{body}</CardBody>}
// main.ts — register the elements + mark aha-* as custom elements
import '@ahaslides-product/design/icons'; // registers <aha-icon> (image variant)
import '@ahaslides-product/design/aha-skeleton';
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue
<script setup>
import { ref } from 'vue';
const loading = ref(true);
</script>
<template>
<aha-skeleton v-if="loading" variant="paragraph" rows="3" />
<CardBody v-else>{{ body }}</CardBody>
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
variant | text | paragraph | title | avatar | button | input | image | text | Placeholder shape |
rows | number | 3 | Paragraph line count (last row is shorter); paragraph variant only |
round | boolean | false | Soften button/input/image corners to the pill |
active | boolean | true | Animate the shimmer (set 'false' to freeze) |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-skeleton'; // registers <aha-skeleton>
Agent feed for this component (absolute, fetchable anywhere): skeleton.agent.json · skeleton.md · skeleton.llms.txt
When to use
When to use
- Skeleton — content is loading and you want to hold its exact layout to avoid a jump
- Spin — a short, indeterminate wait where the final shape is unknown
- Progress — you can measure completion (a percentage)
Match the skeleton to the real content's shape and size so nothing shifts when it resolves. Use it for the first load of a region; don't stack many pulsing blocks for a sub-second wait — a Spin is calmer.
Surfaces
editor dashboard audience
Spec
Fill #F1F1F1 · radius xs (4) · Text height 16 · full width · Paragraph N rows (12 gap); last row 60% width · Title height 24 · 38% width · Avatar 40×40 · radius pill · Button / Input height 32 / 36 · radius default (8) · Image height 96 · centred gray-50 image glyph · radius default · Round corner-bearing variants soften to the pill · Motion opacity pulse 1.6s (respects reduced-motion); active=false stops it