# 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`](https://solid-route-progress.vercel.app/docs/controller.md#options) |

Set them on `:root`, in Tailwind's `@theme`, on a class, or inline.

```css
:root {
  --sp-color: oklch(0.62 0.19 264); /* or var(--color-indigo-500) */
  --sp-height: 2px;
}
```

```css
:root {
  color-scheme: light dark;
  --sp-color: light-dark(oklch(0.55 0.2 264), oklch(0.75 0.12 264));
}
```

## Utilities

```tsx
<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    |

```tsx
<main class="transition-opacity [[data-sp-busy]_&]:opacity-60">…</main>
```

```tsx
<Progress busyAttribute={false} /> // opt out
<Progress ariaBusy /> // also aria-busy="true"
```

## Inside a container

```tsx
<div class="relative overflow-hidden rounded-xl">
  <Progress controller={panel} class="absolute" />…
</div>
```

## Your own template

```tsx
import { Show } from 'solid-js'
import { Bar, useProgress } from 'solid-route-progress'

const Percent = () => {
  const progress = useProgress()
  // `value()` is the target, and while trickling that is `trickleTo`, not the drawn position
  return (
    <Show when={progress.state() !== 'trickle'}>
      <output>{Math.round(progress.value() * 100)}%</output>
    </Show>
  )
}

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

```css
.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](https://solid-route-progress.vercel.app/docs/examples.md#styling-recipes).

### Failed loads

```css
.sprogress[data-error] {
  --sp-color: oklch(0.63 0.19 23);
}
```

### Spinner

```tsx
<RouteProgress>
  <Bar />
  <div class="spinner" aria-hidden="true" />
</RouteProgress>
```

```css
.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;
  }
}
```

```tsx
<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

```css
.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

```css
/*
 * 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`. Motion is
 * `opacity` and `translate` only, so it runs on the compositor even while the page is busy.
 */

@property --sp-value {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

@layer components.sprogress {
  .sprogress {
    position: fixed;
    inset: 0 0 auto;
    z-index: var(--sp-z-index, 9999);
    height: var(--sp-height, 3px);
    pointer-events: none;
    visibility: visible;
    transition:
      opacity var(--sp-speed),
      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. A
       delayed 0s step, not a timed one: the main thread sleeps through the fade */
    transition-delay: 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; `translate` leaves `transform` to you */
    translate: calc((var(--sp-value) - 1) * 100%);
    /* set() / done(): a short, eased hop */
    transition: translate var(--sp-speed);
  }

  .sprogress:dir(rtl) .sprogress-bar {
    translate: calc((1 - var(--sp-value)) * 100%);
  }

  /* loading: fast out of the gate, then a long crawl — never quite arriving. Longhands, so a
     browser without linear() loses only the curve (falling back to ease), not the drift */
  .sprogress[data-state='trickle'] .sprogress-bar {
    transition-duration: var(--sp-trickle-duration, 10s);
    transition-timing-function: 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)
    );
  }

  /* 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-duration: 0s;
    transition-delay: var(--sp-speed);
  }

  /* forced colors strip backgrounds; paint the bar with the system highlight instead */
  @media (forced-colors: active) {
    .sprogress-bar {
      forced-color-adjust: none;
      background: Highlight;
    }
  }
}
```