Skeleton Loader
This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.
Skeleton loader provides a visual placeholders while content is loading, improving perceived performance and user experience. It represents the approximate shape and size of the content that will appear.
Examples
Live Preview
Text Skeleton
Default text skeleton with subtle border radius, perfect for text placeholders.
Circular Skeleton
Circular variant, ideal for avatar placeholders.
Rectangular Skeleton
Rectangular variant with no border radius, perfect for images.
Rounded Skeleton
Rounded variant with medium border radius, ideal for cards.
Effects
Different animation effects: pulse (default), sheen, and none.
Pulse
Sheen
None
Intensity
Light (default) uses --ag-background-secondary, medium uses --ag-background-tertiary.
Light
Medium
User Profile Card Loading
Real-world example showing a loading profile card.
Article Card Loading
Real-world example showing a loading article card.
List Loading
Real-world example showing a loading list.
CSS Shadow Parts Customization
Use CSS Shadow Parts to customize the skeleton's appearance.
View Vue Code
<template>
<section>
<div class="mbe4">
<h2>Text Skeleton</h2>
<p class="mbs2 mbe3">
Default text skeleton with subtle border radius, perfect for text placeholders.
</p>
<div style="width: 300px">
<VueSkeletonLoader />
<VueSkeletonLoader style="margin-top: 8px" />
<VueSkeletonLoader style="margin-top: 8px; width: 60%" />
</div>
</div>
<div class="mbe4">
<h2>Circular Skeleton</h2>
<p class="mbs2 mbe3">
Circular variant, ideal for avatar placeholders.
</p>
<div
class="flex items-center"
style="gap: 16px"
>
<VueSkeletonLoader
variant="circular"
width="40px"
height="40px"
/>
<VueSkeletonLoader
variant="circular"
width="60px"
height="60px"
/>
<VueSkeletonLoader
variant="circular"
width="80px"
height="80px"
/>
</div>
</div>
<div class="mbe4">
<h2>Rectangular Skeleton</h2>
<p class="mbs2 mbe3">
Rectangular variant with no border radius, perfect for images.
</p>
<VueSkeletonLoader
variant="rectangular"
width="300px"
height="200px"
/>
</div>
<div class="mbe4">
<h2>Rounded Skeleton</h2>
<p class="mbs2 mbe3">
Rounded variant with medium border radius, ideal for cards.
</p>
<VueSkeletonLoader
variant="rounded"
width="300px"
height="200px"
/>
</div>
<div class="mbe4">
<h2>Effects</h2>
<p class="mbs2 mbe3">
Different animation effects: pulse (default), sheen, and none.
</p>
<div
class="flex"
style="gap: 24px; flex-wrap: wrap"
>
<div>
<h3 class="mbe2">Pulse</h3>
<VueSkeletonLoader
effect="pulse"
width="200px"
height="100px"
variant="rounded"
/>
</div>
<div>
<h3 class="mbe2">Sheen</h3>
<VueSkeletonLoader
effect="sheen"
width="200px"
height="100px"
variant="rounded"
/>
</div>
<div>
<h3 class="mbe2">None</h3>
<VueSkeletonLoader
effect="none"
width="200px"
height="100px"
variant="rounded"
/>
</div>
</div>
</div>
<div class="mbe4">
<h2>Intensity</h2>
<p class="mbs2 mbe3">
Light (default) uses --ag-background-secondary, medium uses --ag-background-tertiary.
</p>
<div
class="flex"
style="gap: 24px; flex-wrap: wrap"
>
<div>
<h3 class="mbe2">Light</h3>
<VueSkeletonLoader
intensity="light"
width="200px"
height="100px"
variant="rounded"
/>
</div>
<div>
<h3 class="mbe2">Medium</h3>
<VueSkeletonLoader
intensity="medium"
width="200px"
height="100px"
variant="rounded"
/>
</div>
</div>
</div>
<div class="mbe4">
<h2>User Profile Card Loading</h2>
<p class="mbs2 mbe3">
Real-world example showing a loading profile card.
</p>
<div
class="flex"
style="gap: 16px; padding: 20px; border: 1px solid var(--ag-border); border-radius: var(--ag-radius-md); max-width: 400px"
>
<VueSkeletonLoader
variant="circular"
width="60px"
height="60px"
/>
<div style="flex: 1">
<VueSkeletonLoader
width="40%"
height="16px"
/>
<VueSkeletonLoader
width="60%"
height="14px"
style="margin-top: 8px"
/>
<VueSkeletonLoader
width="80%"
height="14px"
style="margin-top: 8px"
/>
</div>
</div>
</div>
<div class="mbe4">
<h2>Article Card Loading</h2>
<p class="mbs2 mbe3">
Real-world example showing a loading article card.
</p>
<div style="border: 1px solid var(--ag-border); border-radius: var(--ag-radius-md); overflow: hidden; max-width: 350px">
<VueSkeletonLoader
variant="rectangular"
width="100%"
height="200px"
/>
<div style="padding: 16px">
<VueSkeletonLoader
width="60%"
height="20px"
/>
<VueSkeletonLoader
width="100%"
height="14px"
style="margin-top: 12px"
/>
<VueSkeletonLoader
width="100%"
height="14px"
style="margin-top: 8px"
/>
<VueSkeletonLoader
width="40%"
height="14px"
style="margin-top: 8px"
/>
</div>
</div>
</div>
<div class="mbe4">
<h2>List Loading</h2>
<p class="mbs2 mbe3">
Real-world example showing a loading list.
</p>
<div style="max-width: 500px">
<div
v-for="i in 3"
:key="i"
class="flex items-center"
style="gap: 12px; margin-bottom: 16px"
>
<VueSkeletonLoader
variant="circular"
width="40px"
height="40px"
/>
<div style="flex: 1">
<VueSkeletonLoader
width="60%"
height="16px"
/>
<VueSkeletonLoader
width="40%"
height="14px"
style="margin-top: 8px"
/>
</div>
</div>
</div>
</div>
<div class="mbe4">
<h2>CSS Shadow Parts Customization</h2>
<p class="mbs2 mbe3">
Use CSS Shadow Parts to customize the skeleton's appearance.
</p>
<VueSkeletonLoader
class="custom-skeleton"
width="300px"
height="100px"
variant="rounded"
/>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { VueSkeletonLoader } from "agnosticui-core/skeleton/vue";
export default defineComponent({
name: "SkeletonLoaderExamples",
components: {
VueSkeletonLoader,
},
});
</script>
<style>
.custom-skeleton::part(ag-skeleton-wrapper) {
background: linear-gradient(
90deg,
var(--ag-primary-light) 0%,
var(--ag-primary) 50%,
var(--ag-primary-light) 100%
);
background-size: 200% 100%;
animation: custom-shimmer 2s infinite;
}
@keyframes custom-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
</style>
Live Preview
View Lit / Web Component Code
import { LitElement, html } from 'lit';
import 'agnosticui-core/skeleton';
export class SkeletonLoaderLitExamples extends LitElement {
// CRITICAL: Must include createRenderRoot() to use light DOM
// Without this, global CSS utility classes won't work!
createRenderRoot() {
return this;
}
render() {
return html`
<section>
<div class="mbe4">
<h2>Text Skeleton</h2>
<p class="mbs2 mbe3">
Default text skeleton with subtle border radius, perfect for text placeholders.
</p>
<div style="width: 300px">
<ag-skeleton-loader></ag-skeleton-loader>
<ag-skeleton-loader style="margin-top: 8px"></ag-skeleton-loader>
<ag-skeleton-loader style="margin-top: 8px; width: 60%"></ag-skeleton-loader>
</div>
</div>
<div class="mbe4">
<h2>Circular Skeleton</h2>
<p class="mbs2 mbe3">
Circular variant, ideal for avatar placeholders.
</p>
<div
class="flex items-center"
style="gap: 16px"
>
<ag-skeleton-loader
variant="circular"
width="40px"
height="40px"
></ag-skeleton-loader>
<ag-skeleton-loader
variant="circular"
width="60px"
height="60px"
></ag-skeleton-loader>
<ag-skeleton-loader
variant="circular"
width="80px"
height="80px"
></ag-skeleton-loader>
</div>
</div>
<div class="mbe4">
<h2>Rectangular Skeleton</h2>
<p class="mbs2 mbe3">
Rectangular variant with no border radius, perfect for images.
</p>
<ag-skeleton-loader
variant="rectangular"
width="300px"
height="200px"
></ag-skeleton-loader>
</div>
<div class="mbe4">
<h2>Rounded Skeleton</h2>
<p class="mbs2 mbe3">
Rounded variant with medium border radius, ideal for cards.
</p>
<ag-skeleton-loader
variant="rounded"
width="300px"
height="200px"
></ag-skeleton-loader>
</div>
<div class="mbe4">
<h2>Effects</h2>
<p class="mbs2 mbe3">
Different animation effects: pulse (default), sheen, and none.
</p>
<div
class="flex"
style="gap: 24px; flex-wrap: wrap"
>
<div>
<h3 class="mbe2">Pulse</h3>
<ag-skeleton-loader
effect="pulse"
width="200px"
height="100px"
variant="rounded"
></ag-skeleton-loader>
</div>
<div>
<h3 class="mbe2">Sheen</h3>
<ag-skeleton-loader
effect="sheen"
width="200px"
height="100px"
variant="rounded"
></ag-skeleton-loader>
</div>
<div>
<h3 class="mbe2">None</h3>
<ag-skeleton-loader
effect="none"
width="200px"
height="100px"
variant="rounded"
></ag-skeleton-loader>
</div>
</div>
</div>
<div class="mbe4">
<h2>Intensity</h2>
<p class="mbs2 mbe3">
Light (default) uses --ag-background-secondary, medium uses --ag-background-tertiary.
</p>
<div
class="flex"
style="gap: 24px; flex-wrap: wrap"
>
<div>
<h3 class="mbe2">Light</h3>
<ag-skeleton-loader
intensity="light"
width="200px"
height="100px"
variant="rounded"
></ag-skeleton-loader>
</div>
<div>
<h3 class="mbe2">Medium</h3>
<ag-skeleton-loader
intensity="medium"
width="200px"
height="100px"
variant="rounded"
></ag-skeleton-loader>
</div>
</div>
</div>
<div class="mbe4">
<h2>User Profile Card Loading</h2>
<p class="mbs2 mbe3">
Real-world example showing a loading profile card.
</p>
<div
class="flex"
style="gap: 16px; padding: 20px; border: 1px solid var(--ag-border); border-radius: var(--ag-radius-md); max-width: 400px"
>
<ag-skeleton-loader
variant="circular"
width="60px"
height="60px"
></ag-skeleton-loader>
<div style="flex: 1">
<ag-skeleton-loader
width="40%"
height="16px"
></ag-skeleton-loader>
<ag-skeleton-loader
width="60%"
height="14px"
style="margin-top: 8px"
></ag-skeleton-loader>
<ag-skeleton-loader
width="80%"
height="14px"
style="margin-top: 8px"
></ag-skeleton-loader>
</div>
</div>
</div>
<div class="mbe4">
<h2>Article Card Loading</h2>
<p class="mbs2 mbe3">
Real-world example showing a loading article card.
</p>
<div style="border: 1px solid var(--ag-border); border-radius: var(--ag-radius-md); overflow: hidden; max-width: 350px">
<ag-skeleton-loader
variant="rectangular"
width="100%"
height="200px"
></ag-skeleton-loader>
<div style="padding: 16px">
<ag-skeleton-loader
width="60%"
height="20px"
></ag-skeleton-loader>
<ag-skeleton-loader
width="100%"
height="14px"
style="margin-top: 12px"
></ag-skeleton-loader>
<ag-skeleton-loader
width="100%"
height="14px"
style="margin-top: 8px"
></ag-skeleton-loader>
<ag-skeleton-loader
width="40%"
height="14px"
style="margin-top: 8px"
></ag-skeleton-loader>
</div>
</div>
</div>
<div class="mbe4">
<h2>List Loading</h2>
<p class="mbs2 mbe3">
Real-world example showing a loading list.
</p>
<div style="max-width: 500px">
${[1, 2, 3].map(() => html`
<div
class="flex items-center"
style="gap: 12px; margin-bottom: 16px"
>
<ag-skeleton-loader
variant="circular"
width="40px"
height="40px"
></ag-skeleton-loader>
<div style="flex: 1">
<ag-skeleton-loader
width="60%"
height="16px"
></ag-skeleton-loader>
<ag-skeleton-loader
width="40%"
height="14px"
style="margin-top: 8px"
></ag-skeleton-loader>
</div>
</div>
`)}
</div>
</div>
<div class="mbe4">
<h2>CSS Shadow Parts Customization</h2>
<p class="mbs2 mbe3">
Use CSS Shadow Parts to customize the skeleton's appearance.
</p>
<ag-skeleton-loader
class="custom-skeleton"
width="300px"
height="100px"
variant="rounded"
></ag-skeleton-loader>
</div>
</section>
`;
}
}
// Register the custom element (at the bottom, NOT with decorator)
customElements.define('skeleton-loader-lit-examples', SkeletonLoaderLitExamples);
Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.
View React Code
import { ReactSkeletonLoader } from "agnosticui-core/skeleton/react";
export default function SkeletonLoaderReactExamples() {
return (
<section>
<div className="mbe4">
<h2>Text Skeleton</h2>
<p className="mbs2 mbe3">
Default text skeleton with subtle border radius, perfect for text placeholders.
</p>
<div style={{ width: '300px' }}>
<ReactSkeletonLoader />
<ReactSkeletonLoader style={{ marginTop: '8px' }} />
<ReactSkeletonLoader style={{ marginTop: '8px', width: '60%' }} />
</div>
</div>
<div className="mbe4">
<h2>Circular Skeleton</h2>
<p className="mbs2 mbe3">
Circular variant, ideal for avatar placeholders.
</p>
<div
className="flex items-center"
style={{ gap: '16px' }}
>
<ReactSkeletonLoader
variant="circular"
width="40px"
height="40px"
/>
<ReactSkeletonLoader
variant="circular"
width="60px"
height="60px"
/>
<ReactSkeletonLoader
variant="circular"
width="80px"
height="80px"
/>
</div>
</div>
<div className="mbe4">
<h2>Rectangular Skeleton</h2>
<p className="mbs2 mbe3">
Rectangular variant with no border radius, perfect for images.
</p>
<ReactSkeletonLoader
variant="rectangular"
width="300px"
height="200px"
/>
</div>
<div className="mbe4">
<h2>Rounded Skeleton</h2>
<p className="mbs2 mbe3">
Rounded variant with medium border radius, ideal for cards.
</p>
<ReactSkeletonLoader
variant="rounded"
width="300px"
height="200px"
/>
</div>
<div className="mbe4">
<h2>Effects</h2>
<p className="mbs2 mbe3">
Different animation effects: pulse (default), sheen, and none.
</p>
<div
className="flex"
style={{ gap: '24px', flexWrap: 'wrap' }}
>
<div>
<h3 className="mbe2">Pulse</h3>
<ReactSkeletonLoader
effect="pulse"
width="200px"
height="100px"
variant="rounded"
/>
</div>
<div>
<h3 className="mbe2">Sheen</h3>
<ReactSkeletonLoader
effect="sheen"
width="200px"
height="100px"
variant="rounded"
/>
</div>
<div>
<h3 className="mbe2">None</h3>
<ReactSkeletonLoader
effect="none"
width="200px"
height="100px"
variant="rounded"
/>
</div>
</div>
</div>
<div className="mbe4">
<h2>Intensity</h2>
<p className="mbs2 mbe3">
Light (default) uses --ag-background-secondary, medium uses --ag-background-tertiary.
</p>
<div
className="flex"
style={{ gap: '24px', flexWrap: 'wrap' }}
>
<div>
<h3 className="mbe2">Light</h3>
<ReactSkeletonLoader
intensity="light"
width="200px"
height="100px"
variant="rounded"
/>
</div>
<div>
<h3 className="mbe2">Medium</h3>
<ReactSkeletonLoader
intensity="medium"
width="200px"
height="100px"
variant="rounded"
/>
</div>
</div>
</div>
<div className="mbe4">
<h2>User Profile Card Loading</h2>
<p className="mbs2 mbe3">
Real-world example showing a loading profile card.
</p>
<div
className="flex"
style={{ gap: '16px', padding: '20px', border: '1px solid var(--ag-border)', borderRadius: 'var(--ag-radius-md)', maxWidth: '400px' }}
>
<ReactSkeletonLoader
variant="circular"
width="60px"
height="60px"
/>
<div style={{ flex: 1 }}>
<ReactSkeletonLoader
width="40%"
height="16px"
/>
<ReactSkeletonLoader
width="60%"
height="14px"
style={{ marginTop: '8px' }}
/>
<ReactSkeletonLoader
width="80%"
height="14px"
style={{ marginTop: '8px' }}
/>
</div>
</div>
</div>
<div className="mbe4">
<h2>Article Card Loading</h2>
<p className="mbs2 mbe3">
Real-world example showing a loading article card.
</p>
<div style={{ border: '1px solid var(--ag-border)', borderRadius: 'var(--ag-radius-md)', overflow: 'hidden', maxWidth: '350px' }}>
<ReactSkeletonLoader
variant="rectangular"
width="100%"
height="200px"
/>
<div style={{ padding: '16px' }}>
<ReactSkeletonLoader
width="60%"
height="20px"
/>
<ReactSkeletonLoader
width="100%"
height="14px"
style={{ marginTop: '12px' }}
/>
<ReactSkeletonLoader
width="100%"
height="14px"
style={{ marginTop: '8px' }}
/>
<ReactSkeletonLoader
width="40%"
height="14px"
style={{ marginTop: '8px' }}
/>
</div>
</div>
</div>
<div className="mbe4">
<h2>List Loading</h2>
<p className="mbs2 mbe3">
Real-world example showing a loading list.
</p>
<div style={{ maxWidth: '500px' }}>
{[1, 2, 3].map((i) => (
<div
key={i}
className="flex items-center"
style={{ gap: '12px', marginBottom: '16px' }}
>
<ReactSkeletonLoader
variant="circular"
width="40px"
height="40px"
/>
<div style={{ flex: 1 }}>
<ReactSkeletonLoader
width="60%"
height="16px"
/>
<ReactSkeletonLoader
width="40%"
height="14px"
style={{ marginTop: '8px' }}
/>
</div>
</div>
))}
</div>
</div>
<div className="mbe4">
<h2>CSS Shadow Parts Customization</h2>
<p className="mbs2 mbe3">
Use CSS Shadow Parts to customize the skeleton's appearance.
</p>
<ReactSkeletonLoader
className="custom-skeleton"
width="300px"
height="100px"
variant="rounded"
/>
</div>
</section>
);
}
Usage
TIP
The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add SkeletonLoaderThe CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.
Vue
<template>
<VueSkeletonLoader variant="text" />
<VueSkeletonLoader variant="circular" width="60px" height="60px" />
<VueSkeletonLoader variant="rectangular" width="300px" height="200px" />
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { VueSkeletonLoader } from "agnosticui-core/skeleton/vue";
export default defineComponent({
components: { VueSkeletonLoader },
});
</script>React
import { ReactSkeletonLoader } from "agnosticui-core/skeleton/react";
export default function Example() {
return (
<>
<ReactSkeletonLoader variant="text" effect="pulse" intensity="light" />
<ReactSkeletonLoader
variant="circular"
width="60px"
height="60px"
effect="pulse"
intensity="light"
/>
<ReactSkeletonLoader
effect="sheen"
variant="rectangular"
width="300px"
height="200px"
/>
</>
);
}Lit (Web Components)
<script type="module">
import "agnosticui-core/skeleton";
</script>
<ag-skeleton-loader variant="text"></ag-skeleton-loader>
<ag-skeleton-loader variant="circular" width="60px" height="60px"></ag-skeleton-loader>
<ag-skeleton-loader variant="rectangular" width="300px" height="200px"></ag-skeleton-loader>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | text | circular | rectangular | rounded | text | The shape variant of the skeleton. |
effect | pulse | sheen | none | pulse | The animation effect applied to the skeleton. |
intensity | light | medium | light | The background intensity using design tokens. |
width | string | 100% | The width of the skeleton (any CSS value). |
height | string | 1em | The height of the skeleton (any CSS value). |
Variants
Shape Variants
- text: Default variant with small border radius, ideal for text placeholders
- circular: Perfect circle shape, ideal for avatar placeholders
- rectangular: No border radius, ideal for image placeholders
- rounded: Medium border radius, ideal for card/button placeholders
Effect Variants
- pulse: Gentle opacity animation (default)
- sheen: Shimmer effect that sweeps across the skeleton
- none: No animation, static placeholder
Intensity Variants
- light: Uses
--ag-background-secondary(default, lighter background) - medium: Uses
--ag-background-tertiary(darker background for more contrast)
CSS Shadow Parts
| Part | Description |
|---|---|
ag-skeleton-wrapper | The main skeleton wrapper element. |
Events
| Event | Payload | Description |
|---|---|---|
| No custom events. |
Accessibility
Screen Readers
The skeleton loader automatically sets aria-hidden="true" to hide the placeholder content from screen readers. When implementing loading states:
- Wrap skeletons in a container with
role="status"andaria-live="polite"to announce loading - Add
aria-busy="true"to the container to tell screen readers to wait for actual content - When content loads, remove the skeleton and update
aria-busy="false"
Motion Preferences
The component respects the prefers-reduced-motion media query. When users enable reduced motion in their OS settings, all animations (pulse and sheen effects) are automatically disabled.
Color Contrast
Skeleton loaders use semantic design tokens (--ag-background-secondary and --ag-background-tertiary) that maintain appropriate contrast in both light and dark modes. The contrast requirements are more lenient for loading placeholders due to their temporary nature and large size.