Top Loading Bar
A slim bar creeps across the top while a page loads, slowing as it goes, then races to the end and fades once it is done.
Create a top-of-page loading bar in React. On start, show a 3px accent bar at transform: scaleX(0.08) with transform-origin left. Every 300ms, move it a fraction of the remaining distance (progress += (0.9 − progress) × 0.15), so it creeps forward and slows down without ever reaching the end on its own. Give the transform a 300ms ease-out transition. When loading finishes (simulate with a 1.8s timeout), set progress to 1 with a faster 200ms transition, then fade the bar out over 300ms and reset it. Add a soft glow on the leading edge with a box-shadow on a pseudo-element. Clear all timers on unmount. Under prefers-reduced-motion, skip the creeping and show a static bar until done.
Source
'use client'
import { useEffect, useRef, useState } from 'react'
import s from './demo.module.css'
type Phase = 'idle' | 'loading' | 'done'
export default function TopLoadingBar({ labels }: { labels: Record<string, string> }) {
const [phase, setPhase] = useState<Phase>('idle')
const [p, setP] = useState(0)
const timers = useRef<number[]>([])
const clear = () => {
timers.current.forEach((t) => (clearInterval(t), clearTimeout(t)))
timers.current = []
}
useEffect(() => clear, [])
const start = () => {
clear()
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches
setPhase('loading')
setP(reduced ? 0.6 : 0.08)
if (!reduced) timers.current.push(window.setInterval(() => setP((v) => v + (0.9 - v) * 0.15), 300))
timers.current.push(
window.setTimeout(() => {
clear()
setPhase('done')
setP(1)
timers.current.push(window.setTimeout(() => (setPhase('idle'), setP(0)), 700))
}, 1800),
)
}
return (
<div className={s.window}>
<span className={s.bar} data-phase={phase} style={{ transform: `scaleX(${p})` }} aria-hidden="true" />
<div className={s.chrome}>
<span />
<span />
<span />
</div>
<div className={s.body}>
<button type="button" className={s.btn} onClick={start} disabled={phase === 'loading'}>
{labels.load}
</button>
<span className={s.status} role="status">
{phase === 'done' ? labels.loaded : ''}
</span>
</div>
</div>
)
}
.window {
position: relative;
width: min(300px, 100%);
overflow: hidden;
border: 1px solid var(--stage-rule);
border-radius: 12px;
background: var(--stage-surface);
}
.bar {
position: absolute;
left: 0;
top: 0;
z-index: 1;
width: 100%;
height: 3px;
background: var(--stage-accent);
transform-origin: left;
opacity: 0;
transition: transform 300ms var(--ease-out), opacity 300ms;
}
.bar::after {
content: '';
position: absolute;
right: 0;
top: 0;
width: 60px;
height: 100%;
box-shadow: 0 0 10px 1px var(--stage-accent);
}
.bar[data-phase='loading'] { opacity: 1; }
.bar[data-phase='done'] { opacity: 0; transition: transform 200ms var(--ease-out), opacity 300ms 250ms; }
.chrome {
display: flex;
gap: 6px;
padding: 12px;
border-bottom: 1px solid var(--stage-rule);
}
.chrome span { width: 8px; height: 8px; border-radius: 50%; background: var(--stage-rule); }
.body {
display: grid;
justify-items: center;
gap: 8px;
padding: 28px 16px;
}
.btn {
height: 36px;
padding: 0 16px;
border: 0;
border-radius: var(--radius-sm);
background: var(--stage-accent);
color: var(--stage-accent-ink);
font: 500 var(--text-sm) / 1 var(--font-body);
cursor: pointer;
}
.btn:disabled { opacity: 0.6; cursor: progress; }
.btn:focus-visible { outline: 2px solid var(--color-focus); outline-offset: 2px; }
.status {
min-height: 1em;
font: 500 var(--text-xs) / 1 var(--font-mono);
color: var(--stage-muted);
}
@media (prefers-reduced-motion: reduce) {
.bar, .bar[data-phase='done'] { transition: opacity 150ms; }
}