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.

  • JS
  • Layout
  • Click
  • Added

AI prompt

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

Demo.tsx
'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>
  )
}
demo.module.css
.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; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.