Gecko UIGecko UI

Skeleton

Placeholder that holds the space content will take while it loads

Skeleton

A placeholder that holds the space content will take while it loads.

Size it with className the way you would size the real thing, so the page does not move when the content arrives.

Installation

import { Skeleton } from '@geckoui/geckoui';

Basic Usage

<Skeleton />          {/* one line */}
<Skeleton lines={3} /> {/* a paragraph */}

The last line of a multi-line block is drawn short.

Props API

PropTypeDefaultDescription
shape'text' | 'rounded' | 'circle''text'The outline of the placeholder
animation'pulse' | 'wave' | 'none''pulse'How it animates while it waits
linesnumber1How many stacked lines to draw. Only applies to text
loadingbooleantrueWhile true the placeholder is drawn, otherwise children render
childrenReactNode-What replaces the placeholder once loading is false
classNamestring-CSS class for the placeholder, and where you size it
...restHTMLAttributes<HTMLDivElement>-All standard div attributes

Examples

Shapes

<Skeleton shape="circle" className="size-12" />
<Skeleton shape="rounded" className="h-24 w-40" />
<Skeleton lines={2} />

Size a skeleton through className. A text skeleton takes its height from the current font size.

Animation

pulse
wave
none
<Skeleton animation="pulse" shape="rounded" className="h-16 w-full" />
<Skeleton animation="wave" shape="rounded" className="h-16 w-full" />
<Skeleton animation="none" shape="rounded" className="h-16 w-full" />

pulse fades the whole bar, wave sweeps a highlight across it. Both are dropped under prefers-reduced-motion.

Swapping in the real content

<Skeleton loading={isLoading} shape="circle" className="size-12">
  <Avatar src={user.image} />
</Skeleton>

<Skeleton loading={isLoading} className="w-40">
  <p className="font-semibold">{user.name}</p>
</Skeleton>

<Skeleton loading={isLoading} lines={2}>
  <p>{user.bio}</p>
</Skeleton>

Children are never rendered while loading, so user.bio is not read until user exists. Once loading is over the wrapper is gone too, leaving only your own markup.

Styling with CSS

VariableDefaultApplies to
--gecko-skeleton-bg--color-surface-hover-strongBar fill
--gecko-skeleton-highlightrgb(255 255 255 / 55%)The sweep on wave
--gecko-skeleton-radius0.25remCorner radius, except on circle
--gecko-skeleton-duration1.6sOne cycle of either animation
--gecko-skeleton-line-height1emHeight of a text bar
--gecko-skeleton-line-gap0.5remSpace between stacked lines
--gecko-skeleton-last-line-width60%Width of the short last line
.GeckoUISkeleton {
  --gecko-skeleton-bg: oklch(0.92 0 none);
  --gecko-skeleton-duration: 1s;
}

Class names and data attributes

  • GeckoUISkeleton - The placeholder, with data-shape and data-animation
  • GeckoUISkeleton__bar - One bar. A text skeleton has one per line, every other shape has exactly one

Adding your own shapes

Both axes are extensible maps, so you can add keys through module augmentation and style them with CSS. No component change is needed:

declare module '@geckoui/geckoui' {
  interface SkeletonShapeMap {
    pill: unknown;
  }
}

<Skeleton shape="pill" className="h-6 w-24" />
.GeckoUISkeleton[data-shape="pill"] > .GeckoUISkeleton__bar {
  border-radius: 9999px;
}

The same works for SkeletonAnimationMap.

Accessibility

The placeholder is marked aria-busy="true" and has no role of its own.

To announce a loading region, put the announcement on the region rather than on each skeleton:

<section aria-busy={isLoading} aria-live="polite">
  <Skeleton loading={isLoading} lines={3}>
    <Article body={article.body} />
  </Skeleton>
</section>
  • Spinner - For work with no shape to hold, like a button that is submitting
  • Badge - Small inline labels