UI Components Easy
Progress Bar
Linear progress indicators — determinate with label, indeterminate animated, stepped segments, and multi-color stacked bars.
Open in Lab
MCP
css vanilla-js
Targets: JS HTML React Native
Expo Snack
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, system-ui, sans-serif;
background: #050910;
color: #f2f6ff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.demo {
width: 100%;
max-width: 520px;
}
.demo-title {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 0.375rem;
}
.demo-sub {
color: #475569;
font-size: 0.875rem;
margin-bottom: 2rem;
}
.section {
margin-bottom: 2rem;
}
.section-label {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #475569;
margin-bottom: 0.75rem;
}
.bars {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.bar-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.bar-row .progress {
flex: 1;
}
.bar-pct {
font-size: 0.75rem;
color: #475569;
width: 2.5rem;
text-align: right;
flex-shrink: 0;
}
/* ── Track ── */
.progress {
height: 8px;
border-radius: 999px;
background: #0d1117;
overflow: hidden;
position: relative;
}
/* ── Fill ── */
.progress-fill {
height: 100%;
border-radius: 999px;
background: #38bdf8;
width: 0%;
transition: width 0.8s cubic-bezier(0.23, 1, 0.32, 1);
}
.progress--green .progress-fill {
background: #22c55e;
}
.progress--amber .progress-fill {
background: #f59e0b;
}
.progress--red .progress-fill {
background: #ef4444;
}
/* ── Indeterminate ── */
.progress--indeterminate {
overflow: hidden;
}
.progress-fill--indeterminate {
width: 40% !important;
animation: indeterminate 1.4s ease-in-out infinite;
}
@keyframes indeterminate {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(350%);
}
}
@media (prefers-reduced-motion: reduce) {
.progress-fill--indeterminate {
animation: none;
width: 60% !important;
}
}
/* ── Steps ── */
.progress-steps {
display: flex;
gap: 0.375rem;
margin-bottom: 0.625rem;
}
.step {
flex: 1;
height: 6px;
border-radius: 999px;
background: #0d1117;
}
.step--done {
background: #38bdf8;
}
.step--active {
background: linear-gradient(90deg, #38bdf8, #818cf8);
}
.steps-label {
font-size: 0.8rem;
color: #64748b;
}
/* ── Stacked ── */
.progress--stacked {
height: 12px;
display: flex;
overflow: hidden;
gap: 0;
border-radius: 999px;
}
.progress-segment {
height: 100%;
transition: width 0.8s cubic-bezier(0.23, 1, 0.32, 1);
}
.progress-segment--blue {
background: #38bdf8;
}
.progress-segment--purple {
background: #a78bfa;
}
.progress-segment--green {
background: #22c55e;
}
.stacked-legend {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 0.75rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.75rem;
color: #64748b;
}
.legend-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.15);
flex-shrink: 0;
}
.legend-dot--blue {
background: #38bdf8;
}
.legend-dot--purple {
background: #a78bfa;
}
.legend-dot--green {
background: #22c55e;
}(function () {
var reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".progress-fill[data-value]").forEach(function (fill) {
var target = parseFloat(fill.dataset.value) || 0;
if (reduced) {
fill.style.width = target + "%";
return;
}
requestAnimationFrame(function () {
setTimeout(function () {
fill.style.width = target + "%";
}, 100);
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Progress Bar</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="demo">
<h1 class="demo-title">Progress Bar</h1>
<p class="demo-sub">Determinate, indeterminate, stepped, and stacked variants.</p>
<section class="section">
<p class="section-label">Determinate</p>
<div class="bars">
<div class="bar-row">
<div class="progress" role="progressbar" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100">
<div class="progress-fill" data-value="25"></div>
</div>
<span class="bar-pct">25%</span>
</div>
<div class="bar-row">
<div class="progress progress--green" role="progressbar" aria-valuenow="60">
<div class="progress-fill" data-value="60"></div>
</div>
<span class="bar-pct">60%</span>
</div>
<div class="bar-row">
<div class="progress progress--amber" role="progressbar" aria-valuenow="80">
<div class="progress-fill" data-value="80"></div>
</div>
<span class="bar-pct">80%</span>
</div>
<div class="bar-row">
<div class="progress progress--red" role="progressbar" aria-valuenow="95">
<div class="progress-fill" data-value="95"></div>
</div>
<span class="bar-pct">95%</span>
</div>
</div>
</section>
<section class="section">
<p class="section-label">Indeterminate</p>
<div class="progress progress--indeterminate" role="progressbar" aria-label="Loading">
<div class="progress-fill progress-fill--indeterminate"></div>
</div>
</section>
<section class="section">
<p class="section-label">Stepped (3 of 5)</p>
<div class="progress-steps">
<div class="step step--done"></div>
<div class="step step--done"></div>
<div class="step step--active"></div>
<div class="step"></div>
<div class="step"></div>
</div>
<p class="steps-label">Step 3 of 5 — Payment details</p>
</section>
<section class="section">
<p class="section-label">Stacked / multi-segment</p>
<div class="progress progress--stacked" role="meter" aria-label="Storage breakdown">
<div class="progress-segment progress-segment--blue" style="width:45%" title="Documents 45%"></div>
<div class="progress-segment progress-segment--purple" style="width:25%" title="Media 25%"></div>
<div class="progress-segment progress-segment--green" style="width:15%" title="Backups 15%"></div>
</div>
<div class="stacked-legend">
<span class="legend-item"><i class="legend-dot legend-dot--blue"></i>Documents 45%</span>
<span class="legend-item"><i class="legend-dot legend-dot--purple"></i>Media 25%</span>
<span class="legend-item"><i class="legend-dot legend-dot--green"></i>Backups 15%</span>
<span class="legend-item"><i class="legend-dot"></i>Free 15%</span>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>import React, { useEffect, useRef } from "react";
import { Animated, Easing, StyleSheet, Text, View } from "react-native";
type BarColor = "default" | "success" | "warning" | "error";
const COLOR_MAP: Record<BarColor, string> = {
default: "#818cf8",
success: "#34d399",
warning: "#fbbf24",
error: "#f87171",
};
interface ProgressBarProps {
value?: number;
color?: BarColor;
indeterminate?: boolean;
height?: number;
label?: string;
}
function ProgressBar({
value = 0,
color = "default",
indeterminate = false,
height = 8,
label,
}: ProgressBarProps) {
const fillWidth = useRef(new Animated.Value(0)).current;
const indeterminateAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (indeterminate) {
Animated.loop(
Animated.sequence([
Animated.timing(indeterminateAnim, {
toValue: 1,
duration: 1200,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
Animated.timing(indeterminateAnim, {
toValue: 0,
duration: 1200,
easing: Easing.inOut(Easing.ease),
useNativeDriver: true,
}),
])
).start();
} else {
Animated.timing(fillWidth, {
toValue: Math.min(100, Math.max(0, value)),
duration: 600,
easing: Easing.out(Easing.cubic),
useNativeDriver: false,
}).start();
}
}, [value, indeterminate, fillWidth, indeterminateAnim]);
const barColor = COLOR_MAP[color];
if (indeterminate) {
const translateX = indeterminateAnim.interpolate({
inputRange: [0, 1],
outputRange: [-120, 300],
});
return (
<View style={styles.barWrapper}>
{label && <Text style={styles.label}>{label}</Text>}
<View style={[styles.track, { height }]}>
<Animated.View
style={[
styles.indeterminateFill,
{
height,
backgroundColor: barColor,
transform: [{ translateX }],
},
]}
/>
</View>
</View>
);
}
const width = fillWidth.interpolate({
inputRange: [0, 100],
outputRange: ["0%", "100%"],
});
return (
<View style={styles.barWrapper}>
<View style={styles.labelRow}>
{label && <Text style={styles.label}>{label}</Text>}
<Text style={[styles.percentage, { color: barColor }]}>{value}%</Text>
</View>
<View style={[styles.track, { height }]}>
<Animated.View
style={[
styles.fill,
{
width,
height,
backgroundColor: barColor,
},
]}
/>
</View>
</View>
);
}
export default function App() {
return (
<View style={styles.container}>
<Text style={styles.header}>Progress</Text>
<Text style={styles.subheader}>Animated progress indicators</Text>
<View style={styles.demos}>
<ProgressBar value={72} color="default" label="Upload" height={10} />
<ProgressBar value={100} color="success" label="Complete" height={10} />
<ProgressBar value={45} color="warning" label="Storage" height={10} />
<ProgressBar value={88} color="error" label="CPU Usage" height={10} />
<ProgressBar indeterminate color="default" label="Loading..." height={6} />
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0f172a",
paddingTop: 60,
paddingHorizontal: 20,
},
header: {
color: "#f8fafc",
fontSize: 28,
fontWeight: "700",
},
subheader: {
color: "#64748b",
fontSize: 14,
marginTop: 4,
marginBottom: 32,
},
demos: {
gap: 24,
},
barWrapper: {
width: "100%",
},
labelRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 8,
},
label: {
color: "#cbd5e1",
fontSize: 14,
fontWeight: "500",
marginBottom: 8,
},
percentage: {
fontSize: 14,
fontWeight: "600",
},
track: {
width: "100%",
backgroundColor: "#1e293b",
borderRadius: 999,
overflow: "hidden",
},
fill: {
borderRadius: 999,
},
indeterminateFill: {
width: 120,
borderRadius: 999,
},
});Progress Bar
Communicate progress, loading state, or multi-step completion.
Variants
| Variant | Use case |
|---|---|
| Determinate | Known completion percentage |
| Indeterminate | Unknown duration loading |
| Stepped | Multi-step form / onboarding |
| Stacked | Breakdown of multiple segments |
Features
- Animated fill-in on page load via
requestAnimationFrame - Color variants (blue, green, amber, red)
- Percentage label inside or outside the track
prefers-reduced-motionrespected — instant fill when opted out