Skeleton

Free · MITReact Native · Expo

Loading placeholders whose shimmer is one light passing diagonally over the whole screen, plus presets and a Swap that eases from the skeleton's height to the content's while it cross-fades.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/skeleton.json

Then run npx expo install --fix so react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/skeleton.

Usage

import { Skeleton } from "@/components/goodui-native/skeleton/skeleton";

export function Inbox({ messages }: { messages?: Message[] }) {
  return (
    <Skeleton.Swap
      loading={!messages}
      loadedAnnouncement={messages ? `${messages.length} messages` : undefined}
      skeleton={
        <>
          <Skeleton.ListRow trailing />
          <Skeleton.ListRow trailing />
          <Skeleton.ListRow trailing />
        </>
      }
    >
      {messages?.map((m) => <MessageRow key={m.id} message={m} />)}
    </Skeleton.Swap>
  );
}

When to use

Use it for
  • The first load of a screen or list whose layout you know: feeds, inboxes, profiles, product grids.
  • Content that takes longer than about 300 ms, where a blank area would look broken.
  • Skeleton.Swap whenever the placeholder and the content occupy the same place.
Reach for something else
  • A button or form submit that is waiting: return the promise from button's onPress and it shows its own spinner.
  • Refreshing content that is already on screen: use pull-to-refresh and keep the old content visible.
  • Progress you can measure, like an upload: use progress-ring.

Examples

Primitives

<Skeleton.Circle size={48} />
<Skeleton.Line size="title2" width="50%" />
<Skeleton.Line size="subhead" lines={3} />
<Skeleton width={120} height={44} radius={14} />

Presets

<Skeleton.Profile />
<Skeleton.Card mediaHeight={180} />
<Skeleton.ListRow avatar="square" lines={3} />

Quieter animation

<Skeleton.ListRow animation="pulse" />
<Skeleton.Card animation="none" />

API reference

Skeleton

PropTypeDefault
width
Width in points or percent. Default `"100%"`.
DimensionValue—
height
Height in points. Default 16.
number—
radius
Corner radius. Defaults to the theme's `sm` radius.
number—
animation
How the placeholder signals loading. Under reduced motion `shimmer` becomes a slow pulse.
"pulse" | "none" | "shimmer"—
style
Style for the outer container.
StyleProp<ViewStyle>—

SkeletonLine

PropTypeDefault
size
Matches the text it stands in for: each line takes the size's line height, the bar sits inside it.
"caption" | "footnote" | "subhead" | "body" | "title3" | "title2" | "title1" | "largeTitle""body"
lines
Number of lines. Default 1.
number1
width
Width of every line but the last. Default `"100%"`.
DimensionValue"100%"
lastLineWidth
Width of the last line when there is more than one, so a paragraph ends ragged. Default `"60%"`.
DimensionValue"60%"
align
Line alignment inside the block. Default `"left"`.
"left" | "center""left"
animation
How the placeholder signals loading.
"pulse" | "none" | "shimmer"—
style
Style for the outer container.
StyleProp<ViewStyle>—

SkeletonCircle

PropTypeDefault
size
Diameter in points. Default 40.
number40
animation
How the placeholder signals loading.
"pulse" | "none" | "shimmer"—
style
Style for the outer container.
StyleProp<ViewStyle>—

SkeletonListRow

PropTypeDefault
avatar
Leading avatar shape, or `false` for none. Default `"circle"`.
"circle" | "square" | false"circle"
lines
Text lines next to the avatar, 1 to 3. Default 2.
number2
trailing
A short trailing value, like a time or an amount.
booleanfalse
animation
How the placeholder signals loading.
"pulse" | "none" | "shimmer"—
style
Style for the outer container.
StyleProp<ViewStyle>—

SkeletonCard

PropTypeDefault
media
Image placeholder on top. Default `true`.
booleantrue
mediaHeight
Height of the image placeholder. Default 160.
number160
lines
Body lines under the title. Default 2.
number2
animation
How the placeholder signals loading.
"pulse" | "none" | "shimmer"—
style
Style for the outer container.
StyleProp<ViewStyle>—

SkeletonProfile

PropTypeDefault
stats
A row of three stat placeholders under the bio. Default `true`.
booleantrue
animation
How the placeholder signals loading.
"pulse" | "none" | "shimmer"—
style
Style for the outer container.
StyleProp<ViewStyle>—

SkeletonSwap

PropTypeDefault
loading*
While true the skeleton shows; when it turns false the content cross-fades in.
boolean—
skeleton*
Placeholder shaped like the content, built from the Skeleton primitives or presets.
ReactNode—
children*
The real content. It mounts when loading ends.
ReactNode—
loadingLabel
Screen reader label while loading. Same as `labels.loading`.
string—
loadedAnnouncement
Announced to screen readers when loading ends, for example "3 new messages".
string—
labels
Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide.
Partial<SkeletonLabels>—
style
Style for the outer container.
StyleProp<ViewStyle>—

Motion and haptics

  • One clock drives every skeleton on screen: the shimmer is a single light that crosses the whole layout diagonally, then pauses.
  • Skeleton.Swap fades the placeholder out on top, the content rises 6 pt into place on the smooth spring, and the box eases between the two heights.
  • Reduced motion: the shimmer becomes a slow opacity pulse, the content only fades in and the height changes at once.
  • Haptic: None. Loading is not a commit.
  • The shared clock runs on the UI thread only while at least one animated skeleton is mounted.
  • Each placeholder measures its window position once per layout change, not per frame.
  • Use a handful of rows that fill the screen, not a full page of them.

Accessibility

  • Placeholders are hidden from screen readers.
  • Skeleton.Swap is one element labelled loadingLabel (default 'Loading') with accessibilityState busy while loading.
  • Pass loadedAnnouncement to announce the result when loading ends.
  • Skeleton.Line takes the line height of the text size it stands in for, so the layout matches at every size.
  • On the web Skeleton.Swap is a busy progressbar named loadingLabel while loading.

Notes for AI

  • Build the placeholder from the same layout as the content: same paddings, same text sizes in Skeleton.Line.
  • Use Skeleton.Swap instead of a ternary, so the switch cross-fades without a jump.
  • Do not add your own shimmer or gradient; set animation='pulse' or 'none' for a quieter look.
  • Skeleton.X and the named exports (SkeletonLine, SkeletonCard, ...) are the same components.