# Skeleton

> Reserve layout space while content is loading.

Source: https://www.honestui.com/docs/components/skeleton

```tsx
"use client";

import { useEffect, useState } from "react";
import {
  UserRoundPlus as UserRoundPlusIcon,
  UsersRound as UsersRoundIcon,
} from "honestui/icons";

import {
  Avatar,
  AvatarFallback,
  AvatarImage,
} from "@/components/honest-ui/ui/avatar";
import { Button } from "@/components/honest-ui/ui/button";
import { Skeleton } from "@/components/honest-ui/ui/skeleton";

const users = [
  {
    name: "Sarah Johnson",
    role: "Design Engineer",
    followers: "15k",
    image:
      "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=80&h=80&dpr=2&q=80",
    fallback: "SJ",
    delay: 3000,
  },
  {
    name: "Mark Bennett Andersson",
    role: "Product Designer",
    followers: "8k",
    image:
      "https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=80&h=80&dpr=2&q=80",
    fallback: "MA",
    delay: 4000,
  },
  {
    name: "Alex Rivera",
    role: "UI/UX Designer",
    followers: "12k",
    image:
      "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=80&h=80&dpr=2&q=80",
    fallback: "AR",
    delay: 3400,
  },
];

function UserCard({ delay, user }: { delay: number; user: (typeof users)[0] }) {
  const [isLoaded, setIsLoaded] = useState(false);

  useEffect(() => {
    const timer = setTimeout(() => {
      setIsLoaded(true);
    }, delay);

    return () => clearTimeout(timer);
  }, [delay]);

  if (!isLoaded) {
    return <UserCardSkeleton name={user.name} />;
  }

  return (
    <>
      <Avatar className="size-10">
        <AvatarImage src={user.image} alt={user.name} />
        <AvatarFallback>{user.fallback}</AvatarFallback>
      </Avatar>
      <div className="flex min-w-0 flex-1 flex-col gap-1">
        <h4 className="line-clamp-1 text-sm font-medium">{user.name}</h4>
        <div className="flex items-center gap-3 text-xs text-muted-foreground">
          <span className="truncate">{user.role}</span>
          <div className="flex min-w-0 items-center gap-1">
            <UsersRoundIcon aria-hidden="true" className="size-3 shrink-0" />
            <span className="truncate">
              {user.followers}
              <span className="max-sm:hidden"> followers</span>
            </span>
          </div>
        </div>
      </div>
      <Button size="xs">
        <UserRoundPlusIcon aria-hidden="true" />
        Follow
      </Button>
    </>
  );
}

function UserCardSkeleton({ name }: { name: string }) {
  return (
    <>
      <span className="sr-only" role="status">
        Loading {name}
      </span>
      <Skeleton aria-hidden="true" className="size-10 rounded-full" />
      <div className="flex flex-1 flex-col">
        <Skeleton aria-hidden="true" className="my-0.5 h-4 max-w-54" />
        <div className="flex max-w-54 items-center gap-1">
          <Skeleton aria-hidden="true" className="my-0.5 h-4 w-1/2" />
          <Skeleton aria-hidden="true" className="my-0.5 h-4 w-1/2" />
        </div>
      </div>
      <Skeleton aria-hidden="true" className="h-6 w-17" />
    </>
  );
}

export function SkeletonDemo() {
  return (
    <div className="flex w-full max-w-92 flex-col gap-6">
      {users.map((user) => (
        <div key={user.fallback} className="flex items-center gap-4">
          <UserCard delay={user.delay} user={user} />
        </div>
      ))}
    </div>
  );
}

```

## Overview [#overview]

Use Skeleton to reserve space while content is loading, so the page around the data keeps its shape instead of collapsing and jumping when values arrive. Skeletons are useful for cards, tables, lists, avatars, charts, and any region whose layout is known before its contents are.

## Anatomy [#anatomy]

A skeleton is a single placeholder shape: a muted surface with a lighter band that suggests motion. It takes whatever size and radius you give it. That is the whole component — which means every decision about what it communicates lives in how you compose it.

## When to use skeletons [#when-to-use-skeletons]

Skeletons buy you something specific: stable layout during short, predictable loads. They work best when the final shape is known and the wait is brief. For long operations, pair the placeholders with plain language ("Loading your invoices…"); for fast ones, delay showing skeletons briefly so they don't flash for 100 ms and vanish.

Match each placeholder to the geometry of what replaces it — circle for avatar, full-width bar for a line of text, block for an image — without reproducing every element. A rough rhythm is the goal; a wireframe of the entire page is noise. Keep the total number of animated shapes low; a page of shimmering rectangles reads as broken rather than busy.

## Accessibility [#accessibility]

Skeletons have no semantic meaning by default — a screen reader encounters empty `div`s and announces nothing. That silence is mostly right (the shapes carry no information), but it must not leave people without status:

* Mark placeholder shapes `aria-hidden="true"` to make their decoration explicit.
* Set `aria-busy="true"` on the containing region whose contents are still loading, and clear it when data arrives.
* Provide one status message for the whole region — visually hidden text or a polite live region such as "Loading conversation" — rather than announcing each shape.

Replace skeletons with real content rather than nesting content inside them; assistive technology should encounter either the placeholder state or the finished one, not both at once. Colors come from theme tokens, including a dimmer highlight in dark mode, so placeholders stay subtle against either theme.

