Glin UI
Primitive / Atom

Skeleton

Loading placeholder surface with reduced-motion fallback.

Edit on GitHub
MK
Maya KovacsProduct designer, 2h ago

Shipped the new billing page today. The empty states took longer than the happy path, but they feel much better now.

billing-page.png
24 likes6 comments

Installation

Install from
pnpm dlx @glinui/cli@latest add skeleton

Prefer the package?

Usage

Variants

import { Skeleton } from "@glinui/ui"
export function Demo() {
return (
<div className="grid gap-3 sm:grid-cols-2">
<Skeleton variant="glinr" className="h-6 w-full" />
<Skeleton variant="solid" className="h-6 w-full" />
<Skeleton variant="plain" className="h-6 w-full" />
<Skeleton variant="soft" className="h-6 w-full" />
<Skeleton variant="outline" className="h-6 w-full" />
<Skeleton variant="ghost" className="h-6 w-full" />
</div>
)
}

Loading to loaded

Swap the skeleton for real content and flip aria-busy.

import { useState } from "react"
import { Avatar, Button, Card, Skeleton } from "@glinui/ui"
export function Demo() {
const [loading, setLoading] = useState(true)
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Button variant="outline" size="sm" className="w-fit" onClick={() => setLoading((value) => !value)}>
{loading ? "Show loaded state" : "Show loading state"}
</Button>
<Card className="flex items-center gap-4" aria-busy={loading}>
{loading ? (
<>
<Skeleton className="size-12 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>
</>
) : (
<>
<Avatar fallback="JO" size="lg" tone="success" />
<div className="flex flex-col">
<span className="font-medium">Jonas Okafor</span>
<span className="text-sm text-[var(--color-muted)]">Engineering manager</span>
</div>
</>
)}
</Card>
</div>
)
}

Variants

Six visual variants to match any surface context.

import { Skeleton } from "@glinui/ui"

export function SkeletonVariantDemo() {
  return (
    <div className="space-y-4">
      <div className="space-y-1">
        <span className="text-xs font-medium text-neutral-500 dark:text-neutral-400">Default</span>
        <Skeleton variant="default" className="h-5 w-64" />
      </div>
      <div className="space-y-1">
        <span className="text-xs font-medium text-neutral-500 dark:text-neutral-400">Glass</span>
        <Skeleton variant="glass" className="h-5 w-64" />
      </div>
      <div className="space-y-1">
        <span className="text-xs font-medium text-neutral-500 dark:text-neutral-400">Liquid</span>
        <Skeleton variant="liquid" className="h-5 w-64" />
      </div>
      <div className="space-y-1">
        <span className="text-xs font-medium text-neutral-500 dark:text-neutral-400">Matte</span>
        <Skeleton variant="matte" className="h-5 w-64" />
      </div>
      <div className="space-y-1">
        <span className="text-xs font-medium text-neutral-500 dark:text-neutral-400">Outline</span>
        <Skeleton variant="outline" className="h-5 w-64" />
      </div>
      <div className="space-y-1">
        <span className="text-xs font-medium text-neutral-500 dark:text-neutral-400">Ghost</span>
        <Skeleton variant="ghost" className="h-5 w-64" />
      </div>
    </div>
  )
}

Sizes

import { Skeleton } from "@glinui/ui"

export function SkeletonSizeDemo() {
  return (
    <div className="space-y-3">
      <Skeleton size="sm" className="w-40" />
      <Skeleton size="md" className="w-56" />
      <Skeleton size="lg" className="w-72" />
    </div>
  )
}

Card Loading

Compose multiple skeletons to mirror real card layouts during async fetch.

import { Skeleton } from "@glinui/ui"

export function SkeletonCardDemo() {
  return (
    <div className="max-w-sm rounded-2xl border border-neutral-200 p-4 dark:border-neutral-800">
      <Skeleton className="h-40 w-full rounded-xl" />
      <div className="mt-4 space-y-3">
        <Skeleton className="h-5 w-3/4" />
        <Skeleton className="h-4 w-full" />
        <Skeleton className="h-4 w-5/6" />
      </div>
      <div className="mt-5 flex items-center gap-3">
        <Skeleton className="h-9 w-9 rounded-full" />
        <div className="space-y-2">
          <Skeleton className="h-3.5 w-24" />
          <Skeleton className="h-3 w-16" />
        </div>
      </div>
    </div>
  )
}

Profile Loading

Avatar, name, and bio placeholders for a user profile state.

import { Skeleton } from "@glinui/ui"

