Overview
Reusable progress card that takes over a section while a long (10–30s) POST is in flight. Animates a fake-but-realistic timeline of step text and progress-bar fill so users don’t reload the page and kill the request. Fast-forwards to 100% on success and restores the original markup with an error banner on failure.
Designed for HTMX form takeovers, file uploads with server-side processing, and any other long-running POST that would otherwise leave the user staring at an unresponsive page.
Demo: vanilla defaults
new LongProgress(section).start() — no
config, just sensible defaults: a 4-phase ~22s timeline and the load-bearing
“don’t refresh” hint. Buttons compress the timeline for the demo so you
don’t have to wait the full duration.
Demo: custom phases (verify-account takeover)
Surface-specific copy and per-phase fill targets. Same shape as the existing verify-my-account flow, compressed to ~5s for the demo.
Demo: bytes + phases (upload card)
Bytes own the first 25% of the bar (driven by simulated
xhr.upload.progress). When the upload completes,
time-driven phases take over for the ~75% server-side processing window. The
inline option uses the row layout instead of the
centered card chrome.
myresume.pdf — ready to upload
Demo: pure upload (no server-side wait)
bytes: { capPct: 100 } + no phases — the
component degrades to a vanilla upload progress bar.
avatar.png — ready to upload
Demo: bar color variants
barVariant selects the fill color.
'info' is the default (solid Flowbite blue);
'orange' and
'blue'
are the kit's brand gradients;
'success' and
'danger' are solid Flowbite semantic colors that
auto-swap for dark mode.
Click a variant above to see the bar in that color.
Usage
Minimal — just works
import { LongProgress } from '@marketdataapp/ui/long-progress';
const lp = new LongProgress(sectionEl);
lp.start(); // mount the card, kick off timeline
await lp.fastForward(); // on success
lp.restore('Sorry, that failed.'); // on error
Custom phases (HTMX form takeover)
const lp = new LongProgress(sectionEl, {
phases: [
{ step: 'Verifying your employer information…', fillPct: 30, durationMs: 4000 },
{ step: 'Cross-referencing your documents…', fillPct: 65, durationMs: 8000 },
{ step: 'Finalizing your classification…', fillPct: 85, durationMs: 10000 },
{ step: 'Still working — this is taking longer than usual…', fillPct: 92, durationMs: 15000 },
],
errorInsertBefore: '.form-actions',
});
const unbind = lp.bindHtmx(formEl, {
errorMessages: {
responseError: (e) => `Sorry, the request failed (HTTP ${e.detail.xhr.status}).`,
sendError: "Sorry, couldn't reach the server. Please try again.",
timeout: 'The request took too long. Please try again.',
},
});
Upload (bytes + server-side processing)
const lp = new LongProgress(rowEl, {
inline: true,
bytes: { capPct: 25, step: 'Uploading…' },
phases: [ /* phases fire back-to-back after upload completes */
{ step: 'Classifying document type…', fillPct: 35, durationMs: 4500 },
{ step: 'Extracting evidence from document…', fillPct: 65, durationMs: 7500 },
{ step: 'Almost done…', fillPct: 85, durationMs: 8000 },
],
});
lp.start();
xhr.upload.onprogress = (e) => lp.setBytesProgress(e.loaded / e.total);
xhr.upload.onload = () => lp.setBytesProgress(1); // triggers phases
xhr.onload = () => lp.fastForward();
xhr.onerror = () => lp.restore('Upload failed. Please try again.');
API
Constructor options
| Option | Default | Purpose |
|---|---|---|
| hint | “Don’t refresh…” |
Warning text directly under the bar. Rendered via innerHTML so the default can
include <strong>. Never pass
untrusted input.
|
| phases | Generic 4-phase ~22s timeline |
Each phase:
{ step, fillPct, durationMs }. Phases run
back-to-back; the bar fills from the previous phase's
fillPct to this one's over
durationMs. First phase fires at
start() (or at upload-complete in bytes
mode). Pass [] to disable.
|
| bytes | null |
{ capPct, step? }. Enables bytes mode for
upload surfaces.
|
| inline | false |
true swaps the centered card layout for the
inline-row variant.
|
| errorInsertBefore | null | CSS selector in the restored DOM to insert the error banner before. Falls back to appending to the section. |
| doneStep | 'Done' |
Step text shown briefly during
fastForward().
|
| barVariant | 'info' |
Fill color. 'orange' and
'blue' use the kit's brand gradients (same
as btn-orange-to-blue /
btn-blue-to-orange resting states).
'info',
'success',
'danger' use solid Flowbite semantic colors
(auto dark mode).
|
Methods
| Method | Description |
|---|---|
| start() |
Saves section.innerHTML, mounts the card,
starts the phase timer (immediately if not in bytes mode).
|
| setBytesProgress(fraction) |
Update bar to fraction * capPct. Monotonic.
When fraction === 1, fires the phase timer.
|
| startPhases() |
Manual phase-timer trigger. Use when you prefer
xhr.upload.onloadstart over the byte
signal.
|
| fastForward({ holdMs }) |
Cancels pending phases, fills to 100% over
holdMs (default 200), sets step to done.
Returns a promise that resolves after the transition.
|
| restore(message?) |
Restores original innerHTML, re-runs
htmx.process() if HTMX is loaded, inserts
an admonition admonition-danger banner with
role="alert".
|
| bindHtmx(formEl, opts) |
Wires HTMX lifecycle events (beforeRequest,
beforeSwap, error events). Returns an
unbind function.
|