Skip to content
Ndop UI

Skeleton

Shows a placeholder while content loads.

  • v1.0.0
  • SSR
  • Zoneless
  • 0.4 kB gzip
Loading profile
import { Component } from '@angular/core';
import { Skeleton } from '@/ui/skeleton/skeleton';

/** Placeholders for a profile while it loads. */
@Component({
  selector: 'example-skeleton-demo',
  imports: [Skeleton],
  template: `
    <div class="flex items-center gap-4" aria-busy="true">
      <span class="sr-only">Loading profile</span>
      <div ndSkeleton class="size-12 rounded-full"></div>
      <div class="grid gap-2">
        <div ndSkeleton class="h-4 w-56"></div>
        <div ndSkeleton class="h-4 w-48"></div>
      </div>
    </div>
  `,
})
export class SkeletonDemoExample {}

A skeleton is a placeholder in the shape of content that's still loading, so the page doesn't jump when it arrives. Size it with classes. It's hidden from assistive technology; mark the region that's loading with aria-busy="true" so screen readers wait for the real content.

Installation

npx @ndop/cli add skeleton

Usage

import { Skeleton } from '@/ui/skeleton/skeleton';
<div class="flex items-center gap-4" aria-busy="true">
  <span class="sr-only">Loading profile</span>
  <div ndSkeleton class="size-12 rounded-full"></div>
  <div class="grid gap-2">
    <div ndSkeleton class="h-4 w-56"></div>
    <div ndSkeleton class="h-4 w-48"></div>
  </div>
</div>

Examples

Skeleton card

A card that shows skeletons while its content loads.

Weekly report

import { Component, signal } from '@angular/core';
import { Button } from '@/ui/button/button';
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from '@/ui/card/card';
import { Skeleton } from '@/ui/skeleton/skeleton';

/**
 * A card that shows skeletons while its content loads, and marks itself
 * busy so screen readers wait for the real content.
 */
@Component({
  selector: 'example-skeleton-card',
  imports: [
    Button,
    Card,
    CardContent,
    CardDescription,
    CardHeader,
    CardTitle,
    Skeleton,
  ],
  template: `
    <div class="grid w-full max-w-sm gap-4">
      <section
        ndCard
        aria-labelledby="skeleton-card-title"
        [attr.aria-busy]="loading()"
      >
        <div ndCardHeader>
          <h2 ndCardTitle id="skeleton-card-title">Weekly report</h2>
          @if (loading()) {
            <div ndSkeleton class="h-4 w-40"></div>
          } @else {
            <p ndCardDescription>October 2 to October 8</p>
          }
        </div>
        <div ndCardContent class="grid gap-2">
          @if (loading()) {
            <div ndSkeleton class="h-24 w-full"></div>
            <div ndSkeleton class="h-4 w-3/4"></div>
          } @else {
            <p class="text-3xl font-semibold">1,284 visits</p>
            <p class="text-sm text-muted-foreground">Up 12% on last week.</p>
          }
        </div>
      </section>
      <button
        ndButton
        variant="outline"
        class="justify-self-start"
        (click)="loading.set(!loading())"
      >
        {{ loading() ? 'Show content' : 'Show loading' }}
      </button>
    </div>
  `,
})
export class SkeletonCardExample {
  protected readonly loading = signal(true);
}

Accessibility

Skeleton has no keyboard interaction or focus handling of its own; it keeps the semantics of the element it's on.

API reference

Skeleton

[ndSkeleton]

A pulsing placeholder in the shape of content that's loading. It's hidden from assistive technology; mark the region that's loading with aria-busy="true" instead.

Skeleton binding Type Default
[class]
Extra classes, merged last so they win: usually a size and shape.
string''
Data attribute Values
data-slotskeleton

Agent notes

Do

  • Match the size and shape of what's coming: lines of text, an avatar, an image.
  • Set aria-busy="true" on the container while it loads, and remove it when the content is in.
  • Use @defer or a resource()'s loading state to swap skeletons for content.

Don't

  • Don't show skeletons for content that loads in a blink; it flickers.
  • Don't put text or controls inside a skeleton.