export function SkeletonProfileDemo() {
  return (
    <div className="flex items-start gap-4">
      <Skeleton className="h-16 w-16 shrink-0 rounded-full" />
      <div className="flex-1 space-y-3 pt-1">
        <Skeleton className="h-5 w-40" />
        <Skeleton className="h-3.5 w-56" />
        <div className="flex gap-4 pt-1">
          <Skeleton className="h-3 w-20" />
          <Skeleton className="h-3 w-20" />
          <Skeleton className="h-3 w-20" />
        </div>
      </div>
    </div>
  )
}

Media Grid

Image grid placeholder for gallery or feed views.

import { Skeleton } from "@glinui/ui"

export function SkeletonMediaGridDemo() {
  return (
    <div className="grid grid-cols-3 gap-3">
      <Skeleton className="aspect-square w-full rounded-xl" />
      <Skeleton className="aspect-square w-full rounded-xl" />
      <Skeleton className="aspect-square w-full rounded-xl" />
      <Skeleton className="col-span-2 aspect-video w-full rounded-xl" />
      <Skeleton className="aspect-square w-full rounded-xl" />
    </div>
  )
}

Glass Card Loading

Skeleton on a glass surface for overlay loading states.

import { Skeleton } from "@glinui/ui"

export function SkeletonGlassDemo() {
  return (
    <div className="max-w-sm rounded-2xl border border-white/20 bg-white/30 p-5 backdrop-blur-xl dark:border-white/10 dark:bg-white/5">
      <div className="flex items-center gap-3">
        <Skeleton variant="glass" className="h-11 w-11 rounded-full" />
        <div className="space-y-2 flex-1">
          <Skeleton variant="glass" className="h-4 w-32" />
          <Skeleton variant="glass" className="h-3 w-20" />
        </div>
      </div>
      <div className="mt-4 space-y-2.5">
        <Skeleton variant="glass" className="h-3.5 w-full" />
        <Skeleton variant="glass" className="h-3.5 w-full" />
        <Skeleton variant="glass" className="h-3.5 w-3/4" />
      </div>
      <div className="mt-4 flex gap-2">
        <Skeleton variant="glass" className="h-9 w-24 rounded-xl" />
        <Skeleton variant="glass" className="h-9 w-24 rounded-xl" />
      </div>
    </div>
  )
}

List Loading

Row-based skeleton for table or list views.

import { Skeleton } from "@glinui/ui"

export function SkeletonListDemo() {
  return (
    <div className="max-w-md divide-y divide-neutral-200 dark:divide-neutral-800">
      {[1, 2, 3, 4].map((i) => (
        <div key={i} className="flex items-center gap-3 py-3">
          <Skeleton className="h-10 w-10 shrink-0 rounded-lg" />
          <div className="flex-1 space-y-2">
            <Skeleton className="h-4 w-3/5" />
            <Skeleton className="h-3 w-2/5" />
          </div>
          <Skeleton className="h-8 w-16 rounded-lg" />
        </div>
      ))}
    </div>
  )
}

Accessibility

  • Sets `aria-hidden` when decorative.
  • Uses `motion-safe:animate-pulse` with `motion-reduce:animate-none`.
  • Decorative placeholders default to `aria-hidden` so screen readers skip them.
  • Set `decorative={false}` and add `role="status"` with `aria-live="polite"` for semantic loading regions.
  • Avoid stacking more than 5-6 skeletons without a loading boundary to limit screen reader noise.

ARIA Attributes

  • `aria-hidden="true"` when decorative
  • `aria-busy="true"` on parent container

Reduced Motion

Pulse animation disabled with `prefers-reduced-motion: reduce`.

Shimmer animation uses `motion-safe:animate-[...]` / `motion-reduce:hidden`. Under `prefers-reduced-motion`, the shimmer overlay is hidden entirely and the skeleton renders as a static muted shape.

Affected properties

opacity

API Reference

PropTypeRequiredDefaultDescription
variantControlVariantPropNoambient (glinr)Placeholder look. Omit to follow the ambient design style. Glass is opt-in.
size"sm" | "md" | "lg"NomdHeight (h-4/h-6/h-10).
decorativebooleanNotrueWhen true, sets aria-hidden.

Source

Import directly from the package or browse the source on GitHub. Click any file to view it.

Import

import { Skeleton } from "@glinui/ui"

Source Files

Dependencies

@glinui/ui

Generated API Snapshot

Beta

Auto-extracted from TypeScript source in packages/ui/src/components/skeleton.tsx. This section is in beta and may lag behind hand-curated docs. Regenerate with pnpm --filter @glinui/docs api:generate.

Generated: 2026-10-08T01:41:21.546Z · Full index: /docs/api-metadata

Primary Props Type

SkeletonProps

SkeletonProps

PropTypeRequiredDefaultDescription
decorativebooleanNotrue-
size"sm" | "md" | "lg"No"md"Variant option from skeletonVariants.
variantControlVariantPropNo-Placeholder look. Omit to follow the ambient design style (glinr = inset well). Same vocabulary as Input.