Long Progress

← Back to components

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.

Verify your employment information.

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.