Animated fill bar with indeterminate sweep mode.
components/ui/progress/progress.tsx@lumioui/core@latestreact-native-reanimated@*Live preview — the real component, driven by the props below.
import { Progress } from "@/components/ui/progress/progress";
export function Example() {
return (
<Progress />
);
} import * as React from "react";
import { forwardRef } from "react";
import { View, type ViewProps } from "react-native";
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withRepeat,
Easing,
} from "react-native-reanimated";
import { cn, useReducedMotion, timing, ease } from "@lumioui/core";
import { Glass } from "../glass";
export interface ProgressProps extends ViewProps {
/** 0–100 */
value: number;
className?: string;
/** Fill bar classes. */
indicatorClassName?: string;
/** Unknown progress — shows a looping sliding bar instead of the fill. */
indeterminate?: boolean;
/** Liquid-glass lane — a taller frosted pill instead of the hairline. */
glass?: boolean;
}
/** Determinate fill — timing.normal (250ms) on the ease.out decel token:
* one fluid glide to the new value, not a state snap. */
const FILL_TIMING = { duration: timing.normal.duration, easing: Easing.bezier(...ease.out) };
/** Indeterminate sweep — ambient loop, intentionally outside the UI-motion
* band (same reasoning as skeleton's shimmer cycle). Rides the ease.inOut
* token so the bar accelerates in and decelerates out symmetrically. */
const SWEEP_CYCLE = { duration: 1200, easing: Easing.bezier(...ease.inOut) };
/** Progress — animated fill bar. Animates smoothly to new values. */
export const Progress = forwardRef<React.ElementRef<typeof View>, ProgressProps>(
function Progress(
{ value, className, indicatorClassName, indeterminate, glass = false, onLayout, ...props },
ref
) {
const clamped = Math.max(0, Math.min(100, value));
const reduceMotion = useReducedMotion();
const width = useSharedValue(clamped);
const [trackW, setTrackW] = React.useState(0);
const slide = useSharedValue(0);
React.useEffect(() => {
// Reduced motion: jump straight to the value — no gliding travel.
width.value = reduceMotion ? clamped : withTiming(clamped, FILL_TIMING);
}, [clamped, width, reduceMotion]);
React.useEffect(() => {
if (indeterminate && trackW > 0) {
if (reduceMotion) {
// Reduced motion: static band, no looping travel (skeleton pattern).
slide.value = 0;
return;
}
const barW = trackW * 0.4;
slide.value = -barW;
slide.value = withRepeat(
withTiming(trackW, SWEEP_CYCLE),
-1,
false
);
} else {
slide.value = 0;
}
}, [indeterminate, trackW, slide, reduceMotion]);
const fillStyle = useAnimatedStyle(() => ({ width: `${width.value}%` }));
const slideStyle = useAnimatedStyle(() => ({
width: trackW * 0.4,
transform: [{ translateX: slide.value }],
}));
return (
<View
ref={ref}
className={cn(glass ? "w-full overflow-hidden rounded-full" : "h-2 w-full overflow-hidden rounded-full bg-secondary", className)}
style={glass ? { height: 14 } : undefined}
onLayout={(e) => {
setTrackW(e.nativeEvent.layout.width);
onLayout?.(e);
}}
accessibilityRole="progressbar"
accessibilityValue={indeterminate ? undefined : { min: 0, max: 100, now: clamped }}
accessibilityState={indeterminate ? { busy: true } : undefined}
// RNW drops the accessibilityValue/accessibilityState objects —
// direct aria-* props keep web screen readers informed (switch pattern).
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={indeterminate ? undefined : clamped}
aria-busy={indeterminate ? true : undefined}
{...props}
>
{glass ? (
<Glass
effect="clear"
className="absolute inset-0 rounded-full"
pointerEvents="none"
/>
) : null}
{indeterminate ? (
<Animated.View
style={[{ height: "100%" }, slideStyle]}
className={cn("rounded-full bg-primary", indicatorClassName)}
/>
) : (
<Animated.View
style={fillStyle}
className={cn("h-full rounded-full bg-primary", indicatorClassName)}
/>
)}
</View>
);
}
);
Progress.displayName = "Progress";
| value | number | 0–100 | |
| className? | string | ||
| indicatorClassName? | string | Fill bar classes. | |
| indeterminate? | boolean | Unknown progress — shows a looping sliding bar instead of the fill. | |
| glass? | boolean | Liquid-glass lane — a taller frosted pill instead of the hairline. |
Animated fill bar with indeterminate sweep mode.
import { ProgressAnimated } from "@/components/ui/progress/variants/animated";
export function Example() {
return (
<ProgressAnimated />
);
} import * as React from "react";
import { View } from "react-native";
import { Progress } from "../progress";
import { Text } from "../../text/text";
import { Glass } from "../../glass/glass";
/** ProgressAnimated — delivery tracker card with a live-ticking bar and
* supporting context so the bar reads as part of a real screen. */
export function ProgressAnimated() {
const [value, setValue] = React.useState(0);
React.useEffect(() => {
const id = setInterval(() => setValue((v) => (v >= 100 ? 0 : v + 10)), 800);
return () => clearInterval(id);
}, []);
return (
<View className="w-full gap-3">
<Glass interactive curvature="edge" className="w-full gap-4 rounded-2xl p-4">
<View className="flex-row items-center justify-between">
<View className="gap-0.5">
<Text variant="small" className="font-semibold">Order #1042</Text>
<Text variant="muted">Tonkotsu ×2 · Sakura Ramen</Text>
</View>
<Glass interactive curvature="edge" className="rounded-full px-2.5 py-1">
<Text className="text-xs font-semibold text-primary">On its way</Text>
</Glass>
</View>
<View className="gap-1.5">
<Progress glass value={value} className="w-full" />
<View className="flex-row justify-between">
<Text variant="muted">Picked up</Text>
<Text variant="muted">arriving 6:45 PM · {value}%</Text>
</View>
</View>
<View className="flex-row items-center gap-3 border-t border-border pt-3">
<Glass interactive curvature="edge" className="h-8 w-8 items-center justify-center rounded-full">
<Text className="text-xs font-semibold">DR</Text>
</Glass>
<View className="flex-1">
<Text variant="small">Diego Ruiz is 8 min away</Text>
<Text variant="muted">Scooter · Alfama</Text>
</View>
</View>
</Glass>
</View>
);
}
Animated fill bar with indeterminate sweep mode.
import { ProgressGlass } from "@/components/ui/progress/variants/glass";
export function Example() {
return (
<ProgressGlass />
);
} import * as React from "react";
import { View, Text } from "react-native";
import { Progress } from "../progress";
import { Ambient } from "../../glass/ambient";
/** ProgressGlass — a frosted glass lane tracking an upload over a night photo. */
export function ProgressGlass() {
const [v, setV] = React.useState(12);
React.useEffect(() => {
const t = setInterval(() => setV((x) => (x >= 96 ? 12 : x + 7)), 900);
return () => clearInterval(t);
}, []);
return (
<View className="relative h-[420px] w-full overflow-hidden rounded-3xl">
<Ambient seed="prog" />
<View className="absolute inset-x-6 bottom-8 gap-2">
<View className="flex-row items-baseline justify-between">
<Text className="text-sm font-semibold text-white">Uploading</Text>
<Text className="text-xs text-white/80">{v}%</Text>
</View>
<Progress glass value={v} />
</View>
<View className="absolute left-5 top-5">
<Text className="text-[10px] font-semibold uppercase tracking-widest text-white/90">
GLASS LANE
</Text>
</View>
</View>
);
}
Animated fill bar with indeterminate sweep mode.
import { ProgressIndeterminate } from "@/components/ui/progress/variants/indeterminate";
export function Example() {
return (
<ProgressIndeterminate />
);
} import * as React from "react";
import { View } from "react-native";
import { Progress } from "../progress";
import { Text } from "../../text/text";
import { Glass } from "../../glass/glass";
/** ProgressIndeterminate — static fill + indeterminate loading bar. */
export function ProgressIndeterminate() {
return (
<View className="w-full gap-3">
<Glass interactive curvature="edge" className="w-full gap-4 rounded-2xl p-4">
<View className="gap-2">
<Text variant="small" className="font-semibold">Photo backup</Text>
<Progress glass value={66} className="w-full" />
<Text variant="muted">164 of 248 photos</Text>
</View>
<View className="gap-2 border-t border-border pt-3">
<Text variant="small" className="font-semibold">Finding nearby spots</Text>
<Progress glass value={0} indeterminate className="w-full" />
</View>
</Glass>
</View>
);
}