RORK LABJP
PRICE — Rork Max spans $200 to $1,800 per month, with the upper tiers aimed at heavier builders and teamsFREE — The free tier lands at roughly five prompts per week, enough to try it but not to build on continuouslySHIP — App Store publishing is automated through builds, certificates, and submission, so you can ship an iOS app without a Mac or XcodeSIM — A browser-streamed simulator lets you watch your app run in a real Apple environment from your own browserNATIVE — It reaches HealthKit, ARKit and LiDAR, NFC, Dynamic Island, and Metal 3D — territory React Native cannot touchFUNDING — Rork raised a $15M seed led by Left Lane Capital, announced April 9, 2026, and acquired app builder PaperlinePRICE — Rork Max spans $200 to $1,800 per month, with the upper tiers aimed at heavier builders and teamsFREE — The free tier lands at roughly five prompts per week, enough to try it but not to build on continuouslySHIP — App Store publishing is automated through builds, certificates, and submission, so you can ship an iOS app without a Mac or XcodeSIM — A browser-streamed simulator lets you watch your app run in a real Apple environment from your own browserNATIVE — It reaches HealthKit, ARKit and LiDAR, NFC, Dynamic Island, and Metal 3D — territory React Native cannot touchFUNDING — Rork raised a $15M seed led by Left Lane Capital, announced April 9, 2026, and acquired app builder Paperline
Articles/Dev Tools
Dev Tools/2026-04-25Intermediate

Skeleton Screens in Rork Apps — Practical Patterns for Better Perceived Performance

If your Rork app still shows a spinner on a blank screen during load, you are leaving a lot of perceived performance on the table. Here is how I drop skeleton screens and shimmer effects into Rork-generated React Native code without over-engineering it.

Rork532UI/UX3React Native224Skeleton ScreenPerformance25

The most common piece of feedback I get when I put a freshly built Rork app in front of testers is: "those first few seconds after launch feel a little uncertain." The app is working. Data is loading. But a blank screen with a spinner in the middle makes people wonder whether something is stuck.

Skeleton screens are the classic fix for that feeling. They don't actually shorten the load time, but they consistently make users stay longer and complain less. In this post I want to walk through the exact patterns I use in Rork-generated React Native projects — small enough to drop in, but solid enough to ship.

Why ActivityIndicator alone isn't enough

The code Rork generates often falls back on ActivityIndicator, which is better than nothing but carries very little information. It just says "something is happening." Skeleton screens go further: they pre-announce the layout that is about to arrive, which helps users anchor their attention before the real content shows up.

I use a simple rule of thumb on every Rork project: any screen that may take more than a second to settle gets a skeleton. Home screens, profile pages, and lists with more than 10 visible cells benefit the most. In testing it's obvious — people describe the app as "snappier" even though the measured load time hasn't changed.

The smallest possible skeleton

Start static. A handful of grey rectangles already beats a spinner.

import { View, StyleSheet } from 'react-native';
 
// Minimal placeholder unit
export function SkeletonBlock({ width, height, radius = 8 }: {
  width: number | string;
  height: number;
  radius?: number;
}) {
  return (
    <View
      style={[
        styles.block,
        { width, height, borderRadius: radius },
      ]}
    />
  );
}
 
const styles = StyleSheet.create({
  block: {
    backgroundColor: '#e5e7eb', // equivalent to Tailwind gray-200
  },
});

Compose it into a profile card skeleton and you already have something much more reassuring than an empty screen:

import { View, StyleSheet } from 'react-native';
import { SkeletonBlock } from './SkeletonBlock';
 
export function ProfileSkeleton() {
  return (
    <View style={styles.card}>
      <SkeletonBlock width={64} height={64} radius={32} />
      <View style={styles.body}>
        <SkeletonBlock width="70%" height={16} />
        <View style={{ height: 8 }} />
        <SkeletonBlock width="40%" height={12} />
      </View>
    </View>
  );
}
 
const styles = StyleSheet.create({
  card: { flexDirection: 'row', padding: 16, alignItems: 'center' },
  body: { flex: 1, marginLeft: 12 },
});

That's it. No animation yet, but the blank screen is gone and users now see a layout preview. Honestly, for many Rork apps I stop here on the first iteration and only add motion if testing shows it's worth it.

Adding a shimmer effect without killing performance

Once static placeholders feel too still, add a shimmer — a highlight that sweeps across each block. React Native Reanimated 3 is the right tool because the animation runs on the UI thread instead of fighting the JS bridge.

import { useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withRepeat,
  withTiming,
  Easing,
} from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
 
// Skeleton block with a moving highlight
export function ShimmerBlock({ width, height, radius = 8 }: {
  width: number;
  height: number;
  radius?: number;
}) {
  const translateX = useSharedValue(-width);
 
  useEffect(() => {
    // Loop from -width to +width every 1.2 seconds
    translateX.value = withRepeat(
      withTiming(width, { duration: 1200, easing: Easing.linear }),
      -1,
      false,
    );
  }, [width]);
 
  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }],
  }));
 
  return (
    <View style={[styles.container, { width, height, borderRadius: radius }]}>
      <Animated.View style={[StyleSheet.absoluteFill, style]}>
        <LinearGradient
          colors={['transparent', 'rgba(255,255,255,0.6)', 'transparent']}
          start={{ x: 0, y: 0 }}
          end={{ x: 1, y: 0 }}
          style={StyleSheet.absoluteFill}
        />
      </Animated.View>
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: {
    backgroundColor: '#e5e7eb',
    overflow: 'hidden', // clip the highlight inside rounded corners
  },
});

