Skeleton
Animated placeholder for loading states.
Installation
npx axiom add skeletonpnpm dlx axiom add skeletonyarn dlx axiom add skeletonbun x axiom add skeletonInstalls: react-native-reanimated.
The shared animation clock lives in use-skeleton.ts. Colors come from skeleton.default.default.{background,highlight} in the component tokens.
Usage
import { Skeleton } from '@/components/ui/skeleton';
<Skeleton width={48} height={48} radius="full" />
<Skeleton width="60%" height={16} />Props
Skeleton
| Prop | Type | Default | Description |
|---|---|---|---|
width | DimensionValue | '100%' | Width in points or percent. Match the content it replaces. |
height | DimensionValue | 16 | Height. For text, use the line height of the text variant it stands for. |
radius | RadiusToken | 'sm' | Corner radius from the tokens. full for avatars. |
variant | 'shimmer' | 'pulse' | 'none' | 'shimmer' | Animation. pulse fades in and out, none is static. Reduce motion forces none. |
loading | boolean | true | When false, renders children instead of the placeholder. |
children | ReactNode | — | The real content, shown once loading is false. |
style | StyleProp<ViewStyle> | — | Extra styles for the placeholder. |
Skeleton.Text
| Prop | Type | Default | Description |
|---|---|---|---|
lines | number | 3 | Number of lines. The last one is shorter, like a real paragraph. |
variant | TextVariant | 'body' | Uses the line height of this Text variant, so the placeholder has the same height as the text. |
animation | 'shimmer' | 'pulse' | 'none' | 'shimmer' | Animation of the lines. |
loading | boolean | true | When false, renders children instead of the lines. |
Every skeleton on screen shares one animation clock, so shimmers stay in sync. Skeletons are hidden from screen readers; announce the loading state once on the container.
Use cases
List placeholder
Repeat a skeleton row with the shape of the real row. The layout doesn’t jump when data arrives.
function ContactRowSkeleton() {
return (
<Item>
<Item.Leading>
<Skeleton width={40} height={40} radius="full" />
</Item.Leading>
<Item.Content>
<Skeleton width="50%" height={17} />
<Skeleton width="30%" height={13} />
</Item.Content>
</Item>
);
}
{
isLoading
? Array.from({ length: 6 }, (_, i) => <ContactRowSkeleton key={i} />)
: contacts.map(renderContact);
}Content already partly known
The title comes from the previous screen, only the body is loading. Skeleton.Text stands in for the paragraph.
<Title>{params.title}</Title>
<Skeleton loading={!article} height={180} radius="lg">
<Image source={article?.cover} />
</Skeleton>
{article ? <Text>{article.body}</Text> : <Skeleton.Text lines={3} />}Grid of cards
Same number of tiles as the first page of results, with the aspect ratio of the images.
<FlatList
numColumns={2}
data={isLoading ? placeholders : products}
renderItem={({ item }) =>
isLoading ? <ProductCardSkeleton /> : <ProductCard product={item} />
}
/>