22 CSS Progress Bars

8 Pure CSS14 Light JSPublished 10 new designs · Updated on

A CSS progress bar communicates a value — bytes uploaded, form step, storage used, password strength, tokens streamed. These 22 hand-coded patterns cover the native <progress> element, role="progressbar" ARIA fallback, XHR upload progress, ReadableStream streaming, view-transition page loads, multi-step trackers, dark-mode color-mix() tokens, WCAG 2.2 semantics, and video buffered-range overlays. Every demo is scoped under a unique .pb-NN root, guards motion behind prefers-reduced-motion, and drops into React, Vue, Svelte, or Astro without framework lock-in.

Related12 CSS Steppers20 CSS Loaders22 CSS Skeleton Loaders

Course pickPartner: Zero to Mastery

Affiliate link

Want to build production-ready CSS layouts, animations and UI from scratch?

You just browsed 22 progress bars. Learn the Flexbox, Grid and animation thinking behind them, so the next component starts in your head, not in a copy-paste.

10% off at checkout with code
Check ZTM Course

Every ZTM course in one membership: $25/month, paid yearly at $299 · 30‑day money-back guarantee

The CSS Bootcamp. We may earn a commission, at no extra cost to you.

01. Native HTML Progress Element with ARIA Fallback (opens in a new tab)

Published

Pure CSSNEW
The decision primer: a real <progress value="68" max="100"> on the left, a custom role="progressbar" div on the right, both at the same value so you can compare what each one buys you. Includes the complete ::-webkit-progress-bar / ::-webkit-progress-value / ::-moz-progress-bar styling block that makes the native element look designed in every engine.

02. Indeterminate Progress Bar with aria-busy State (opens in a new tab)

Published

Light JSNEW
The bar for work whose length you cannot know yet: a sliding fill sweeping a track on a 1.8s linear loop while aria-busy="true" sits on the container and a polite live region announces the state once. Hit Complete and it snaps into a determinate 100% bar — the exact transition production loaders get wrong.
Newsletter

Enjoying these demos? Get new ones by email.

A short email when a new collection ships. Unsubscribe any time.

at

Almost there: check your inbox to confirm.Manage

08. Dark Mode Progress Bar with color-mix Tokens (opens in a new tab)

Published

Light JSNEW
One bar, two themes, no duplicated rules: the track is color-mix(in oklab, currentColor 15%, transparent) and the fill is the accent mixed with a touch of white, so both re-derive themselves from whatever colour the surface inherits. Light and dark cards render side by side to prove it.

14. Segmented Battery Style Progress Bar (opens in a new tab)

PublishedUpdated

Pure CSS
A ten-cell battery meter that changes character with its charge: accent above 30%, amber from 10 to 30, red below, with a lightning bolt and a cell-by-cell charging animation. Colour never travels alone — every state also writes its own label.
Newsletter

Enjoying these demos? Get new ones by email.

A short email when a new collection ships. Unsubscribe any time.

at

Almost there: check your inbox to confirm.Manage

15. Animated Stripe Loading Bar (opens in a new tab)

PublishedUpdated

Pure CSS
The striped bar, rebuilt properly: 45° repeating-linear-gradient hatching that slides forever, shown as both a determinate fill with a value label and an indeterminate full-track sweep — so you can see exactly which one your state needs.

16. Gradient Pulse Progress Bar (opens in a new tab)

PublishedUpdated

Pure CSS
The onboarding bar that looks alive: a violet-to-cyan fill with a highlight sheen travelling across it and a glow that breathes on a 3s cycle. Two ambient animations, both compositor-friendly, both switched off under reduced motion.

Build your own

Tweak the exact look in our visual generators — no signup, instant copy-paste.

FAQ

Frequently asked questions

