Shipnative
ShipnativeShipnative
Sign in
GuideSeptember 2026 · 8 min read

React Native Progress Bar: Build One in 20 Lines

Searching for a React Native progress bar still turns up ProgressBarAndroid and ProgressViewIOS, both of which were pulled out of core years ago and neither of which you should add today. The replacement is not a package — it is two Views and a percentage. This guide covers that component, the one-line change that keeps its animation smooth while JavaScript is busy, the circular variant, and the accessibility props everyone forgets.

The options, briefly

ApproachDependenciesAnimationBest for
Two ViewsNoneWith ReanimatedAlmost everything
react-native-progressreact-native-svgBuilt inBars, pies, and circles at once
react-native-svg (custom)react-native-svgstrokeDashoffsetRings, arcs, segmented dials
ProgressBarAndroidCommunity packagePlatformNothing — removed from core

This is one of the few UI problems where writing it yourself is genuinely the right default. A progress bar has no gesture handling, no platform behaviour, and no accessibility tree of its own — it is a rectangle inside a rectangle. Pulling in a dependency buys you a preset colour scheme and an API you then have to learn.

The component

import { StyleSheet, View } from 'react-native';

export default function ProgressBar({ progress, height = 8, color = '#fb923c' }) {
  // clamp — a bad API response should not render a fill wider than its track
  const pct = Math.max(0, Math.min(1, progress)) * 100;

  return (
    <View
      style={[styles.track, { height, borderRadius: height / 2 }]}
      accessibilityRole="progressbar"
      accessibilityValue={{ min: 0, max: 100, now: Math.round(pct) }}
    >
      <View
        style={[styles.fill, { width: `${pct}%`, backgroundColor: color, borderRadius: height / 2 }]}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  track: {
    width: '100%',
    backgroundColor: 'rgba(255,255,255,0.08)',
    overflow: 'hidden',
  },
  fill: { height: '100%' },
});

Three details carry more weight than they look like they do. overflow: 'hidden'on the track is what keeps the fill inside the rounded corners on Android, where a child does not clip to its parent’s border radius by default. The clamp stops a server that reports 1.04 from drawing a fill that overhangs its track. And accessibilityRole plus accessibilityValue are the only reason a screen reader announces anything at all here — a styled View has no semantics on its own.

That version is correct and, for a bar that updates a few times a second from an upload callback, entirely sufficient. It has one weakness, and it shows up exactly when the app is busiest.

Why you animate scaleX and not width

width is a layout property. Changing it means the layout engine re-measures the subtree, so it cannot be driven natively — every frame has to round-trip through JavaScript. That is fine when the bar is idle and terrible during the moment a progress bar exists for, because uploading, parsing, and decoding are all things that block the JS thread. The bar stutters precisely while the work it is reporting on is happening.

transform: scaleX is not a layout property. Give the fill its full width up front, anchor it to the left with transformOrigin, and scale it — the animation then runs on the UI thread and keeps moving regardless of what JavaScript is doing:

import { useEffect } from 'react';
import { StyleSheet, View } from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
  Easing,
} from 'react-native-reanimated';

export default function AnimatedProgressBar({ progress, height = 8 }) {
  const pct = Math.max(0, Math.min(1, progress));
  const scale = useSharedValue(pct);

  useEffect(() => {
    scale.value = withTiming(pct, { duration: 280, easing: Easing.out(Easing.quad) });
  }, [pct, scale]);

  const fillStyle = useAnimatedStyle(() => ({ transform: [{ scaleX: scale.value }] }));

  return (
    <View
      style={[styles.track, { height, borderRadius: height / 2 }]}
      accessibilityRole="progressbar"
      accessibilityValue={{ min: 0, max: 100, now: Math.round(pct * 100) }}
    >
      <Animated.View style={[styles.fill, { borderRadius: height / 2 }, fillStyle]} />
    </View>
  );
}

const styles = StyleSheet.create({
  track: { width: '100%', backgroundColor: 'rgba(255,255,255,0.08)', overflow: 'hidden' },
  fill: {
    width: '100%',              // full width, then scaled down
    height: '100%',
    backgroundColor: '#fb923c',
    transformOrigin: 'left',    // otherwise it grows from the centre
  },
});

Forget transformOrigin and the bar expands outward from the middle in both directions, which is a distinctive enough bug that you will recognise it instantly the first time. On older React Native versions without that style prop, the equivalent trick is to wrap the fill and offset it by half its width with translateX. Reanimated is the general tool here — the animations guide covers the wider set of things it makes cheap.

