SolidJS Route Progress Bar with CSS Trickling

Track SolidJS route changes, Suspense waits, fetches, and cross-document navigation with one configurable top progress bar.

solid-route-progress is a SolidJS and SolidStart route progress library that creates a thin top loading bar to route transitions and tracked async work.

<RouteProgress /> follows useIsRouting(), skips navigations that finish inside the default 200 ms delay, and moves toward completion through one CSS transition.

Features

  • One <RouteProgress /> integration for <A> clicks, navigate(), back/forward, action redirects, and route <Suspense> waits.
  • 200 ms default delay. Quicker navigations draw nothing.
  • CSS trickling toward 95%, with timing, easing, color, height, and stacking order exposed through options and CSS variables.
  • Shared holds for route changes, tracked promises, and manual start() calls.
  • Cross-document tracking for page-started external links, form posts, location changes, and reload() where the Navigation API is present.
  • Tailwind v4-friendly cascade-layer CSS plus state and busy hooks.
  • Server-rendered progress markup with role="progressbar", RTL behavior, and forced-colors styling.

How To Use It

Install the package:

npm i solid-route-progress

Import the stylesheet once:

/* src/app.css */
@import 'tailwindcss'; /* optional */
@import 'solid-route-progress/style.css';

Basic SolidStart Usage

Render <RouteProgress /> inside <Router>:

// src/app.tsx
import { Router } from '@solidjs/router'
import { FileRoutes } from '@solidjs/start/router'
import { Suspense } from 'solid-js'
import { RouteProgress } from 'solid-route-progress/router'
import './app.css'
export default function App() {
  return (
    <Router
      root={(props) => (
        <>
          <RouteProgress />
          <Suspense>{props.children}</Suspense>
        </>
      )}
    >
      <FileRoutes />
    </Router>
  )
}

RouteProgress Props

When a <ProgressProvider> supplies the controller, set trickleTo, delay, stopDelay, and speed on the provider. Values passed to <RouteProgress /> for those four options are ignored in that setup.

PropDefaultPurpose
shallowfalseSkip navigation when only the search string or hash changes.
filterReturn false to skip a route navigation.
crossDocumenttrueTrack page-started navigation that leaves the document. Accepts false or { timeout, filter }.
trickleTo0.95Value the bar drifts toward during loading.
delay200Milliseconds before the bar becomes visible.
stopDelay0Milliseconds before completion after the final hold releases.
speed200Milliseconds for explicit progress moves, completion, and fades.
controllernearest controller or a new oneDrive the bar from an existing controller.
labelLoadingAccessible name for the progress bar.
getValueLabelGenerate aria-valuetext after explicit set() progress.
busyAttributetrueSet data-sp-busy on <html> while the bar is visible.
ariaBusyfalseSet aria-busy="true" on <html> while the bar is visible.
children<Bar />Replace the default bar template.
class, style, other div attributesForward attributes to the root progress element.

Track Fetches and Other Async Work

Place <ProgressProvider> inside the router root when descendant components need useProgress():

import { Router } from '@solidjs/router'
import { FileRoutes } from '@solidjs/start/router'
import { Suspense, type ParentProps } from 'solid-js'
import { ProgressProvider } from 'solid-route-progress'
import { RouteProgress } from 'solid-route-progress/router'
function Root(props: ParentProps) {
  return (
    <ProgressProvider>
      <RouteProgress />
      <Suspense>{props.children}</Suspense>
    </ProgressProvider>
  )
}
export default function App() {
  return (
    <Router root={Root}>
      <FileRoutes />
    </Router>
  )
}

Track a Fetch

import { useProgress } from 'solid-route-progress'
function SaveButton() {
  const progress = useProgress()
  const saveItems = async () => {
    await progress.track(
      fetch('/api/items', {
        method: 'POST',
        body: JSON.stringify({ status: 'saved' }),
      }),
    )
  }
  return <button onClick={saveItems}>Save items</button>
}

Progress Controller API

Create a standalone controller when a provider or progress component does not own one:

