Progress Bar
A 200 × 6 px bar; its 80 px pill (40% of the track), indigo-to-fuchsia gradient, slides from translateX(−100%) to translateX(350%) in 1.5 s ease-in-out, infinite. Zero JavaScript — the whole motion lives in one @keyframes and a ::after selector.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Loaders has 29 effects, including 4 free. Explore the category →
3 usage examples



How it works
The code exposes a single element: .loader-bar, 200 × 6 px, background rgba(99, 102, 241, .2) (indigo at 20% opacity), border-radius: 3px, overflow: hidden. It has no children in the sold HTML; all motion comes from the ::after pseudo-element. That pseudo-element is 40% of the track's width — 80 × 6 px — and displays the gradient linear-gradient(90deg, #6366f1, #d946ef) (indigo → fuchsia).
barProgress moves the pill from translateX(−100%) to translateX(350%). These values are relative to its own width (80 px): it enters at −80 px (off the track on the left) and exits at 280 px from the track's left edge (off the track on the right; the track is only 200 px). Total travel: 360 px. Duration is 1.5 s, timing function ease-in-out: the pill accelerates as it enters from the left, reaches peak speed at the center and slows before leaving to the right. Measured in Chromium: −67.8 px at 200 ms (still off-track on the left), −0.7 px at 500 ms (pill entering the visible zone). The loop is infinite, delay 0 s: on each restart the pill is instantly back off-track on the left — the jump is hidden by overflow: hidden.
There is no JavaScript in the sold code. Starting or stopping the animation requires only toggling animation-play-state: paused / running on .loader-bar::after, done by adding or removing a class on the track. The 200px width is fixed and not responsive: on a narrower container the bar overflows; on a wider one it stays centered but does not stretch. Replace with width: 100% to follow the parent.
Accessibility
- prefers-reduced-motion not handled: measured in Playwright with
reducedMotion: 'reduce', the pill is at −68.3 px at 200 ms and −2.1 px at 500 ms — the animation runs unchanged. Add@media (prefers-reduced-motion: reduce) { .loader-bar::after { animation: none; } }. - No accessibility markup: the bar is an empty
divwith norole,aria-live,aria-busyor alternative text. For screen readers the loading state is invisible. Add at minimum a sibling element withrole="status" aria-live="polite"and visually hidden text: "Loading…". - The track is purely decorative: add
aria-hidden="true"on.loader-barand delegate the state to a dedicated element. If the bar is embedded in a button (button in progress),aria-busy="true"goes on the button, not on the bar. - Contrast: the pill (#6366f1 → #d946ef) on background #0a0a0f measures 3.4:1 for the indigo end and 3.8:1 for the fuchsia end — above the graphical elements threshold (3:1). On a white background those values drop to 2.8:1 and 3.4:1; lower the lightness or raise saturation to hold the threshold on a light background.
Browser compatibility
Requires @keyframes, translateX(), linear-gradient(), border-radius, overflow: hidden and the ::after pseudo-element with content: "". All of these have been available since Chrome 26, Firefox 16, Safari 7. No dependencies.
Without CSS animation (very old browsers), the track renders empty: the pill exists (the ::after is generated by CSS) but frozen at its initial position (translateX(−100%)), outside the track and invisible. Without linear-gradient (old IE), the pill renders with a transparent background — invisible. Without CSS, the track is an empty div.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="loader-bar"></div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Track width (CSS — .loader-bar, width) |
200px | Total width of the track. Switch to 100% to follow the parent; the pill's travel adapts automatically because its width is a percentage. |
Thickness (CSS — .loader-bar, height) |
6px | Height shared by the track and the pill (::after height: 100%). 4px for an indicator embedded in a button; 8px for a more visible page header bar. |
Track color (CSS — .loader-bar, background) |
rgba(99, 102, 241, .2) | Background of the track. Use the primary hue at 15–25% opacity to stay in the same color family as the pill. |
Pill width (CSS — .loader-bar::after, width) |
40% (= 80 px on a 200 px track) | Fraction of the track the pill covers. 25% = longer sweep, snappier feel; 60% = wide pill that nearly fills the track as it passes. |
Pill gradient (CSS — .loader-bar::after, background) |
linear-gradient(90deg, #6366f1, #d946ef) | Color of the pill. A solid color (background: #6366f1) or a reversed gradient (#d946ef, #6366f1) for the opposite visual direction. |
Cycle duration (CSS — .loader-bar::after, animation-duration) |
1.5s | Time for one full pass. 0.8s = urgent feel (network request); 3s = slow process (computation, generation). Also replace ease-in-out with linear for a steady speed beyond 2 s. |
Corner radius (CSS — .loader-bar and .loader-bar::after, border-radius) |
3px | Both must share the same value for consistent track and pill ends. 0 = sharp bar; 9999px = oval pill on a rounded track. |
FAQ
width via JavaScript. For that, add a CSS variable --progress on .loader-bar::after and update it from your code.animation-play-state: paused on .loader-bar::after freezes the pill at its current position — motion stops but the track remains visible. (2) display: none or visibility: hidden on .loader-bar removes it visually. For a polished transition, add opacity: 0; transition: opacity .3s on .loader-bar and toggle the class from JavaScript once the response is received.stroke-dashoffset — the arc goes from 157 to 0 then to −157 over 2 s (enter then exit). The square format suits a standalone indicator or one placed next to a label. The Progress Bar (fx-0424) is horizontal, 200 × 6 px, and can stretch to 100% of its parent: it belongs above or below content (table, button, text area) and occupies the full available width without adding height.