Route Progress React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React route progress bar: the thin bar at the top of the app while a page loads, started by in-app links or by hand for any async work.
Click a link inside the LinkProvider and the bar slides in from the start edge after a short delay, creeps toward 90% while the next page loads, then fills to the end, holds for a beat and fades out once the path changes. Navigations faster than the delay never show it, a click on the page you are already on does nothing, and browser back / forward shows nothing either. Call routeProgress.start() and routeProgress.done() from anywhere (saving, exporting) to drive it by hand, or pass loading. It is never focusable and has nothing to tap; on a phone it sits under the status bar through the safe-area inset. Screen readers get a labeled progressbar with its value while it is visible and nothing while idle. Under reduced motion the bar jumps between values without springs or glow.
Route Progress is a Pro React component in the React feedback components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add route-progress, together with Navigation, Motion presets, Tones and UIProvider and the npm package motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add route-progressRoute Progress is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import RouteProgress from @/components/ui/route-progress.
"use client"
import type { ReactNode } from "react"
import Link from "next/link"
import { usePathname, useRouter } from "next/navigation"
import { Button } from "@/components/ui/button"
import { RouteProgress, routeProgress } from "@/components/ui/route-progress"
import { LinkProvider } from "@/lib/navigation"
// once, in the root layout's client providers: every NavLink click starts the bar, the new path finishes it
export function Providers({ children }: { children: ReactNode }) {
const router = useRouter()
return (
<LinkProvider component={Link} currentPath={usePathname()} navigate={router.push}>
<RouteProgress />
{children}
</LinkProvider>
)
}
// by hand, for any async work
export function SaveInvoice({ save }: { save: () => Promise<void> }) {
return (
<Button
onClick={async () => {
routeProgress.start()
try {
await save()
} finally {
routeProgress.done()
}
}}
>
Save invoice
</Button>
)
}Props
autodefaulttruebooleanStarts on in-app navigations (a NavLink click, useNavigate) and finishes when the current path changes. A click on the current page, a hash or a query change does not count.
loadingbooleanControlled mode: true starts, false finishes. It overrides auto. Leave it undefined to use auto and the imperative routeProgress API. Docs-only: the Controlled loading example drives it from a fake request.
currentPathstringThe path whose change finishes a navigation. Defaults to the LinkProvider's currentPath, which is how the playground feeds it.
delaydefault120numberMilliseconds before the bar appears. Anything that finishes sooner never shows it, so fast pages do not flash.
minimumdefault0.08numberWhere the bar starts, as a fraction of the width.
trickledefaulttruebooleanCreeps toward 90% while waiting; each step covers 5 to 15% of the distance left.
trickleSpeeddefault250numberMilliseconds between trickle steps.
timeoutdefault10000numberMilliseconds after which a navigation that never finished completes anyway (a cancelled route, a redirect back to the same page). 0 turns the safety net off.
sizedefault"md""sm" | "md" | "lg"Bar height: 2 / 3 / 4px.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The bar's color from the design tokens. neutral is black in light mode and white in dark mode.
colordefault""stringAny CSS color. Replaces the tone for the bar, the glow and the spinner. Leave empty to use the tone.
glowdefaulttruebooleanA soft glow on the last 96px of the bar, at 60% opacity. Dropped under reduced motion.
spinnerdefaultfalsebooleanA 14px ring spinner in a small floating chip under the top end of the bar while loading.
positiondefault"fixed""fixed" | "absolute"fixed pins it to the top of the viewport; absolute pins it to the nearest positioned ancestor (a card, a panel, a demo frame).
offsetdefault0numberPixels from the top, e.g. 56 to sit under a fixed header.
safeAreadefaulttruebooleanAdds env(safe-area-inset-top) to the offset, so an installed web app draws the bar under the status bar. It is 0 in desktop browsers.
labeldefault"Loading page"stringThe progressbar's aria-label.
storedefault"routeProgress"RouteProgressStoreThe store this bar shows. Defaults to the shared routeProgress; pass createRouteProgress() when two bars must not share state.
classNamesPartial<Record<"track" | "bar" | "glow" | "spinner", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root, merged last. Override the tone vars here, e.g. [--tone:var(--info)].
Parts
routeProgress
constantThe shared imperative store. start() shows the bar after the delay (ignored while running), done() finishes it (silently while still pending), set() jumps to a fraction, inc() moves it forward, reset() hides it at once. Type: { start(); done(); set(fraction: number); inc(amount?: number); reset() }.
useRouteProgress
hookThe store's state and value, for a custom progress UI (a spinner in the tab title, a disabled form). Signature: (store?: RouteProgressStore) => { state: "idle" | "pending" | "loading" | "done"; value: number }.
createRouteProgress
functionMakes an independent store with the same API as routeProgress. Signature: (config?: Partial<RouteProgressConfig>) => RouteProgressStore.
Examples
Tones and a custom color
Every tone from the design tokens plus any CSS color; brand fits a marketing site.
Sizes, glow and spinner
2 / 3 / 4px bars, with and without the leading-edge glow, and the optional spinner.
Delay hides fast navigations
Both links start the bar; the 80ms page arrives before the 120ms delay, so only the slow one shows it.
Controlled loading
loading follows a fake fetch: true starts the bar, false finishes it.
Under a fixed header
offset={56} places the bar under a 56px top bar.
Usage with Next
LinkProvider with next/link and usePathname, plus one RouteProgress.
Dependencies
Files
- components/ui/route-progress.tsx
FAQ
Is the React Route Progress component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Route Progress?
Run npx wingo-ui@latest add route-progress. It also adds the Wingo UI items it builds on (Navigation, Motion presets, Tones and UIProvider) and the npm package motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/route-progress.
How does Route Progress behave on phones?
It is never focusable and has nothing to tap; on a phone it sits under the status bar through the safe-area inset.