Skeleton

A hand-drawn loading placeholder that animates its hachure shading, styled with shadcn conventions.

Skeleton
A hand-drawn placeholder whose hachure shading sweeps while content loads.
Text lines
Shapes
Card placeholder

Installation

npx shadcn@latest add https://sketchcn.chuwii.com/r/skeleton.json

Styles

Skeleton leans on these rules from sketch.css. The shadcn CLI installs the file with sketch-provider; copying the component by hand means copying them too.

@property --sketch-bg-reveal-lead {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

@property --sketch-bg-reveal-trail {
  syntax: "<percentage>";
  inherits: false;
  initial-value: 0%;
}

[data-sketch-reveal] [data-sketch-bg] {
  --sketch-bg-reveal-mask: linear-gradient(
    calc((180 + var(--sketch-bg-hachure-angle)) * 1deg),
    transparent var(--sketch-bg-reveal-trail),
    #000 var(--sketch-bg-reveal-trail),
    #000 var(--sketch-bg-reveal-lead),
    transparent var(--sketch-bg-reveal-lead)
  );
  mask-image: var(--sketch-bg-reveal-mask);
  -webkit-mask-image: var(--sketch-bg-reveal-mask);
  animation: sketch-hatch-reveal 2400ms linear infinite;
}

@keyframes sketch-hatch-reveal {
  0% {
    --sketch-bg-reveal-lead: 0%;
    --sketch-bg-reveal-trail: 0%;
  }

  45%,
  55% {
    --sketch-bg-reveal-lead: 100%;
    --sketch-bg-reveal-trail: 0%;
  }

  100% {
    --sketch-bg-reveal-lead: 100%;
    --sketch-bg-reveal-trail: 100%;
  }
}

Read the whole file for the drawing variables, paper textures and reduced-motion rules.

Usage

import { Skeleton } from "@/components/ui/skeleton";

export function LoadingLines() {
  return (
    <div className="flex w-44 flex-col gap-2">
      <Skeleton className="h-4 w-2/3" />
      <Skeleton className="h-4 w-full" />
      <Skeleton className="h-4 w-1/2" />
    </div>
  );
}

Props

Takes every div prop. Size it with utility classes; there is no size variant.

Examples

Profile loading

import { Skeleton } from "@/components/ui/skeleton";

export function ProfileLoading() {
  return (
    <div className="flex items-center gap-3">
      <Skeleton className="size-10 rounded-full" />
      <div className="flex flex-col gap-2">
        <Skeleton className="h-4 w-32" />
        <Skeleton className="h-3 w-20" />
      </div>
    </div>
  );
}

List loading

import { Skeleton } from "@/components/ui/skeleton";

export function NoteListLoading() {
  return (
    <ul className="flex w-full flex-col gap-3">
      {[0, 1, 2].map((row) => (
        <li key={row} className="flex flex-col gap-2">
          <Skeleton className="h-4 w-1/2" />
          <Skeleton className="h-3 w-full" />
        </li>
      ))}
    </ul>
  );
}

Wider hatching

import { Skeleton } from "@/components/ui/skeleton";

export function CalmSkeleton() {
  return <Skeleton className="h-24 w-full [--sketch-bg-hachure-gap:8]" />;
}

Upright hatching

import { Skeleton } from "@/components/ui/skeleton";

export function UprightSkeleton() {
  return (
    <Skeleton className="h-24 w-full [--sketch-bg-hachure-angle:90]" />
  );
}

Animation

Skeleton is a data-sketch-reveal wrapper around a data-sketch-bg svg. sketch.css masks the hachure and wipes it on a 2400ms sketch-hatch-reveal loop; there is no JavaScript timer.

[data-sketch-reveal] [data-sketch-bg] {
  --sketch-bg-reveal-mask: linear-gradient(
    calc((180 + var(--sketch-bg-hachure-angle)) * 1deg),
    transparent var(--sketch-bg-reveal-trail),
    #000 var(--sketch-bg-reveal-trail),
    #000 var(--sketch-bg-reveal-lead),
    transparent var(--sketch-bg-reveal-lead)
  );
  mask-image: var(--sketch-bg-reveal-mask);
  -webkit-mask-image: var(--sketch-bg-reveal-mask);
  animation: sketch-hatch-reveal 2400ms linear infinite;
}

The mask angle is derived from --sketch-bg-hachure-angle, so rotating the hatch rotates the wipe with it. Under prefers-reduced-motion the mask and the animation are both dropped, leaving a static hatched block.