Qure UI
Components

Skeleton

A placeholder shaped like the thing that is loading.

import { Skeleton } from '@/registry/qure/ui/skeleton'export default function SkeletonDemo() {  return (    <div className="flex w-full max-w-sm items-center gap-3">      <Skeleton className="size-10 rounded-full" />      <div className="flex flex-1 flex-col gap-2">        <Skeleton className="h-4 w-2/3" />        <Skeleton className="h-3 w-1/2" />      </div>    </div>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/skeleton.json

Usage

import { Skeleton } from '@/components/ui/skeleton'

Size it with utilities to match what will replace it. The value of a skeleton is that the layout does not jump when the content lands — one that is the wrong shape is worse than a spinner, because it promised something.

<Skeleton className="size-10 rounded-full" />
<Skeleton className="h-4 w-2/3" />

Use it where you know the shape of what is coming and it is coming soon. For an operation of unknown length, or one where the result could be nothing at all, a spinner and then an empty state is more honest. Never skeleton a whole page: a screen of grey bars is slower to read than a blank one.

A person and a line of text

import { Skeleton } from '@/registry/qure/ui/skeleton'export default function SkeletonDemo() {  return (    <div className="flex w-full max-w-sm items-center gap-3">      <Skeleton className="size-10 rounded-full" />      <div className="flex flex-1 flex-col gap-2">        <Skeleton className="h-4 w-2/3" />        <Skeleton className="h-3 w-1/2" />      </div>    </div>  )}

The smallest useful case — a circle where the avatar goes, two bars of decreasing width for the name and the subtitle. Vary the widths; equal bars read as a pattern rather than as text.

Shaped like the card it replaces

import { Card, CardContent, CardFooter, CardHeader } from '@/registry/qure/ui/card'import { Skeleton } from '@/registry/qure/ui/skeleton'/** The same shape as card-demo, so nothing moves when the study arrives. */export default function SkeletonCard() {  return (    <Card className="w-full max-w-sm" aria-busy aria-label="Loading study">      <CardHeader className="grid gap-2">        <Skeleton className="h-6 w-32" />        <Skeleton className="h-5 w-44" />      </CardHeader>      <CardContent className="grid gap-2">        <Skeleton className="h-4 w-full" />        <Skeleton className="h-4 w-3/5" />      </CardContent>      <CardFooter>        <Skeleton className="h-8 w-24" />        <Skeleton className="h-8 w-20" />      </CardFooter>    </Card>  )}

The same Card, the same header, content and footer, with each piece of text swapped for a bar of roughly its size. Because the real card comes back into the same box, nothing below it moves. Keep the skeleton beside its real component in the file so the two stay in step — a skeleton that has drifted from its content is the reason layouts jump.

A worklist that has not arrived

import { Skeleton } from '@/registry/qure/ui/skeleton'export default function SkeletonTable() {  return (    <div className="w-full max-w-md divide-y rounded-lg border" aria-busy aria-label="Loading worklist">      {[0, 1, 2, 3].map(row => (        <div key={row} className="flex items-center gap-4 p-3">          <Skeleton className="h-4 w-24" />          <Skeleton className="h-4 flex-1" />          <Skeleton className="h-6 w-16 rounded-full" />        </div>      ))}    </div>  )}

Three or four rows is the right number. Show as many as will fill the viewport and no more; a hundred placeholder rows imply a hundred results, which you do not yet know.

Accessibility

A skeleton is decorative. Announce the loading state on the region that is changing, not on each placeholder:

<div aria-busy aria-live="polite">
  {loading ? <Skeleton className="h-4 w-2/3" /> : <p>{finding}</p>}
</div>

aria-busy on the container tells assistive technology to hold off; a Skeleton carries no role and no text, so it is passed over. The pulse is disabled under prefers-reduced-motion.

API Reference

No primitive and no added props — a div with data-slot="skeleton", everything else comes from className. What the stylesheet gives it:

PropertyValue
Background--background-base-secondary
Radius--radius-200, overridable with rounded-full and friends
AnimationA 1.6s opacity pulse, off under prefers-reduced-motion

On this page