solid-route-progress

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()

ButtonResult
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()

ButtonResult
Take a holdone more hold
Release onebar keeps going until the last hold goes
Release as cancelfades out without reaching 100%
Release as errorcompletes 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().

0%
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.

Linkdefault + filtershallow
?skip=goflashesignores
?skip=blockedfilter returns falseignores
?skip=quietdata-sp-ignoreignores
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.

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>
  )
}