Source Code

<div class="demo">
  <button class="run-btn" id="runBtn">Run bulk export (6 items)</button>

  <div class="batch-panel" id="batchPanel" hidden>
    <div class="batch-header">
      <span class="batch-title">Exporting 6 items</span>
      <span class="batch-summary" id="batchSummary">0 / 6 complete</span>
    </div>
    <div class="batch-bar-track"><div class="batch-bar-fill" id="batchBarFill"></div></div>
    <ul class="batch-list" id="batchList"></ul>
    <div class="batch-footer" id="batchFooter" hidden>
      <span id="batchFooterText"></span>
      <button class="retry-btn" id="retryFailedBtn" hidden>Retry failed</button>
    </div>
  </div>
</div>

Batch Operation Progress Panel — Per-Item Success/Fail Tracking with Retry

Batch Operation Progress Panel — Per-Item Success/Fail Tracking · Dashboards · Plain HTML, CSS & JS · Live preview

What's included

Features

Every item tracked through its own independent pending/running/done/failed state, not one aggregate batch status
Bounded concurrency (configurable worker count) processes items in parallel without overwhelming a real backend
Progress bar and text summary are both derived from the same live item-state data, so they can never drift out of sync
Retry action re-runs only the items that actually failed, leaving already-succeeded items untouched
Per-item visual status (spinner, checkmark, or failure icon) gives immediate feedback on which specific items had problems
Footer summary only appears once every item has reached a terminal (done or failed) state
Scrollable item list handles batches of any size without the panel growing unbounded

About this UI Snippet

Batch Operation Progress Panel — Tracking Every Item Independently, Correctly

Screenshot of the Batch Operation Progress Panel — Per-Item Success/Fail Tracking snippet rendered live

A single progress bar for a bulk operation ("Exporting… 60%") tells a user *something* is happening, but not *what* — if three of twenty items failed, a bare percentage bar can't communicate that, and a "try again" button has to retry the entire batch, including the seventeen items that already succeeded. This snippet tracks every item's status independently and renders it individually, so failures are visible per-item and retrying only re-runs what actually needs it.

Every item has its own state, tracked in a plain object keyed by id

itemStates maps each item's id to one of four states: pending, running, done, or failed. Nothing about the batch as a whole is stored as a single aggregate flag — the overall progress bar and summary text (updateSummary()) are entirely *derived* from counting how many individual items are in each state, recalculated fresh every time any single item's state changes. This is what makes it possible to show a genuine per-item breakdown rather than a single opaque percentage.

Limited concurrency — a realistic middle ground

