Skip to content

Skeleton Loader

Experimental Alpha

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

Vue
Lit
React
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>
  );
}
Open in StackBlitz

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:

bash
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add SkeletonLoader

The 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
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
tsx
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)
html
<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

PropTypeDefaultDescription
varianttext | circular | rectangular | roundedtextThe shape variant of the skeleton.
effectpulse | sheen | nonepulseThe animation effect applied to the skeleton.
intensitylight | mediumlightThe background intensity using design tokens.
widthstring100%The width of the skeleton (any CSS value).
heightstring1emThe 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

PartDescription
ag-skeleton-wrapperThe main skeleton wrapper element.

Events

EventPayloadDescription
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" and aria-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.