Styling
Custom properties
| Property | Default | Meaning |
|---|---|---|
--sp-color | oklch(0.65 0.14 241) | bar color |
--sp-height | 3px | bar thickness |
--sp-z-index | 9999 | bar stacking order |
--sp-start | 0.08 | value the bar is revealed at |
--sp-trickle-duration | 10s | how long the loading drift takes |
--sp-trickle-easing | linear(…) | shape of the drift |
--sp-speed | the speed option | read-only; set it through speed |
Set them on :root, in Tailwind's @theme, on a class, or inline.
:root {
--sp-color: oklch(0.62 0.19 264); /* or var(--color-indigo-500) */
--sp-height: 2px;
}:root {
color-scheme: light dark;
--sp-color: light-dark(oklch(0.55 0.2 264), oklch(0.75 0.12 264));
}Utilities
<RouteProgress class="h-1 data-[state=done]:opacity-50" />The stylesheet sits in the components.sprogress sublayer, so utilities, your own @layer components rules and unlayered CSS all beat it without !important.
Hooks for the rest of the page
| Hook | Where | When |
|---|---|---|
data-state="idle | trickle | active | done" | the bar | always |
data-error | the bar | done phase of a failed load |
data-sp-busy | <html> | while any bar is visible |
<main class="transition-opacity [[data-sp-busy]_&]:opacity-60">…</main><Progress busyAttribute={false} /> // opt out
<Progress ariaBusy /> // also aria-busy="true"Inside a container
<div class="relative overflow-hidden rounded-xl">
<Progress controller={panel} class="absolute" />…
</div>Your own template
import { Bar, useProgress } from 'solid-route-progress'
const Percent = () => <output>{Math.round(useProgress().value() * 100)}%</output>
<RouteProgress>
<Bar class="rounded-r-full" />
<Percent />
</RouteProgress>--sp-value is registered as a <number>, so your own elements can transition it. Keep the var() fallbacks.
.ring {
background: conic-gradient(
var(--sp-color, oklch(0.65 0.14 241)) calc(var(--sp-value) * 1turn),
transparent 0
);
transition: --sp-value var(--sp-trickle-duration, 10s) var(--sp-trickle-easing, ease-out);
}Recipes
They are not shipped, so they cost nothing until you paste them. See them live in Examples.
Failed loads
.sprogress[data-error] {
--sp-color: oklch(0.63 0.19 23);
}Spinner
<RouteProgress>
<Bar />
<div class="spinner" aria-hidden="true" />
</RouteProgress>.spinner {
position: absolute;
top: 15px;
inset-inline-end: 15px;
box-sizing: border-box;
width: 18px;
height: 18px;
border: 2px solid transparent;
border-block-start-color: var(--sp-color, oklch(0.65 0.14 241));
border-inline-start-color: var(--sp-color, oklch(0.65 0.14 241));
border-radius: 50%;
animation: spinner 400ms linear infinite;
}
.sprogress[data-state='idle'] .spinner {
animation: none;
}
@media (prefers-reduced-motion: reduce) {
.spinner {
display: none;
}
}
@keyframes spinner {
to {
rotate: 1turn;
}
}<div
aria-hidden="true"
class="absolute end-4 top-4 size-4.5 animate-spin rounded-full border-2 border-transparent border-s-(color:--sp-color) border-t-(color:--sp-color) in-data-[state=idle]:animate-none motion-reduce:hidden"
/>Glow
.sprogress-bar::after {
content: '';
position: absolute;
inset-inline-end: 0;
top: 0;
width: 100px;
height: 100%;
box-shadow:
0 0 10px var(--sp-color, oklch(0.65 0.14 241)),
0 0 5px var(--sp-color, oklch(0.65 0.14 241));
transform: rotate(3deg) translateY(-4px);
}
.sprogress:dir(rtl) .sprogress-bar::after {
transform: rotate(-3deg) translateY(-4px);
}Built in
| RTL | the bar slides in from the right under dir="rtl" |
| Forced colors | paints with the system Highlight color |
The stylesheet
/* * solid-route-progress — all rendering lives here. JS only writes `--sp-value`, `--sp-speed` and `data-state`. * * Tunables (set them anywhere: `:root`, `@theme`, a class, or inline): * --sp-color bar color oklch(0.65 0.14 241) * --sp-height bar thickness 3px * --sp-z-index bar stacking order 9999 * --sp-start value the bar is revealed at 0.08 * --sp-trickle-duration length of the loading drift 10s * --sp-trickle-easing shape of the loading drift linear() curve * * `--sp-speed` mirrors the `speed` option so CSS transitions and JS timers agree: read it in * your own rules, set it through `speed`. * * Everything sits in the `components.sprogress` sublayer, so Tailwind utilities, your own * `@layer components` rules and unlayered CSS all win without `!important`. */ @property --sp-value { syntax: '<number>'; inherits: true; initial-value: 0; } @layer components.sprogress { .sprogress { position: fixed; inset: 0 0 auto 0; z-index: var(--sp-z-index, 9999); height: var(--sp-height, 3px); pointer-events: none; visibility: visible; transition: opacity var(--sp-speed) ease, visibility 0s; } .sprogress[data-state='idle'] { opacity: 0; visibility: hidden; /* keep painting while the opacity fades, then drop out of the a11y tree / hit-testing */ transition: opacity var(--sp-speed) ease, visibility 0s var(--sp-speed); } .sprogress-bar { position: absolute; inset: 0; background: var(--sp-color, oklch(0.65 0.14 241)); /* full-width bar slid in from the inline-start edge */ transform: translateX(calc((var(--sp-value) - 1) * 100%)); /* fast out of the gate, then a long crawl — never quite arriving */ transition: transform var(--sp-trickle-duration, 10s) var( --sp-trickle-easing, linear(0, 0.25 5%, 0.4 10%, 0.55 18%, 0.7 30%, 0.8 45%, 0.88 60%, 0.94 75%, 0.98 90%, 1) ); } .sprogress:dir(rtl) .sprogress-bar { transform: translateX(calc((1 - var(--sp-value)) * 100%)); } /* hidden: park the bar at the start position, but only after the fade-out finished */ .sprogress[data-state='idle'] .sprogress-bar { --sp-value: var(--sp-start, 0.08); transition: transform 0s var(--sp-speed); } /* explicit set() / done(): short, eased hop instead of the long drift */ .sprogress:is([data-state='active'], [data-state='done']) .sprogress-bar { transition-duration: var(--sp-speed); transition-timing-function: ease; } /* forced colors strip backgrounds; paint the bar with the system highlight instead */ @media (forced-colors: active) { .sprogress-bar { forced-color-adjust: none; background: Highlight; } } }