When should I use the native HTML progress element vs a div with role progressbar?
Prefer the native progress element when the bar visualises a value from 0 to max — assistive technology announces the value change automatically, keyboard focus lands correctly, and no ARIA is needed. Use a div with role=progressbar plus aria-valuenow, aria-valuemin, aria-valuemax when you need custom styling that the browser progress bar cannot render (segmented, ring, gauge, buffered range, stacked category). The native element can be styled via ::-webkit-progress-bar and ::-webkit-progress-value on Chromium and Safari, plus ::-moz-progress-bar on Firefox, but multi-layer visuals like the video buffered range are painful with the native element and cleaner with a custom role=progressbar div. For indeterminate state, either drop the value attribute from progress or set aria-busy=true on the container. Real product surfaces: GitHub file uploads use custom bars for the per-file layout with an overall aggregate, Notion database progress uses native progress inside table cells, Linear roadmap milestones use custom conic rings, Vercel deploys use a top-of-viewport thin line similar to NProgress driven by view transitions on route change.
How do I wire a real file upload progress bar with fetch or XHR?
The native fetch API has no upload-progress event yet, so upload progress bars still use XMLHttpRequest in production. The pattern: create an XHR, attach an xhr.upload.onprogress handler that reads event.loaded / event.total, and paint the ratio to --value on your bar container. Update aria-valuenow and aria-valuetext (aria-valuetext="1.2 megabytes of 2.4 megabytes uploaded") so screen readers announce human-friendly progress instead of raw percentages. For download progress from fetch, use response.body.getReader() and read chunks with Content-Length from response headers as the denominator. Cloudinary, Uploadthing, S3 presigned uploads, and Supabase storage all expose progress callbacks that resolve to the same numbers — plug them into the same CSS custom property. AbortController lets users cancel: xhr.abort() or controller.abort() respectively, then reset the bar and flip aria-busy off. For chunked uploads (files >100 MB), track per-chunk completion and aggregate — the download-queue pattern in demo 07 shows the layout.
How do I show streaming response progress from ReadableStream (SSE, OpenAI, Anthropic, Vercel AI SDK)?
Server-Sent Events and streaming completions from OpenAI / Anthropic / Vercel AI SDK / LangChain arrive as chunks with no upfront total, so the progress bar becomes indeterminate — an animated stripe or sliding fill with aria-busy=true and no aria-valuenow. When the stream ends, flip aria-busy=false and snap the bar to full. If your backend sends a Content-Length header on the streaming response you can compute a determinate value from bytesReceived / total, but most LLM streams omit it because the total isn't known until the last token. A common alternative is a token counter — read event.data for each SSE message, parse the token payload, and increment a visible counter next to the bar (Received: 847 tokens). Vercel AI SDK's useChat hook exposes isLoading and data that map cleanly to aria-busy and the counter respectively. Include an aria-live=polite region announcing key milestones (Streaming started, Streaming complete) so screen-reader users know when the response is finished.
How do I meet WCAG 2.2 AA for progress bars?
Five requirements from the AA level: (1) 1.4.3 contrast — the filled portion of the bar needs 3:1 contrast against the unfilled track, and any text label needs 4.5:1 against its background. (2) 1.4.1 use of color — never rely on color alone to communicate state; pair red / amber / green password-strength colors with icon plus text label (Strong — 4 of 4 criteria met). (3) 2.4.13 focus appearance — a 2px focus-visible outline with 3:1 contrast against adjacent colors on every interactive control (cancel, pause, play, step-next). (4) 4.1.2 name role value — every custom bar needs role=progressbar (or use native progress), aria-valuenow / aria-valuemin / aria-valuemax for the numeric value, and aria-label or aria-labelledby naming what is being measured (aria-label="File upload progress", not just aria-label="Progress"). (5) 2.3.3 animation from interactions — respect prefers-reduced-motion by disabling continuous animation loops (indeterminate stripe, pulse, sheen) and snapping bars to their end state instantly. Demo 09 is the reference implementation showing all five wired correctly with an inline annotation panel labeling each attribute.
How do I build a dark-mode adaptive progress bar with color-mix() tokens?
The traditional pattern needs two rule sets — one for the light theme, one for the dark theme — and they drift out of sync. With color-mix(in oklab, currentColor 15%, transparent) for the unfilled track and color-mix(in oklab, var(--accent) 90%, white 10%) for the fill, the bar reads correctly on both grounds without duplicate rules. On a light card currentColor is near-black so a 15% mix creates a soft grey track; on a dark card currentColor is near-white so the same 15% mix lifts a subtle white track from the dark surface. Wrap the token block in @supports (color:color-mix(in oklab, white 10%, transparent)) with fallback sRGB values declared first for engines that don't support the color-mix function. Pair with oklch() accent colors for perceptually uniform saturation across the light / dark theme swap — an accent authored as oklch(76% 0.16 295) stays vivid on both grounds where an sRGB #a78bfa washes out on a light background. Demo 08 is the technique primer showing both surfaces side-by-side with the same rules.
How do I keep progress bar animations at 60fps on mobile Safari?
Animate transform: scaleX() from 0 to 1 instead of width from 0% to 100% whenever possible — transform is a compositor-only property that stays on the GPU thread, while width triggers layout on every frame and jerks on mobile Safari with anything else running. Set transform-origin: left center and will-change: transform on the fill layer to give the browser a hint to promote it. Compositor pins also apply to opacity (for fading in the buffered range overlay) and filter (for the sheen shimmer on demo 16). Avoid animating background-position, box-shadow, and border-radius in continuous loops — they force layout or paint on every frame. For the ring / conic patterns (demos 13, 19) animate stroke-dashoffset (compositor-safe on SVG) or wrap the conic angle in @property --value with a syntax descriptor of the number type so it interpolates smoothly instead of jumping. Always pair motion with @media (prefers-reduced-motion: reduce) disabling transitions and snapping bars to end state.
How is this different from the other loading collections on codefronts?
Five loading / progress collections cover different search intent lanes. This collection — css-progress-bars — covers determinate and indeterminate progress UI as a value indicator (upload progress, form completion, password strength, download queue, video buffering, storage / quota meters). The bar communicates a specific value or a specific busy state that will resolve. css-scroll-progress-bar covers scroll-driven reading progress at the top of an article via animation-timeline: scroll() — the value is derived from scroll position, not a task. css-loader-designs covers generic loader shapes (spinners, dots, cubes) — the busy state without a value. css-loading-animations covers narrative branded loaders (chef, delivery truck, coffee brewing) — themed to the product domain. css-spinner-designs covers spinner-only patterns (arc, orbit, gradient conic sweep). css-skeleton-loaders covers content-placeholder skeleton UI showing the shape of upcoming content, not a value or busy state. Each collection ranks for its own set of Google queries.
How portable is the code across Tailwind, React, Vue, Svelte, and Astro?
Every demo is framework-agnostic vanilla CSS scoped under a single unique .pb-NN root so it drops into any framework without leaking. Tailwind v3 and v4 users can either drop the demo CSS into an @layer components block or apply the tokens in their own utility classes — the --value CSS custom property is Tailwind-native (style={{ '--value': 68 }} in React). React / Next.js / Remix consume the bar as JSX with style={{ '--value': progress }} inline styles; the demo's JS (upload progress, streaming, password strength, step tracker) can be lifted into a React hook — useUploadProgress, usePasswordStrength — with useState for the value and useEffect for the XHR / fetch setup. Vue 3 users wrap the demo in a template block with a style binding of the form :style set to an object mapping the --value custom property to the reactive progress ref. Svelte binds --value via style:--value={progress}. Astro islands (client:visible) work with the vanilla JS as-is. SolidJS, Qwik, and Angular signals all map to the same CSS custom property pattern.

