Skeleton
Free · MITReact Native · ExpoLoading 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.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/skeleton.jsonThen 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
- 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.
- 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
| Prop | Type | Default |
|---|---|---|
| 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
| Prop | Type | Default |
|---|---|---|
| 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. | number | 1 |
| 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
| Prop | Type | Default |
|---|---|---|
| size Diameter in points. Default 40. | number | 40 |
| animation How the placeholder signals loading. | "pulse" | "none" | "shimmer" | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
SkeletonListRow
| Prop | Type | Default |
|---|---|---|
| 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. | number | 2 |
| trailing A short trailing value, like a time or an amount. | boolean | false |
| animation How the placeholder signals loading. | "pulse" | "none" | "shimmer" | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
SkeletonCard
| Prop | Type | Default |
|---|---|---|
| media Image placeholder on top. Default `true`. | boolean | true |
| mediaHeight Height of the image placeholder. Default 160. | number | 160 |
| lines Body lines under the title. Default 2. | number | 2 |
| animation How the placeholder signals loading. | "pulse" | "none" | "shimmer" | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
SkeletonProfile
| Prop | Type | Default |
|---|---|---|
| stats A row of three stat placeholders under the bio. Default `true`. | boolean | true |
| animation How the placeholder signals loading. | "pulse" | "none" | "shimmer" | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
SkeletonSwap
| Prop | Type | Default |
|---|---|---|
| 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.
Related
A scroll view whose refresh indicator is a drop that stretches as you pull, snaps round with a bounce and a haptic past the threshold, spins while onRefresh runs and ends on a check or an error.
A circular progress ring that fills on a spring, laps itself past 100 % with a gap that lifts the lap off the ring, and bounces once with a success haptic when it completes.
A press-responsive button with haptics that runs async actions on its own: it gathers into a spinner, then opens into a success or error state.