The circular variant

Rings need a real drawing surface, which means react-native-svg. The trick is a dashed stroke with exactly one dash the length of the whole circle, then offsetting that dash to hide the part you have not completed:

import Svg, { Circle } from 'react-native-svg';

export default function ProgressRing({ progress, size = 120, stroke = 10 }) {
  const pct = Math.max(0, Math.min(1, progress));
  const r = (size - stroke) / 2;
  const circumference = 2 * Math.PI * r;

  return (
    <Svg width={size} height={size} style={{ transform: [{ rotate: '-90deg' }] }}>
      <Circle
        cx={size / 2} cy={size / 2} r={r}
        stroke="rgba(255,255,255,0.08)" strokeWidth={stroke} fill="none"
      />
      <Circle
        cx={size / 2} cy={size / 2} r={r}
        stroke="#fb923c" strokeWidth={stroke} fill="none"
        strokeDasharray={circumference}
        strokeDashoffset={circumference * (1 - pct)}
        strokeLinecap="round"
      />
    </Svg>
  );
}

The -90degrotation is not cosmetic. SVG circles start at three o’clock, and a ring that fills starting from the right-hand side reads as broken to everyone who looks at it. To animate it, wrap the second Circle with Animated.createAnimatedComponent and drive strokeDashoffset the same way you drove scaleX.

When a progress bar is the wrong component

A progress bar is a promise about time. If you do not actually know how long something takes, a determinate bar is a lie the user will catch — the familiar bar that reaches 90% and sits there is worse than no bar, because it converts waiting into distrust. Three honest alternatives: an ActivityIndicator when the wait is short and unquantifiable, an indeterminate looping bar when you want to signal ongoing work in a header, and a skeleton screen when what is loading is a screen full of content.

Reserve the real bar for the cases where the number is genuine: bytes uploaded, steps completed, questions answered. If you want the whole flow rather than the component, describe it — “an upload screen with a progress bar, a cancel button, and a retry state” — and ShipNative builds it as real React Native you can run on your phone and export.

Frequently Asked Questions

Is there a built-in progress bar in React Native?

Not any more. ProgressBarAndroid and ProgressViewIOS were both removed from React Native core and moved to community packages, and neither is actively recommended. The standard answer in 2026 is to build one from two Views — a track and a fill — which is around twenty lines and gives you full control of the styling.

How do I animate a progress bar in React Native?

Animate transform scaleX rather than width. Width is a layout property, so animating it forces a layout pass on every frame and cannot run on the native driver. scaleX is a transform, which runs on the UI thread — with Reanimated or with Animated and useNativeDriver: true — and stays smooth even while JavaScript is busy fetching or parsing.

How do I make a circular progress ring?

Draw a Circle with react-native-svg and animate strokeDashoffset. Set strokeDasharray to the full circumference (2 * pi * r), then set strokeDashoffset to circumference * (1 - progress). Rotate the whole SVG by -90 degrees so the ring starts at twelve o-clock instead of three.

Should I show a progress bar or a spinner?

Show a progress bar only when you genuinely know the proportion complete — a file upload, a multi-step form, an onboarding flow. If you do not know, an indeterminate bar or an ActivityIndicator is honest, and a skeleton screen is usually better still because it shows the shape of what is loading rather than an abstract wait.

How do I make a progress bar accessible?

Set accessibilityRole="progressbar" on the track and pass accessibilityValue with min, max, and now. Without it, VoiceOver and TalkBack announce nothing at all, because a styled View carries no semantics. It is two props and it is the difference between a usable upload screen and a silent one.

Can an AI app builder add progress indicators to my app?

Yes. Describe the flow — "an upload screen with a progress bar and a cancel button" or "a three-step onboarding with a progress indicator in the header" — and ShipNative generates the component, the animation, and the state wiring as real React Native, running live on your device.

→

React Native Skeleton Loader

When you do not know the percentage, a skeleton beats a fake bar.

Read guide →
→

React Native Image Upload

The upload flow that most often needs a real progress value.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →
ShipNative logoShipnative

Build mobile apps with AI. Describe, preview, and ship to iOS & Android in minutes.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide

Legal

FAQTerms of ServicePrivacy Policy

© 2026 ShipNative. All rights reserved.