Related collections

15 CSS 3D Tilt Hover Cards preview

15 CSS 3D Tilt Hover Cards

15 mouse-aware 3D tilt hover cards — e-commerce product spotlight, glassmorphism parallax team card, interactive pricing tier, holographic NFT collectible, pop-out mascot, dark tech grid with border glow, media player album art, blog article preview, cyberpunk neon glow, dashboard KPI widget, minimalist real estate, flip-to-back tilt, mobile app showcase, course learning card, and a Pure CSS touch-friendly tilt. Vanilla JS writes --rx/--ry/--mx/--my; all rendering stays in CSS on the GPU.

24 CSS Animated Cards preview

24 CSS Animated Cards

24 hand-coded CSS animated card patterns organised by what triggers the motion, not by card style. Scroll and entrance triggers: staggered grid reveals, @starting-style mount-in, scroll-driven scale and fade on animation-timeline: view(), View Transitions API card-to-detail morphs, blur-to-sharp lazy image loading, and FLIP re-layout when a filter changes. State-change triggers: add-to-cart success morph, like and save particle burst, expand and collapse to intrinsic height with calc-size(), swipe-to-dismiss on Pointer Events, and skeleton-to-loaded crossfade. Data-driven triggers: count-up KPI metrics, SVG sparkline draw-on-enter, and progress ring fill on load. Ambient idle loops: breathing glow, floating drift, animated gradient mesh backgrounds, conic rotating borders, auto-cycling testimonial decks, and live status pulses. Plus four hover patterns where the effect is the topic itself: cursor spotlight, holographic foil glare, corner ribbon slide, and horizontal accordion expand. Every card is scoped under a .ac-NN prefix for no-collision pasting, guards prefers-reduced-motion, animates compositor-only properties, and ports unchanged to React, Vue, Svelte, Astro, Next.js and Tailwind.

22 CSS Avatars preview

22 CSS Avatars

22 hand-coded CSS avatars for chat apps, team dashboards, comment threads, account menus, and social profiles. Covers circular and squircle shapes, gradient and conic story rings, hexagon clip-path crops, online status dots, notification count badges, verified checkmarks, stacked facepiles with plus-N overflow, initials fallbacks for users without photos, broken-image recovery, avatar pickers, and a size scale system where the ring, badge, and status dot all scale from a single custom property.

Search CodeFronts

Loading…