Three details matter: keep the animation on the UI thread via useSharedValue and withRepeat, fade the gradient to transparent on both ends so the highlight slides in cleanly, and set overflow: 'hidden' so the shimmer respects your border radius. Skip any of those and you get jagged corners or janky motion.

If you want to go deeper on animation techniques, I covered the broader patterns in Advanced Animation Patterns with Rork, Reanimated and Gesture Handler.

Lists need skeletons that match reality

The most common mistake I see in other people's Rork apps is skeleton lists with too few items. If the actual list shows seven cells but the skeleton only has three, the screen visibly jumps down when the real data lands and users lose their place.

A few guidelines that keep list skeletons feeling trustworthy:

  • Render at least as many skeleton rows as can fit on screen — a quick estimate is flatListHeight / itemHeight plus a little extra.
  • Match the real row height exactly. Being off by even 4 pixels makes the transition visibly shift.
  • Start from the top and preserve the real spacing between rows.
import { FlatList, View } from 'react-native';
import { SkeletonBlock } from './SkeletonBlock';
 
const SKELETON_ITEMS = Array.from({ length: 8 }); // fit-to-screen + buffer
 
export function ListSkeleton() {
  return (
    <FlatList
      data={SKELETON_ITEMS}
      keyExtractor={(_, i) => `sk-${i}`}
      renderItem={() => (
        <View style={{ padding: 16, flexDirection: 'row', alignItems: 'center' }}>
          <SkeletonBlock width={48} height={48} radius={24} />
          <View style={{ flex: 1, marginLeft: 12 }}>
            <SkeletonBlock width="80%" height={14} />
            <View style={{ height: 6 }} />
            <SkeletonBlock width="50%" height={12} />
          </View>
        </View>
      )}
      scrollEnabled={false}
    />
  );
}

scrollEnabled={false} is intentional: if a user accidentally scrolls during the loading state, the cost of rendering real data when it arrives spikes on older Android hardware. That one line alone has saved me from janky hand-offs more than once.

Switching from skeleton to real data cleanly

The hand-off between the skeleton and the real content is where most flicker creeps in. Two techniques I reach for on every project:

1. Enforce a minimum display time. If data arrives in 40 ms, the skeleton flashes for a single frame and looks like a glitch. Holding it visible for at least 200 to 300 ms smooths things out.

import { useEffect, useState } from 'react';
 
export function useDelayedReady(ready: boolean, minMs = 250) {
  const [show, setShow] = useState(ready);
  useEffect(() => {
    if (!ready) {
      setShow(false);
      return;
    }
    const id = setTimeout(() => setShow(true), minMs);
    return () => clearTimeout(id);
  }, [ready, minMs]);
  return show;
}

Wire it up like this:

const { data, isLoading } = useProfile();
const ready = useDelayedReady(!isLoading);
 
return ready ? <Profile data={data} /> : <ProfileSkeleton />;

2. Fade the real view in. Wrap the real content in an Animated.View with a 150 ms fade-in. The cost is negligible and the visual seam between skeleton and data softens noticeably.

Pitfalls worth avoiding

A few mistakes I've had to flag during code review:

  • Forgetting image placeholders. Text gets a skeleton, but the image slot does not — then the image pops in and the layout jumps. Lock width and height on the Image parent and render a matching skeleton there.
  • Overusing shimmer. Running 50+ simultaneous shimmer animations on older Android devices drops frames. For lists, only animate currently visible cells; FlatList's onViewableItemsChanged is perfect for this.
  • Using brand colors for skeletons. A soft version of your brand color looks "designed" but can be confused with real content. Stay with neutral grays — something around #2a2a2a for dark mode — and let the real UI stand out when it arrives.

The sibling topic of handling bad connectivity is in Designing for Flaky Networks in Rork Apps — UX and Error States. Skeletons and error states are two sides of the same coin, so they pair well.

What to try next

You don't need to retrofit every screen in a day. Pick your home screen and your most data-heavy list, replace the spinner with a skeleton, and run one round of tester feedback. That alone is usually enough to convince you to keep going.

If you want to widen the lens from loading states to UX patterns as a whole, I put together The Rork App UX Design Patterns Complete Guide for that. Thanks for reading — I hope your next build feels a little kinder to the people using it.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-03-30
Rork × React Native Reanimated & Gesture Handler — Building 60fps Animations and Advanced Gesture Interactions
Keeping Reanimated 3 and Gesture Handler at 60fps, told through the production pitfalls I hit running wallpaper and calming-tone apps: delayed runOnJS, useDerivedValue caching, FlashList pairing, and missing cancelAnimation — each with real measurements.
Dev Tools2026-07-24
Collapsing Duplicate Requests Into One: A Reference-Counted Single-Flight Layer
When several components fire the same API call at launch, you get a burst of identical requests. Here is a single-flight layer that shares one in-flight promise instead: how to build the key that decides the folding, the trap of handing out a failed promise forever, the trap of one caller's abort cancelling everyone, and the test that keeps it all from regressing, with the real network numbers alongside.
Dev Tools2026-07-10
Adding React Compiler to Expo Let Me Delete 41 Hand-Written memo Calls
I enabled React Compiler on Rork-generated React Native screens and measured the rerender counts with Profiler. Here is how I decided which memo and useCallback calls were safe to delete, how to find the components the compiler bailed out on, and how to catch regressions in CI.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →