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
| prop | type | default | |
|---|---|---|---|
as? | ElementType | "div" | the element or component to render |
loading? | boolean | false | whether something is in flight |
fill? | string | the primary gradient | anything a background accepts: a colour, a gradient |
className? | string | — | merged over the defaults, so it wins |
Examples
Usage
<NavigationBar loading={isLoading} />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.
progress = progress + (90 - progress) * 0.12; // every 200ms
progress = 100; // on completionWhen 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.
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.
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.
<NavigationBar loading={isLoading} fill="var(--margo-color-on-main)" />