Skeleton is the loading state — it has no error, disabled, or empty variant of its own. If the request fails while placeholders are up, swap them for an [Empty](/docs/components/empty) state with a retry action instead of leaving the shimmer running.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;skeleton&#x22;]" />
  

  
    
      
        Copy and paste the following code into your project.
      

      ### components/ui/skeleton.tsx

```tsx
import type { ComponentPropsWithoutRef } from "react"

import { cn } from "@/lib/utils"

function Skeleton({ className, ...props }: ComponentPropsWithoutRef<"div">) {
  return (
    <div
      data-slot="skeleton"
      className={cn(
        "animate-skeleton rounded-sm [--skeleton-highlight:--alpha(var(--color-white)/64%)] [background:linear-gradient(120deg,transparent_40%,var(--skeleton-highlight),transparent_60%)_var(--color-muted)_0_0_/_200%_100%_fixed] dark:[--skeleton-highlight:--alpha(var(--color-white)/4%)]",
        className
      )}
      {...props}
    />
  )
}

export { Skeleton }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

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

```tsx
<Skeleton className="size-10 rounded-full" />
```

Compose shapes into the layout they stand in for, and render the whole region conditionally: skeletons while loading, content after.

## Don't do this [#dont-do-this]

### Endless skeletons with no explanation [#endless-skeletons-with-no-explanation]

```tsx
// Bad
{isLoading && (
  <div className="space-y-3">{rows.map((i) => <Skeleton key={i} className="h-12" />)}</div>
)}
// isLoading stays true for 30 seconds with nothing else on screen
```

```tsx
// Good
{isLoading && (
  <div aria-busy="true">
    {rows.map((i) => (
      <Skeleton key={i} className="h-12" aria-hidden="true" />
    ))}
    <p className="sr-only" role="status">Loading your invoices…</p>
  </div>
)}
```

Shimmering placeholders communicate "soon", not "how long". Past a couple of seconds, people can't tell progress from a hang, and screen-reader users hear nothing at all. After roughly two seconds, add a status message; past ten, offer a way out — cancel, retry, or support.

### Shapes that don't match the content [#shapes-that-dont-match-the-content]

```tsx
// Bad
{isLoading ? <Skeleton className="h-4 w-full" /> : <Avatar … />}
```

```tsx
// Good
{isLoading ? <Skeleton className="size-10 rounded-full" /> : <Avatar … />}
```

A thin line swapping for a round avatar moves everything below it twice — once on load, once if the image later fails. Measure the placeholder against what replaces it: same height, same width tendency, same radius. The whole point of the component is zero layout shift.

### Content inside the skeleton [#content-inside-the-skeleton]

```tsx
// Bad
<Skeleton className="h-24 p-4">
  <InvoiceCard invoice={invoice} />
</Skeleton>
```

```tsx
// Good
{invoice ? <InvoiceCard invoice={invoice} /> : <Skeleton className="h-24 rounded-xl" />}
```

Nesting real content under a shimmer shows both states at once and announces both to assistive technology. The skeleton is a stand-in, not a wrapper — render one *or* the other.

## Examples [#examples]

### Dashboard card [#dashboard-card]

Headline, figure, and grid placeholders matching the card they replace.

```tsx
import { Skeleton } from "@/components/honest-ui/ui/skeleton"

export function SkeletonDashboardCard() {
  return (
    <div className="w-full max-w-sm rounded-xl border p-4">
      <Skeleton className="h-4 w-28" />
      <Skeleton className="mt-4 h-10 w-40" />
      <div className="mt-6 grid grid-cols-3 gap-2">
        <Skeleton className="h-16" />
        <Skeleton className="h-16" />
        <Skeleton className="h-16" />
      </div>
    </div>
  )
}

```

### Chat list [#chat-list]

Repeated row rhythm with circular and line placeholders.

```tsx
import { Skeleton } from "@/components/honest-ui/ui/skeleton"

export function SkeletonChatList() {
  return (
    <div className="grid w-full max-w-xs gap-4">
      {[1, 2, 3].map((item) => (
        <div key={item} className="flex items-center gap-3">
          <Skeleton className="size-10 rounded-full" />
          <div className="flex-1 space-y-2"><Skeleton className="h-3 w-full" /><Skeleton className="h-3 w-2/3" /></div>
        </div>
      ))}
    </div>
  )
}

```

### Skeleton Only [#skeleton-only]

The bare primitive at arbitrary sizes.

```tsx
import { Skeleton } from "@/components/honest-ui/ui/skeleton"

export function SkeletonOnly() {
  return (
    <div className="flex w-full max-w-92 items-center gap-4">
      <Skeleton className="size-10 rounded-full" />
      <div className="flex flex-1 flex-col">
        <Skeleton className="my-0.5 h-4 max-w-54" />
        <div className="flex max-w-54 items-center gap-1">
          <Skeleton className="my-0.5 h-4 w-1/2" />
          <Skeleton className="my-0.5 h-4 w-1/2" />
        </div>
      </div>
      <Skeleton className="h-6 w-17" />
    </div>
  )
}

```

## API reference [#api-reference]

`Skeleton` accepts native `div` props:

| Prop        | Values | Default |
| ----------- | ------ | ------- |
| `className` | string | —       |

There are no variants or states. Size and shape come entirely from your classes (`h-*`, `w-*`, `rounded-*`). The component renders a `div` with no accessible role; mark decorative shapes `aria-hidden="true"`, put `aria-busy="true"` on the loading region, and provide a status message when loading is not otherwise clear.
