NovaUI
Components

Skeleton

A placeholder loading animation to indicate content is being loaded.

The Skeleton component displays a pulsing placeholder while content is loading. It uses React Native's Animated API for a smooth opacity pulse animation and is styled with NativeWind for easy customization.

Preview

Installation

npx novaui-cli add skeleton

Import

import { Skeleton } from 'novaui-components'

Basic Usage

The Skeleton renders a pulsing rounded-md bg-muted block. Control its shape and size with className:

import { Skeleton } from 'novaui-components'
import { View } from 'react-native'

export function BasicSkeleton() {
  return (
    <View className="gap-3">
      <Skeleton className="h-4 w-full" />
      <Skeleton className="h-4 w-4/5" />
      <Skeleton className="h-4 w-3/5" />
    </View>
  )
}

Shapes

Rectangle

Default shape for text lines or block content:

<Skeleton className="h-[125px] w-[250px] rounded-md" />

Circle

Perfect for avatar placeholders:

<Skeleton className="h-12 w-12 rounded-full" />

Rounded

For buttons or badges:

<Skeleton className="h-10 w-24 rounded-md" />

Common Patterns

Card Skeleton

A loading placeholder for a card layout:

import { Skeleton } from 'novaui-components'
import { Card, CardHeader, CardContent, CardFooter } from 'novaui-components'
import { View } from 'react-native'

export function CardSkeleton() {
  return (
    <Card>
      <CardHeader>
        <Skeleton className="h-6 w-3/4" />
        <Skeleton className="h-4 w-1/2" />
      </CardHeader>
      <CardContent>
        <View className="gap-2">
          <Skeleton className="h-4 w-full" />
          <Skeleton className="h-4 w-full" />
          <Skeleton className="h-4 w-4/5" />
        </View>
      </CardContent>
      <CardFooter>
        <Skeleton className="h-10 w-24" />
      </CardFooter>
    </Card>
  )
}

Profile Skeleton with Avatar

A loading state that transitions into a real Avatar once data is loaded. This shows how Skeleton and Avatar work together:

Loading...
Loaded
Avatar
John Doe@johndoe
import { useState, useEffect } from 'react'
import { View, Text } from 'react-native'
import { Skeleton } from 'novaui-components'
import { Avatar, AvatarImage, AvatarFallback } from 'novaui-components'

export function ProfileWithSkeleton() {
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    // Simulate data fetching
    const timer = setTimeout(() => setIsLoading(false), 2000)
    return () => clearTimeout(timer)
  }, [])

  if (isLoading) {
    return (
      <View className="flex-row items-center gap-4">
        <Skeleton className="h-14 w-14 rounded-full" />
        <View className="gap-2 flex-1">
          <Skeleton className="h-5 w-2/3" />
          <Skeleton className="h-4 w-1/3" />
        </View>
      </View>
    )
  }

  return (
    <View className="flex-row items-center gap-4">
      <Avatar size="lg">
        <AvatarImage src="https://example.com/avatar.jpg" />
        <AvatarFallback>JD</AvatarFallback>
      </Avatar>
      <View>
        <Text className="text-base font-medium">John Doe</Text>
        <Text className="text-sm text-muted-foreground">@johndoe</Text>
      </View>
    </View>
  )
}

List Skeleton

A loading placeholder for a list of items:

import { Skeleton } from 'novaui-components'
import { View } from 'react-native'

export function ListSkeleton() {
  return (
    <View className="gap-4">
      {[1, 2, 3].map((i) => (
        <View key={i} className="flex-row items-center gap-3">
          <Skeleton className="h-10 w-10 rounded-full" />
          <View className="gap-1.5 flex-1">
            <Skeleton className="h-4 w-3/4" />
            <Skeleton className="h-3 w-1/2" />
          </View>
        </View>
      ))}
    </View>
  )
}

Props API

Skeleton Props

Extends React.ComponentPropsWithoutRef<typeof View>:

PropTypeDefaultDescription
classNamestring-CSS classes to control size, shape, and custom styling

The base classes applied are rounded-md bg-muted with a pulsing opacity animation.

Animation

The Skeleton uses React Native's built-in Animated API:

  • Pulse loop: Opacity animates between 0.5 and 1.0 continuously
  • Duration: 1000ms per half-cycle (2 seconds full loop)
  • Easing: Easing.out(Easing.ease) for fade-in, Easing.in(Easing.ease) for fade-out
  • Native driver: Uses useNativeDriver: true for optimal performance

Best Practices

  1. Match the layout: Shape your skeletons to match the actual content dimensions — this reduces layout shift when content loads

  2. Use rounded-full for avatars: Apply rounded-full for circular placeholders

  3. Vary widths for text: Use different widths (w-full, w-3/4, w-1/2) across lines for a realistic text appearance

  4. Group with containers: Wrap skeletons in the same layout components (Card, View) as the real content

  5. Don't overuse: Only show skeletons for content that takes noticeable time to load

  • Use inside Card for card loading states
  • Combine with Avatar dimensions for profile placeholders
  • Pair with conditional rendering to swap skeletons for real content

On this page