Progress
A horizontal progress bar: a value 0–100 that animates, or indeterminate; role progressbar with its value.
Componentfeedback
Style
Light
Dark
Your app is ready.
Inter body text, Inter headings.
smmdlgxl
you@example.com
Continue
a0: Vega, neutral, neutral accent, default radius, Inter.Change PresetInstallation
$
npx nativecn-cli@latest add progressAlso installs: theme, use-motion.
Packages: react-native-reanimated, react-native-worklets.
Usage
Progress demo
A determinate bar that fills up, and an indeterminate one.
import { useEffect, useState } from "react";
import { View } from "react-native";
import { Progress } from "@/components/progress";
import { createStyles } from "@/theme";
const useStyles = createStyles((t) => ({
stack: { gap: t.spacing[6] },
}));
/** A determinate bar that fills up, and an indeterminate one. */
export default function ProgressDemo() {
const styles = useStyles();
const [value, setValue] = useState(13);
useEffect(() => {
const timer = setTimeout(() => setValue(66), 500);
return () => clearTimeout(timer);
}, []);
return (
<View style={styles.stack}>
<Progress value={value} accessibilityHint="Upload progress" />
<Progress indeterminate />
</View>
);
}Progress demo
A determinate bar that fills up, and an indeterminate one.
import { useEffect, useState } from "react";
import { View } from "react-native";
import { Progress } from "@/components/progress";
import { createStyles } from "@/theme";
const useStyles = createStyles((t) => ({
stack: { gap: t.spacing[6] },
}));
/** A determinate bar that fills up, and an indeterminate one. */
export default function ProgressDemo() {
const styles = useStyles();
const [value, setValue] = useState(13);
useEffect(() => {
const timer = setTimeout(() => setValue(66), 500);
return () => clearTimeout(timer);
}, []);
return (
<View style={styles.stack}>
<Progress value={value} accessibilityHint="Upload progress" />
<Progress indeterminate />
</View>
);
}Props
| Prop | Type and description |
|---|---|
value | number 0–100, clamped (default 0) |
indeterminate | boolean (default false): a bar sweeps along the track; announced busy |
Accessibility
- role progressbar with aria-valuemin 0, aria-valuemax 100 and aria-valuenow (left out while indeterminate).
- An indeterminate bar is announced busy.
- Name it with aria-label, e.g. "Step 2 of 4".
- Under Reduce Motion value changes are instant and the indeterminate sweep is still.
Notes
Value changes animate with the base motion Token. The track height comes from the progress.track Style Slot; set its width with style.
Source
The Vega Style's code, exactly as add copies it into your app.
{components}/progress.tsx
import { useEffect, type Ref } from "react";
import { View, type ViewProps } from "react-native";
import Animated, {
cancelAnimation,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated";
import { useMotion } from "@/hooks/use-motion";
import { createStyles } from "@/theme";
/** The indeterminate bar's length, as a percentage of the track. */
const INDETERMINATE_WIDTH = 40;
/** One indeterminate sweep lasts this many `slow` motion Tokens (3 × 400 = 1200ms). */
const SWEEP_IN_SLOW_STEPS = 3;
export type ProgressProps = Omit<ViewProps, "children"> & {
/** Percent complete, 0–100 (clamped). Ignored while `indeterminate`. Default `0`. */
value?: number;
/** The amount of work is unknown: a bar sweeps along the track and it is announced busy. */
indeterminate?: boolean;
ref?: Ref<View>;
};
const useStyles = createStyles((t) => ({
track: { ...{ height: t.scaleValue(8), borderRadius: t.radius.full }, overflow: "hidden", backgroundColor: t.colors.muted },
indicator: {
position: "absolute",
top: 0,
bottom: 0,
left: 0,
borderRadius: t.radius.full,
backgroundColor: t.colors.primary,
},
}));
const clamp = (value: number) => Math.min(100, Math.max(0, Number.isFinite(value) ? value : 0));
/**
* A horizontal progress bar. Determinate by default (`value` 0–100), announced as a progress
* bar with its value; changes animate with the `base` motion Token. `indeterminate` sweeps a
* bar along the track instead, and is still under Reduce Motion. `style` (e.g. a width) is
* merged last onto the track.
*/
export function Progress({ value = 0, indeterminate = false, style, ...props }: ProgressProps) {
const styles = useStyles();
const motion = useMotion();
const now = clamp(value);
// Determinate: the indicator's width in percent. Indeterminate: its left edge in percent.
const width = useSharedValue(indeterminate ? INDETERMINATE_WIDTH : now);
const left = useSharedValue(0);
useEffect(() => {
if (indeterminate) {
width.set(INDETERMINATE_WIDTH);
if (motion.reduced) {
cancelAnimation(left);
left.set(0);
return;
}
left.set(-INDETERMINATE_WIDTH);
left.set(
withRepeat(
withTiming(100, {
...motion.timing("slow"),
duration: motion.duration("slow") * SWEEP_IN_SLOW_STEPS,
}),
-1,
),
);
return () => cancelAnimation(left);
}
cancelAnimation(left);
left.set(0);
width.set(withTiming(now, motion.timing("base")));
}, [indeterminate, now, motion, width, left]);
const indicator = useAnimatedStyle(() => ({
width: `${width.get()}%`,
left: `${left.get()}%`,
}));
return (
<View
role="progressbar"
accessible
aria-busy={indeterminate}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={indeterminate ? undefined : Math.round(now)}
style={[styles.track, style]}
{...props}
>
<Animated.View style={[styles.indicator, indicator]} />
</View>
);
}{components}/progress.tsx
import { useEffect, type Ref } from "react";
import { View, type ViewProps } from "react-native";
import Animated, {
cancelAnimation,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated";
import { useMotion } from "@/hooks/use-motion";
import { createStyles } from "@/theme";
/** The indeterminate bar's length, as a percentage of the track. */
const INDETERMINATE_WIDTH = 40;
/** One indeterminate sweep lasts this many `slow` motion Tokens (3 × 400 = 1200ms). */
const SWEEP_IN_SLOW_STEPS = 3;
export type ProgressProps = Omit<ViewProps, "children"> & {
/** Percent complete, 0–100 (clamped). Ignored while `indeterminate`. Default `0`. */
value?: number;
/** The amount of work is unknown: a bar sweeps along the track and it is announced busy. */
indeterminate?: boolean;
ref?: Ref<View>;
};
const useStyles = createStyles((t) => ({
track: { ...{ height: t.scaleValue(4), borderRadius: t.radius.full }, overflow: "hidden", backgroundColor: t.colors.muted },
indicator: {
position: "absolute",
top: 0,
bottom: 0,
left: 0,
borderRadius: t.radius.full,
backgroundColor: t.colors.primary,
},
}));
const clamp = (value: number) => Math.min(100, Math.max(0, Number.isFinite(value) ? value : 0));
/**
* A horizontal progress bar. Determinate by default (`value` 0–100), announced as a progress
* bar with its value; changes animate with the `base` motion Token. `indeterminate` sweeps a
* bar along the track instead, and is still under Reduce Motion. `style` (e.g. a width) is
* merged last onto the track.
*/
export function Progress({ value = 0, indeterminate = false, style, ...props }: ProgressProps) {
const styles = useStyles();
const motion = useMotion();
const now = clamp(value);
// Determinate: the indicator's width in percent. Indeterminate: its left edge in percent.
const width = useSharedValue(indeterminate ? INDETERMINATE_WIDTH : now);
const left = useSharedValue(0);
useEffect(() => {
if (indeterminate) {
width.set(INDETERMINATE_WIDTH);
if (motion.reduced) {
cancelAnimation(left);
left.set(0);
return;
}
left.set(-INDETERMINATE_WIDTH);
left.set(
withRepeat(
withTiming(100, {
...motion.timing("slow"),
duration: motion.duration("slow") * SWEEP_IN_SLOW_STEPS,
}),
-1,
),
);
return () => cancelAnimation(left);
}
cancelAnimation(left);
left.set(0);
width.set(withTiming(now, motion.timing("base")));
}, [indeterminate, now, motion, width, left]);
const indicator = useAnimatedStyle(() => ({
width: `${width.get()}%`,
left: `${left.get()}%`,
}));
return (
<View
role="progressbar"
accessible
aria-busy={indeterminate}
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={indeterminate ? undefined : Math.round(now)}
style={[styles.track, style]}
{...props}
>
<Animated.View style={[styles.indicator, indicator]} />
</View>
);
}