import { createProgress } from 'solid-route-progress'
const progress = createProgress({ delay: 300 })
APIPurpose
start()Take one hold and return a release function.
done(outcome?)Drop every hold and complete after stopDelay.
set(value)Move to a value from 0 to 1; 1 completes the bar.
track(promise, { timeout? })Hold until the promise settles and return that promise unchanged.
value()Read the controller value from 0 to 1.
state()Read idle, trickle, active, or done.
active()Read the visibility state.
error()Read the failed state during the done phase.
optionsRead the controller options.

Styling and Tailwind CSS

:root {
  --sp-color: oklch(0.62 0.19 264);
  --sp-height: 2px;
}
CSS PropertyDefaultPurpose
--sp-coloroklch(0.65 0.14 241)Bar color.
--sp-height3pxBar thickness.
--sp-z-index9999Stacking order.
--sp-start0.08Revealed starting value.
--sp-trickle-duration10sDuration of the loading drift.
--sp-trickle-easinglinear(...)Easing curve for the drift.
--sp-speedspeed optionRead-only CSS value for short transitions. Set it through speed.

State Hooks

HookLocationActive State
`data-state=”idletrickleactive
data-errorProgress barFailed load during the done state.
data-sp-busy<html>Any progress bar is visible.

Tailwind Utilities

Tailwind utilities can override the package stylesheet:

<RouteProgress class="h-1 data-[state=done]:opacity-50" />

Ignore or Filter Route Navigation

<RouteProgress
  shallow
  filter={(to) => !to.startsWith('/admin')}
/>
<A href="/settings" data-sp-ignore>
  Settings
</A>

Use It Outside @solidjs/router

Render <NavigationProgress /> for apps that use the browser Navigation API in place of @solidjs/router:

import { NavigationProgress } from 'solid-route-progress/navigation'
import 'solid-route-progress/style.css'
export function Layout(props) {
  return (
    <>
      <NavigationProgress />
      {props.children}
    </>
  )
}

NavigationProgress Options

PropDefaultPurpose
filterReturn false to skip a navigate event.
timeout10000Release a cross-document hold after this many milliseconds if the page never unloads. Set 0 to disable the timeout.

Runtime API Map

ExportImportRole
<RouteProgress />solid-route-progress/routerProgress bar bound to @solidjs/router.
createRouteProgress()solid-route-progress/routerBind an existing controller to Solid Router.
<NavigationProgress />solid-route-progress/navigationRouter-agnostic bar driven by the Navigation API.
createNavigationProgress()solid-route-progress/navigationBind an existing controller to the Navigation API.
<Progress />solid-route-progressRender the base progress bar for a controller.
<ProgressProvider>solid-route-progressSupply a controller to descendant components.
<Bar />solid-route-progressRender the default sliding bar template.
useProgress()solid-route-progressRead the nearest progress controller.
createProgress()solid-route-progressCreate a standalone controller.
createCrossDocumentProgress()solid-route-progressBind an existing controller to cross-document navigation only.
ProgressContextsolid-route-progressLow-level Solid context for custom integrations.
IGNORE_ATTRIBUTEsolid-route-progressExport the data-sp-ignore attribute name.

NProgress and BProgress Migration

NProgress / BProgresssolid-route-progress
minimum--sp-start
trickleSpeed, easing, speed--sp-trickle-duration, --sp-trickle-easing, and speed
color, height, template--sp-color, --sp-height, and children
parentRender <Progress /> inside the container and position it with class.
shallowRouting, targetPreprocessorshallow, filter(to, from)
promise()track(promise)
inc(), dec(), pause(), resume()No direct equivalent. Use set(k / n) for known progress.

Alternatives and Related Resources

FAQs

Q: Why does the bar not appear on fast route changes?
A: The default delay is 200 ms. A navigation that finishes before the delay ends never draws the bar. Set delay={0} while testing if you need to see every route change.

Q: Why does a SolidStart action that only revalidates data show no progress bar?
A: Actions that revalidate without a redirect do not enter the router’s routing state. Read useSubmission(action).pending and take a hold with progress.start() for that action.

Q: What happens when the Navigation API is unavailable?
A: Normal @solidjs/router route tracking continues through useIsRouting(). Cross-document tracking and <NavigationProgress /> stay inactive.

kecan0406

kecan0406

Leave a Reply

Your email address will not be published. Required fields are marked *