Skip to content

Page Loading Bar

An nprogress-style top-of-page bar that eases toward completion, never stalling.

Freeminimalbrutalist
Category
Assets
Added
2026-08-24
Deps
1
Updated
2026-09-01
Preview

Props

Requires framer-motion
"use client";

import { useEffect, useRef, useState } from "react";
import { motion } from "framer-motion";

export function usePageLoadingBar() {
  const [progress, setProgress] = useState(0);
  const [loading, setLoading] = useState(false);
  const rafRef = useRef<number | null>(null);

  function start() {
    setLoading(true);
    setProgress(3);
  }

  useEffect(() => {
    if (!loading) return;
    function tick() {
      setProgress((p) => (p >= 90 ? p : p + (90 - p) * 0.03));
      rafRef.current = requestAnimationFrame(tick);
    }
    rafRef.current = requestAnimationFrame(tick);
    return () => {
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
    };
  }, [loading]);

  function finish() {
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
    setProgress(100);
    setTimeout(() => {
      setLoading(false);
      setProgress(0);
    }, 300);
  }

  return { progress, loading, start, finish };
}

export function PageLoadingBar({ progress, loading, color = "#ff2d2d" }: { progress: number; loading: boolean; color?: string }) {
  return (
    <div
      role="progressbar"
      aria-valuenow={Math.round(progress)}
      aria-valuemin={0}
      aria-valuemax={100}
      aria-label="Page loading progress"
      className="fixed inset-x-0 top-0 z-50 h-1 bg-transparent"
    >
      <motion.div
        animate={{ width: `${progress}%`, opacity: loading || progress > 0 ? 1 : 0 }}
        transition={{ duration: 0.2, ease: "easeOut" }}
        className="h-full"
        style={{ background: color }}
      />
    </div>
  );
}
Notes

About this asset

Route transitions in a client-rendered app have no real progress to report. You do not know how long the next page will take to render, so a literal progress bar is lying by definition. This is the nprogress trick done properly: calling start sets progress to a small initial value, then a requestAnimationFrame loop nudges it toward 90 percent by closing a fraction of the remaining gap every frame, which decelerates asymptotically and never actually reaches 90 on its own. That is deliberate. A bar that visibly finishes before the page is ready looks broken, and a linear bar that hits 100 and stalls looks worse. Calling finish cancels the loop, snaps to 100, holds briefly so completion reads as intentional, then resets both values. You have to wire start and finish to your own router events yourself; the hook has no built-in awareness of any specific router or fetch completion, so skipping that wiring leaves a bar that never moves.

Curator’s note

The bar eases toward 90% (each frame closes a fraction of the remaining gap) rather than advancing linearly toward 100% — a linear bar for genuinely unknown-duration work either finishes visually before the real work is done, or stalls flat at 100% while still waiting.

Related

Pairs well with

Matched on shared tags and category — the entries most likely to be used alongside this one.

NewPro

A sticky section nav whose items fill like progress bars as you read — and still reaches the short last section that fixed-line navs never activate.

navigationscrollsticky

Loading-state placeholder blocks with a light sweep animating across them.

skeletonloadingshimmer
Pro

A recursive, collapsible file tree with real roving-tabindex arrow-key navigation.

treefile explorersidebar