RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
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.

Rork515UI/UX3React Native209Skeleton ScreenPerformance23

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-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.
Dev Tools2026-06-25
When an Image-Heavy Rork App Quietly Bloats Its Cache and Dies on Memory — Field Notes on Measuring and Capping
In a Rork app where images are the product, expo-image's disk cache and resident memory creep up over a session and surface as OOM crashes. Here's how I measured the bloat, where I set caps, and what I trimmed on the delivery side — with working code, in the order that actually helped.
📚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 →