Skeleton loading screens in React Native

By the LumioUI team · Updated

A skeleton screen shows dim blocks in the exact shape of the content that's loading. The page holds its layout, and nothing jumps when the data arrives. In React Native that's a few Views with a muted background, swapped for the real content once it's ready. The LumioUI Skeleton shapes the blocks with classes, and SkeletonSwitch switches the content on in their place.

Live demo of the LumioUI Skeleton component. It's interactive, so try it.

How to build a skeleton screen

  1. Match the layout. Each placeholder takes the size and corner radius of what will replace it: a circle for the avatar, short and long lines for the text. If the skeleton doesn't match, the swap still shifts the page.
  2. Keep it muted. Use a low-contrast block that reads in light and dark mode, such as a semi-transparent grey or a theme "muted" colour.
  3. Swap, don't stack. Render the placeholder until the data is there, then the content in the same spot. A short fade helps, but the content shouldn't slide in from somewhere else.
  4. Accessibility. Group the placeholder and label it "Loading…" so screen readers announce one thing, not a dozen empty boxes.
TSX PostSkeleton.tsx
import { View } from "react-native";

// A placeholder is just a dim block in the shape of what's coming.
export function Bone({ w, h, r = 8 }: { w: number | string; h: number; r?: number }) {
  return <View style={{ width: w, height: h, borderRadius: r, backgroundColor: "rgba(127,127,127,.18)" }} />;
}

export function PostSkeleton() {
  return (
    <View style={{ flexDirection: "row", gap: 12 }} accessibilityLabel="Loading post" accessible>
      <Bone w={40} h={40} r={20} />
      <View style={{ flex: 1, gap: 8 }}>
        <Bone w="40%" h={14} />
        <Bone w="90%" h={12} />
        <Bone w="70%" h={12} />
      </View>
    </View>
  );
}

Shimmer or still?

Many apps sweep a shimmer gradient across their skeletons. It does say "loading", but it's constant motion in the corner of the eye. It costs battery and frame time on long lists, and people with motion sensitivity notice it. A still, dim block that switches on when the content arrives feels calmer and keeps the frame rate free for scrolling. LumioUI's skeletons are still by design. If your product needs a shimmer, add it to a few hero placeholders, not to every row.

The quick way: LumioUI Skeleton

Skeleton is one dim block you shape with NativeWind classes. SkeletonSwitch renders the placeholder until loaded is true, then switches the content on in its place, row by row. It's free and runs in Expo Go.

TSX Post.tsx
import { View } from "react-native";
import { Skeleton, SkeletonSwitch } from "@/components/ui/skeleton/skeleton";
import { PostRow, type PostData } from "./PostRow";

export function Post({ post }: { post?: PostData }) {
  return (
    <SkeletonSwitch
      loaded={!!post}
      placeholder={
        <View className="flex-row gap-3">
          <Skeleton className="h-10 w-10 rounded-full" />
          <View className="flex-1 gap-2">
            <Skeleton className="h-3.5 w-2/5 rounded-md" />
            <Skeleton className="h-3 w-11/12 rounded-md" />
            <Skeleton className="h-3 w-3/4 rounded-md" />
          </View>
        </View>
      }
    >
      {post && <PostRow post={post} />}
    </SkeletonSwitch>
  );
}
  • Shape each block with classes: height, width, rounded-full for avatars.
  • SkeletonSwitch takes loaded, a placeholder and the real content as children.
  • For a whole page with no data yet, use Empty State. For an action in progress, use a Loader.

Add it to your app

LumioUI's CLI copies the component's source into your project, so it's yours to read and change. Run init once per project:

npx @lumioui/cli init
npx @lumioui/cli add skeleton

It works in Expo (SDK 54+) and bare React Native, with Reanimated 4 and NativeWind. See the installation guide and the full Skeleton API.

More React Native guides