Skeleton
Shows a placeholder while content loads.
- v1.0.0
- SSR
- Zoneless
- 0.4 kB gzip
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 skeletonpnpm dlx @ndop/cli add skeletonyarn dlx @ndop/cli add skeletonbunx @ndop/cli add skeletonInstall the dependencies
npm install @ndop/corepnpm add @ndop/coreyarn add @ndop/corebun add @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/skeleton/skeleton.tsimport { computed, Directive, input } from '@angular/core'; import { cn } from '@/lib/utils'; /** * 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. * * @example * ```html * <div aria-busy="true"><div ndSkeleton class="h-4 w-48"></div></div> * ``` */ @Directive({ selector: '[ndSkeleton]', host: { 'data-slot': 'skeleton', 'aria-hidden': 'true', '[class]': 'classes()', }, }) export class Skeleton { /** Extra classes, merged last so they win: usually a size and shape. */ readonly class = input(''); protected readonly classes = computed(() => cn('animate-pulse rounded-md bg-accent', this.class()), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
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-slot | skeleton |
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
@deferor aresource()'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.