Navigation Bar

The thin bar at the top of the page during a route change. It does not measure anything: it approaches an end it never reaches, then completes.

API

Props

proptypedefault
as?ElementType"div"the element or component to render
loading?booleanfalsewhether something is in flight
fill?stringthe primary gradientanything a background accepts: a colour, a gradient
className?stringmerged over the defaults, so it wins

Examples

Usage

tsx
<NavigationBar loading={isLoading} />
previewwatch the top of the page

It is a controlled component: it takes a boolean and knows nothing about routers, requests or promises. Whatever can be described as "in flight" can drive it — a route change, a form submission, a slow mutation.

Details

How the progress is invented

There is nothing to measure during a navigation, so the bar simulates. It starts at 8% and, every 200ms, advances by 12% of the distance left to 90%. The result decelerates on its own and never arrives, which is exactly what it should communicate: work is happening, and no, it cannot tell you how much is left.

tsx
progress = progress + (90 - progress) * 0.12;   // every 200ms
progress = 100;                                 // on completion

When loading goes false the bar jumps to 100% and only then fades out, so the end of the wait is a completion rather than a disappearance.

Driving it from a router

Subscribe to the events the router already emits. Do not derive it from a pending state: a navigation with no loader resolves in the same tick, so a state flag is never observed, while the events fire regardless.

tsx
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);

useEffect(() => {
  const start = () => setIsLoading(true);
  const stop = () => setIsLoading(false);

  const unsubscribeNavigate = router.subscribe("onBeforeNavigate", start);
  const unsubscribeResolved = router.subscribe("onResolved", stop);

  return () => {
    unsubscribeNavigate();
    unsubscribeResolved();
  };
}, [router]);

return <NavigationBar loading={isLoading} />;

A minimum duration

On an instant navigation the bar would appear and vanish within a frame, which reads as a flicker. Hold the flag for a minimum — this site uses 750ms — by subtracting the elapsed time when stopping, so a slow navigation is not delayed and a fast one still gets a visible arc.

tsx
const stop = () => {
  const elapsed = Date.now() - startedAtRef.current;

  timeoutRef.current = window.setTimeout(() => setIsLoading(false), Math.max(MINIMUM_LOADING_MS - elapsed, 0));
};

What it announces

The bar is a progressbar with no value, and it is aria-hidden while idle. Announcing a percentage would mean announcing a number the component invented, which is worse than saying nothing: the simulation is honest as an animation and dishonest as data.

The look

The bar is fixed to the top of the viewport at the same thickness as ProgressBar, filled with the primary gradient, and carries a rotated glow at its leading edge — the detail that reads as speed rather than as a growing rectangle.

fill replaces that gradient with anything a background accepts, in the same way ProgressBar takes its own.

tsx
<NavigationBar loading={isLoading} fill="var(--margo-color-on-main)" />