Examples
Each demo is a real component, and the code under it is that component's file, imported with ?raw. Every bar sits in its own panel (the container recipe) and passes busyAttribute={false}.
track()
| Button | Result |
|---|---|
| Fetch (1.5 s) | holds, then completes |
| Fail it (1.2 s) | releases as 'error', sets data-error, and turns red |
| Too fast (120 ms) | shorter than delay, so it is never painted |
state: idle
import { createProgress, Progress } from 'solid-route-progress' import { Btn, delay, fail, Panel, Readout, Row } from './ui' export function TrackPromise() { // `speed` is stretched from its 200 ms default so the done phase (and the red of a failed // load) lasts long enough to see. const progress = createProgress({ speed: 450 }) return ( <Panel> <Progress controller={progress} class="ex-error absolute" label="Tracked request" busyAttribute={false} /> <Row> <Btn onClick={() => progress.track(delay(1500))}>Fetch (1.5 s)</Btn> <Btn onClick={() => progress.track(fail(1200)).catch(() => {})}>Fail it (1.2 s)</Btn> <Btn onClick={() => progress.track(delay(120))}>Too fast to show (120 ms)</Btn> </Row> <Readout> state: {progress.state()} {progress.error() ? ' · error' : ''} </Readout> </Panel> ) }
start() and release()
| Button | Result |
|---|---|
| Take a hold | one more hold |
| Release one | bar keeps going until the last hold goes |
| Release as cancel | fades out without reaching 100% |
| Release as error | completes with data-error |
0 hold(s) · state: idle
import { createSignal } from 'solid-js' import { createProgress, Progress, type Release } from 'solid-route-progress' import { Btn, Panel, Readout, Row } from './ui' export function ManualHolds() { // `speed` is stretched from its 200 ms default so the done phase (and the red of a failed // load) lasts long enough to see. const progress = createProgress({ speed: 450 }) const [holds, setHolds] = createSignal<Release[]>([]) const take = () => setHolds([...holds(), progress.start()]) const letGo = (outcome?: 'error' | 'cancel') => { const [first, ...rest] = holds() if (!first) return first(outcome) setHolds(rest) } return ( <Panel> <Progress controller={progress} class="ex-error absolute" label="Manual holds" busyAttribute={false} /> <Row> <Btn onClick={take}>Take a hold</Btn> <Btn onClick={() => letGo()}>Release one</Btn> <Btn onClick={() => letGo('cancel')}>Release as cancel</Btn> <Btn onClick={() => letGo('error')}>Release as error</Btn> </Row> <Readout> {holds().length} hold(s) · state: {progress.state()} {progress.error() ? ' · error' : ''} </Readout> </Panel> ) }
set()
aria-valuenow and getValueLabel's aria-valuetext appear once the value is known.
0% · state: idle
import { createSignal, onCleanup } from 'solid-js' import { createProgress, Progress } from 'solid-route-progress' import { Btn, Panel, Readout, Row } from './ui' export function DeterminateValue() { const progress = createProgress() const [sent, setSent] = createSignal(0) let timer: ReturnType<typeof setInterval> | undefined onCleanup(() => clearInterval(timer)) const upload = () => { clearInterval(timer) setSent(0) timer = setInterval(() => { setSent(Math.min(1, sent() + 0.125)) // `set()` reveals the bar, moves it, then hands back to the trickle; 1 completes it. progress.set(sent()) if (sent() >= 1) clearInterval(timer) }, 320) } return ( <Panel> <Progress controller={progress} class="absolute" label="Upload" getValueLabel={(percent) => `${percent} percent uploaded`} busyAttribute={false} /> <Row> <Btn onClick={upload}>Upload a file</Btn> <Btn onClick={() => progress.done('cancel')}>Abort</Btn> </Row> <Readout> {Math.round(progress.value() * 100)}% · state: {progress.state()} </Readout> </Panel> ) }
Custom template
<Progress> renders <Bar /> unless you give it children; children read the controller with useProgress().
import { Bar, createProgress, Progress, useProgress } from 'solid-route-progress' import { Btn, delay, Panel, Row } from './ui' /** Children of `<Progress>` read its controller with `useProgress()`. */ const Percent = () => { const progress = useProgress() return ( <output class="absolute top-3 right-3 font-mono text-[11px] text-muted-foreground"> {Math.round(progress.value() * 100)}% </output> ) } export function CustomTemplate() { const progress = createProgress() return ( <Panel> <Progress controller={progress} class="absolute" label="Custom template" busyAttribute={false} > <Bar class="rounded-r-full" /> <Percent /> </Progress> <Row> <Btn onClick={() => progress.track(delay(2500))}>Load something slow</Btn> </Row> </Panel> ) }
Skipping navigations
Both bars are wired to this page's router. The links only change the search string.
| Link | default + filter | shallow |
|---|---|---|
?skip=go | flashes | ignores |
?skip=blocked | filter returns false | ignores |
?skip=quiet | data-sp-ignore | ignores |
default + filter
shallow
left: idle · right: idle
import { A } from '@solidjs/router' import type { JSX } from 'solid-js' import { createProgress } from 'solid-route-progress' import { RouteProgress } from 'solid-route-progress/router' import { Panel, Readout, Row } from './ui' // These links only change the search string, so every navigation here is instant. `delay: 0` // and a long `stopDelay` make the flash visible; what matters is which bar flashes at all. const options = { delay: 0, stopDelay: 700 } const Slot = (props: { name: string; children: JSX.Element }) => ( <div class="relative overflow-hidden rounded-lg border border-border py-3 text-center"> {props.children} <span class="font-mono text-[11px] text-muted-foreground">{props.name}</span> </div> ) export function SkippingNavigations() { const plain = createProgress(options) const shallow = createProgress(options) return ( <Panel> <div class="mb-4 grid gap-3 sm:grid-cols-2"> <Slot name="default + filter"> <RouteProgress controller={plain} class="absolute" label="Default bar" busyAttribute={false} crossDocument={false} filter={(to) => !to.includes('skip=blocked')} /> </Slot> <Slot name="shallow"> <RouteProgress controller={shallow} class="absolute" label="Shallow bar" busyAttribute={false} crossDocument={false} shallow /> </Slot> </div> <Row> <A href="?skip=go" noScroll class="rounded-lg border border-border px-3 py-2 text-[13px]"> ?skip=go </A> <A href="?skip=blocked" noScroll class="rounded-lg border border-border px-3 py-2 text-[13px]" > ?skip=blocked: filtered out </A> <A href="?skip=quiet" noScroll data-sp-ignore class="rounded-lg border border-border px-3 py-2 text-[13px]" > ?skip=quiet: data-sp-ignore </A> </Row> <Readout> left: {plain.state()} · right: {shallow.state()} </Readout> </Panel> ) }
Leaving the document
createCrossDocumentProgress() on its own, which is what <RouteProgress> does for you.
Leave for GitHub: the bar holds until the browser hands overSame link with data-sp-ignore: nothing shows
state: idle · use the back button to come back
import { createCrossDocumentProgress, createProgress, Progress } from 'solid-route-progress' import { REPO } from '~/links' import { Panel, Readout, Row } from './ui' /** * `<RouteProgress>` does this for you. Here it runs on its own, so the bar reacts only to * navigations that leave the document. It relies on the Navigation API, and where that is missing, * nothing happens. */ export function CrossDocument() { const progress = createProgress() createCrossDocumentProgress(progress) return ( <Panel> <Progress controller={progress} class="absolute" label="Leaving the page" busyAttribute={false} /> <Row> <a href={REPO} class="rounded-lg border border-border px-3 py-2 text-[13px]"> Leave for GitHub: the bar holds until the browser hands over </a> <a href={REPO} data-sp-ignore class="rounded-lg border border-border px-3 py-2 text-[13px]"> Same link with data-sp-ignore: nothing shows </a> </Row> <Readout>state: {progress.state()} · use the back button to come back</Readout> </Panel> ) }
Styling recipes
The recipes, pasted into app.css as .ex-glow and .ex-spinner.
import { createSignal, Show } from 'solid-js' import { Bar, createProgress, Progress } from 'solid-route-progress' import { Btn, delay, fail, Panel, Row } from './ui' const Toggle = (props: { label: string; on: boolean; onChange: (on: boolean) => void }) => ( <label class="flex items-center gap-1.5 text-[13px]"> <input type="checkbox" checked={props.on} onChange={(event) => props.onChange(event.currentTarget.checked)} /> {props.label} </label> ) /** The recipes from the Styling docs, pasted into `app.css` as `.ex-glow` / `.ex-spinner`. */ export function StyleRecipes() { // `speed` is stretched from its 200 ms default so the done phase (and the red of a failed // load) lasts long enough to see. const progress = createProgress({ speed: 450 }) const [glow, setGlow] = createSignal(true) const [spinner, setSpinner] = createSignal(true) return ( <Panel> <Progress controller={progress} class={`ex-error absolute${glow() ? ' ex-glow' : ''}`} label="Styled bar" busyAttribute={false} > <Bar /> <Show when={spinner()}> <div class="ex-spinner" aria-hidden="true" /> </Show> </Progress> <Row> <Btn onClick={() => progress.track(delay(2500))}>Load</Btn> <Btn onClick={() => progress.track(fail(1800)).catch(() => {})}>Load and fail</Btn> <Toggle label="glow" on={glow()} onChange={setGlow} /> <Toggle label="spinner" on={spinner()} onChange={setSpinner} /> </Row> </Panel> ) }