Skeleton
Skeleton is a generic shimmer loading placeholder: a single animated block whose shape, size, and border radius are fully configurable. Compose several of them together to reproduce any layout while its real content is loading. Unlike loader, which is a compact, inline indicator, skeleton stands in for the actual content itself, at its own size and shape, before it arrives.
When to use
Use skeleton to hold the shape of a whole layout, such as a card, a list
row, or a profile header, while its real content is still loading, so the
page doesn't jump once it arrives. For a small, inline indicator that
shows a button or a field is currently working, reach for
Install & usage
Pick a framework in the toolbar above and these snippets adapt.
npm install @eunomia/elements
import "@eunomia/elements/skeleton.js";
<eun-skeleton height="120px"></eun-skeleton>
Importing the file registers <eun-skeleton> as a custom element, with no
further setup needed. It works with any framework, or none, since it's a
standard web component.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/skeleton.js";
</script>
<eun-skeleton height="120px"></eun-skeleton>
npm install @eunomia/elements
import "@eunomia/elements/skeleton.js";
function CardPlaceholder() {
return <eun-skeleton height="120px" />;
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/skeleton.js";
export function CardPlaceholder() {
return <eun-skeleton height="120px" />;
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/skeleton.js";
</script>
<template>
<eun-skeleton height="120px" />
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/skeleton.js";
@Component({
selector: "app-card-placeholder",
template: `<eun-skeleton height="120px"></eun-skeleton>`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class CardPlaceholderComponent {}
Alternatives
eun-loaderGuidance
- Compose several
eun-skeletoninstances to mirror the exact layout (image, avatar, text lines, ...) that's about to load, so the transition to real content doesn't cause a visible reflow - Use
rect(the default) for cards, images, and buttons,circlefor avatars, andtextfor a single line of body copy - Swap the skeleton out for its real content once the request settles, whether that means success or failure, and pair it with an empty or error state rather than leaving it on screen indefinitely
- Set explicit
width/heightmatching the real content's own dimensions as closely as practical, to minimize layout shift once it swaps in
- Reaching for
eun-skeletonfor a compact, inline indicator such as inside a button or a badge, sinceeun-loader is built for that instead - Leaving a screen reader user with no announcement that content is loading: the skeleton itself is silent (see the Accessibility tab), so wrap it in a live region or accessible label describing the pending content whenever that context isn't already obvious
Live testing
Properties
Skeleton <eun-skeleton>
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| variant | 'rect' | 'circle' | 'text' | 'rect' | Rect for cards, images, or buttons, circle for avatars, and text for a single text line |
| width | string | '100%' | Any valid CSS length, such as 120px or 50% |
| height | string | — | Any valid CSS length. Ignored for the text variant, which sizes itself to one line of text |
| radius | string | — | A custom corner radius override. Ignored for the circle variant |
Import the exact TypeScript type behind any property above, see
CSS custom properties
| Name | Description |
|---|---|
| --skeleton-color | Sets the base shimmer color |
| --skeleton-highlight-color | Sets the shimmer highlight color |
| --skeleton-border-radius | Sets the corner radius, ignored for the circle variant |
Rect (default)
Rounded rectangle, for cards, images, or buttons: fills its width, 1rem
tall by default.
<eun-skeleton height="120px"></eun-skeleton>
Text
Sized to 1em regardless of height, matching a single line of text: set
width to vary the line length across a paragraph of several.
<eun-skeleton variant="text" width="90%"></eun-skeleton>
<eun-skeleton variant="text" width="70%"></eun-skeleton>
<eun-skeleton variant="text" width="40%"></eun-skeleton>
Circle
For avatars: set matching width/height for a perfect circle.
<eun-skeleton variant="circle" width="48px" height="48px"></eun-skeleton>
Composing a layout
Stack several eun-skeleton instances to reproduce a real layout: here, a
card image plus an avatar/text row, the same shape a loaded card would take.
<div style="width: 240px; display: flex; flex-direction: column; gap: 12px;">
<eun-skeleton height="120px"></eun-skeleton>
<div style="display: flex; align-items: center; gap: 8px;">
<eun-skeleton variant="circle" width="32px" height="32px"></eun-skeleton>
<eun-skeleton variant="text" width="60%"></eun-skeleton>
</div>
</div>
Custom radius
radius overrides the default border radius directly, though it's ignored
on the circle variant, which is always fully rounded.
<eun-skeleton height="80px" width="200px" radius="4px"></eun-skeleton>
Custom colors
Both the base and highlight shimmer colors are CSS variables.
<eun-skeleton
height="80px"
width="200px"
style="--skeleton-color: #e9def5; --skeleton-highlight-color: #f4effa;"
></eun-skeleton>
Purely decorative
The host element automatically carries role="presentation" and
aria-hidden="true", set in connectedCallback whenever they aren't already
present, so a screen reader skips it entirely, exactly as it would skip a
purely visual, contentless decoration. eun-skeleton never announces
anything on its own, so if the surrounding context doesn't already make it
clear that content is loading (e.g. a heading that already reads "Loading
results…"), wrap the loading region in its own live announcement (an
aria-live="polite" region, or a visually-hidden status message) rather than
relying on the skeleton itself.
Both attributes can still be overridden after the fact (e.g.
el.removeAttribute("aria-hidden")), but that's rarely useful, since a
shimmering block has no accessible information to expose, so exposing it
under a different role or label without also giving it real, meaningful
content mostly just adds noise to the accessibility tree.
Motion
The shimmer animation is fully disabled under prefers-reduced-motion: reduce, leaving a static, unanimated block in place, unlike
Reference links
presentation role