runBatch() doesn't process items one at a time (slow, and it wastes the fact that most real export/upload operations are I/O-bound and can run several in parallel) and doesn't fire all of them simultaneously either (which could overwhelm a real backend API or exceed a browser's connection limit). Instead, it spins up a fixed number of worker() functions (here, 3) that each pull the next item off a shared queue and process it, one at a time, until the queue is empty — a standard bounded-concurrency pattern that keeps a batch of any size processing efficiently without unbounded parallelism.

Retry only touches what actually failed

retryBtn's click handler filters ITEMS down to just the ones whose current state is 'failed', and calls the exact same runBatch() function with only that subset. Because every item's state persists independently in itemStates across the whole session, the items that already succeeded are never touched again — no wasted re-work, and no risk of accidentally re-running (and potentially duplicating the effect of) an already-successful operation.

The progress bar and summary text are always in sync because they share one source of truth

Both the width of .batch-bar-fill and the "X / Y complete" text are computed inside the same updateSummary() call, from the same itemStates snapshot, every single time any item transitions state. There's no separate counter being incremented in parallel with the state object — the visual bar and the text summary can never drift apart from each other because they're two different renderings of the exact same underlying data.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain why deriving the progress bar and summary text from the same underlying item-state data (rather than maintaining a separately incremented counter) prevents them from ever disagreeing with each other, and to walk through exactly how the bounded-concurrency worker pool decides which item each worker processes next. It's also worth asking for a version that adds a cancel-in-progress action (aborting only the items not yet started), or one that shows a specific error message per failed item rather than a generic failure indicator.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a batch operation progress panel in HTML, CSS, and vanilla JavaScript — no external library.

Requirements:
- A list of at least six named items, each with its own visual status indicator that can independently show pending, running (with a spinner), done (success), or failed states — one item's outcome must never affect how any other item is processed or displayed.
- Process items with bounded concurrency: run a configurable fixed number of items in parallel (e.g. 3 at a time) via a worker-pool pattern pulling from a shared queue, rather than running everything fully sequentially or with fully unbounded parallelism.
- Simulate each item's processing with a random delay and a roughly 25% independent chance of failure, structured so it's clear where a real API call per item would be substituted in.
- Derive both an overall progress bar's fill percentage and a text summary ("X / Y complete") from the same underlying per-item state data every time any item's state changes, so the two can never show inconsistent information relative to each other.
- Once every item reaches a terminal (done or failed) state, show a footer summarizing exactly how many succeeded versus failed, and reveal a "Retry failed" button only if at least one item actually failed.
- Clicking "Retry failed" must re-run the batch process using only the items currently in a failed state, leaving already-succeeded items completely untouched and not reprocessed.
- Disable the main run button for the entire duration of an in-progress batch to prevent starting a second overlapping batch.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Click "Run bulk export"All six items start processing with bounded concurrency (three running at a time), each showing its own spinner while in progress.
  2. 2
    Watch items complete individuallyEach item independently lands on a done (green check) or failed (red exclamation) state — the outcome of one item never affects the others.
  3. 3
    Check the footer once the batch finishesShows an exact success/failure breakdown, and a "Retry failed" button appears only if at least one item actually failed.
  4. 4
    Click "Retry failed"Re-runs only the items still in a failed state — items that already succeeded are left untouched and are not re-processed.
  5. 5
    Adapt simulateItem() to a real requestReplace the setTimeout-based simulation with your actual per-item API call, resolving the promise based on that item's real success or failure.

Real-world uses

Common Use Cases

ADMIN
Bulk export or import operations
Exporting multiple reports, importing multiple records, or processing a batch of uploaded files with per-item outcome visibility.
DEVOPS
Multi-target deployment or sync jobs
Deploying to multiple environments or syncing multiple resources, where some targets may succeed while others fail independently.
EMAIL
Bulk email or notification sends
Sending a campaign to multiple recipients or channels where individual delivery failures need to be visible and retryable.
Batch data processing pipelines
Any operation applied across many independent records where partial failure is expected and should be handled gracefully, not treated as an all-or-nothing outcome.
Related: CompressionStream API Demo
See the CompressionStream API Demo for a related dashboards pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

No — every item is processed and tracked completely independently. One item failing has no effect on whether other items continue processing; the worker pool simply moves on to the next queued item regardless of the previous one's outcome.

Fully sequential processing wastes time when the underlying operations are I/O-bound and could run in parallel. Fully unbounded parallel processing risks overwhelming a real backend API or hitting browser connection limits. A fixed worker pool (here, 3 concurrent workers) balances throughput against not overloading the target system.

Only the items whose current state is "failed" are included in a retry batch — items that already succeeded keep their "done" state and are never re-processed, avoiding wasted work or accidental duplicate effects.

It's the count of items in a terminal state (done or failed) divided by the total item count — both the bar width and the "X / Y complete" text are derived from this same calculation inside updateSummary(), so they always stay consistent with each other.

Replace the body of simulateItem() with your actual async request for that one item (e.g. a fetch call), keeping the same pattern of setting itemStates[item.id] to "running" before the request and to "done" or "failed" based on the request's actual outcome once it resolves or rejects.

The run button is disabled for the full duration of the batch (from the moment it starts until every item reaches a terminal state), preventing a second overlapping batch from being started accidentally.