Page Progress
A thin bar at the top edge of the page that appears while something is loading and
retires when the answer lands. Mount it once, in your layout. It needs no opt-in
per screen: it hooks Livewire's message lifecycle and the wire:navigate events, so a
screen you build tomorrow is covered without anyone remembering to add anything to it.
It reports waiting, not work. A round trip has no percentage, so the bar eases towards a ceiling and slows the longer it waits; only the answer takes it to the end.
The two buttons above exist only to drive the demonstration. In an application nothing dispatches these events by hand — Livewire does, on every navigation and every component round trip.
Turn off Livewire's own bar
Livewire ships a navigation progress bar of its own, on by default. It reports
wire:navigate and never a component round trip, and it paints above anything you
mount yourself — so leaving it on gives you two bars stacked on each other.
// config/livewire.php
'navigate' => [
'show_progress_bar' => false,
],
Livewire reads that setting on the server, so this component cannot switch it off for you. It is one line, and it is the only setup step.
The delay is the feature
Most requests finish inside 180ms and show nothing. That is deliberate: a bar that
blinks on every click is noise, and noise is exactly what stops a reader noticing the
one request that really is slow. Raise showAfter on a fast internal tool, lower it on
a surface where most actions are slow anyway.
The bar never reaches 100% on its own. ceiling is where the easing stops, so 100
keeps meaning finished rather than the animation ran out. A bar that fills to the
end on a timer lies on the first request that runs long.
A poll is not a request the reader made
A screen with wire:poll makes a round trip every few seconds on its own. Those are
skipped: the bar only reports messages the reader caused.
The test is Livewire's own metadata — a message every one of whose actions is tagged as a poll is ignored. A message with no actions is a component refresh, not a poll, and is reported.
This is the defect that makes a page-edge bar worthless rather than merely imperfect. A bar running through every three seconds on its own teaches its reader to ignore it, and then it no longer reports the waiting that is worth reporting.
When to reach for something else
| You want to show | Use |
|---|---|
| A value somebody knows — an upload percentage, seats used | progress |
| How far through an article the reader has scrolled | reading-progress |
| That one element is waiting, in place | spinner |
| The shape of content that has not arrived | skeleton |
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
height |
sm | md | lg |
md |
Thickness of the strip. md is 3px. |
intent |
primary | neutral | success | warning | danger | info | auto |
primary |
Fill color. info is a synonym of primary; auto resolves to currentColor for an embedded surface that should match the text around it. |
showAfter |
int (ms) |
180 |
How long a request has to run before the bar appears at all. |
ceiling |
int (%) |
92 |
The percentage the easing approaches while it waits. |
scope |
string|null |
null |
Scoped personalization key. |
Accessibility
The bar carries aria-hidden="true", and that is a decision rather than an omission. It
is presentation with no text: a number creeping towards a ceiling is not something anybody
can act on, and putting "loading" into the one channel that cannot be skimmed on every
round trip would be noise.
Nothing here is a live region, and that is the point — the component exposes no
aria-live, no role="status" and no ARIA state, so it adds nothing to what a screen
reader already gets from the page change itself.
reading-progress makes the opposite call and exposes
role="progressbar" with aria-valuenow — because how far through an article you are is
something a reader acts on.
Under prefers-reduced-motion: reduce the bar keeps the report and loses the travel:
it still appears, and still says when the waiting ended, it just stops sliding to say
it. Removing the indicator entirely would take the information away from the readers
most likely to want it.
It never prints. A position: fixed strip does not scroll with the page when the page
becomes sheets — it lands on the text of whichever sheet it fell on.
Keyboard Interaction
None. The bar reports; it is not a control, it is not focusable, and it sets
pointer-events: none so it never intercepts a click on whatever sits under it.
Customization
Two token groups, both settable in your :root {}:
:root {
/* thickness per size */
--page-progress-height-sm: 2px;
--page-progress-height-md: 3px;
--page-progress-height-lg: 5px;
/* retint every bar on the page without touching a call site */
--page-progress-fill: var(--color-wk-accent);
}