Rental
Bulk Progress
Progress for a long run over many items — posting readings, generating invoices, sending reminders — where individual items can fail without stopping the rest.
Month-end in a rental system is a batch: 120 invoices, 90 reminders, a meter reading posted for every unit. Some of them will fail — a unit with no reading, a tenant with no email — and the rest should carry on regardless.
A bar that only says “84 of 120” leaves the reader to find out later which three were skipped. This keeps the failed items and their reasons in view while the run goes, and offers them back for a retry when it stops.
Preview
Section titled “Preview”<nana-bulk-progress
label="Generating invoices"
unit="invoice"
status="complete"
total="120"
done="117"
open
failures='[{"label":"Unit 4B","reason":"No meter reading for August"},{"label":"Unit 9A","reason":"Tenant has no email address"},{"label":"Unit 12","reason":"Rent not set on the tenancy"}]'
style="max-width: 32rem"
></nana-bulk-progress> Installation
Section titled “Installation”npx @nana-tec/ui add bulk-progressLands as src/components/ui/bulk-progress/, with progress.ts beside the
component — the counting and the wording live there, so a worker or a job
summary can reuse them without importing the view.
npx @nana-tec/ui add bulk-progressLands in src/lib/components/ui/bulk-progress/ as BulkProgress.svelte, with
progress.ts beside it holding the counting and the wording.
npm install @nana-tec/ui-componentsOr copy the source in and own it:
npx @nana-tec/ui add bulk-progressimport { BulkProgress } from "@/components/ui/bulk-progress";
export function InvoiceRun({ run, retry }) { return ( <BulkProgress label="Generating invoices" unit="invoice" status={run.status} total={run.total} done={run.done} failures={run.failures} onRetry={(failures) => retry(failures.map((f) => f.id))} /> );}failures is an array of { label, reason?, id? }. onRetry hands the whole
array back, id included, so the caller knows exactly which items to run again.
<script lang="ts"> import { BulkProgress } from '$lib/components/ui/bulk-progress';
let { run, retry } = $props();</script>
<BulkProgress label="Generating invoices" unit="invoice" status={run.status} total={run.total} done={run.done} failures={run.failures} onRetry={(failures) => retry(failures.map((f) => f.id))}/>import "@nana-tec/ui-components/bulk-progress";<nana-bulk-progress label="Generating invoices" unit="invoice" status="running" total="120" done="84"></nana-bulk-progress>Set failures as a property from JavaScript — or as JSON in the attribute,
which is what the examples on this page do:
const run = document.querySelector("nana-bulk-progress");
run.failures = [{ id: "u-4b", label: "Unit 4B", reason: "No meter reading for August" }];run.addEventListener("nana-retry", (e) => retry(e.detail.failures));While it runs
Section titled “While it runs”status="running" counts up. The line under the bar reports what has come back
— successes and failures together, so it matches the bar — and names the
failures separately. cancellable adds a stop button that fires nana-cancel.
<nana-bulk-progress
label="Posting meter readings"
unit="reading"
status="running"
total="120"
done="84"
cancellable
failures='[{"label":"Unit 4B","reason":"No meter reading for August"}]'
style="max-width: 32rem"
></nana-bulk-progress> Retry is deliberately absent until the run stops: retrying halfway would race the run itself.
When the total is unknown
Section titled “When the total is unknown”Leave total at 0 and the bar runs indeterminate, counting what has come back
so far. Useful when the work is streamed rather than counted up front.
<nana-bulk-progress
label="Importing tenancies"
unit="tenancy"
units="tenancies"
status="running"
total="0"
done="47"
style="max-width: 32rem"
></nana-bulk-progress> units covers the plurals an s will not fix.
How it ends
Section titled “How it ends”| Status | Reads as | Tone |
|---|---|---|
idle | 120 invoices to process | Neutral |
running | 87 of 120 invoices · 3 failed | Primary |
complete, nothing failed | All 120 invoices done | Success |
complete, some failed | 117 of 120 invoices done · 3 failed | Warning |
cancelled | Stopped after 41 of 120 invoices · 1 failed | Warning |
A run that ends with failures opens its failure list once, unasked — the reader has come back to a finished job and the failures are the only thing left to do. Once they close it themselves, it stays as they left it.
<nana-bulk-progress
label="Sending rent reminders"
unit="reminder"
status="cancelled"
total="90"
done="40"
failures='[{"label":"Unit 2C","reason":"Phone number is not a valid MSISDN"}]'
style="max-width: 32rem"
></nana-bulk-progress> Your own failure rows
Section titled “Your own failure rows”The generated list is a label and a reason. Anything richer — a link to the
unit, a per-item retry, a status chip — goes in the failures slot on the Web
Component, or the renderFailures prop on the framework builds.
<nana-bulk-progress
label="Generating invoices"
unit="invoice"
status="complete"
total="120"
done="118"
open
failures='[{"label":"Unit 4B"},{"label":"Unit 9A"}]'
style="max-width: 32rem"
>
<table
slot="failures"
style="width: 100%; font-size: 0.875rem; border-collapse: collapse"
>
<tbody>
<tr>
<td style="padding: 0.25rem 0; white-space: nowrap">Unit 4B</td>
<td>No meter reading</td>
<td><a href="#unit-4b">Open unit</a></td>
</tr>
<tr>
<td style="padding: 0.25rem 0; white-space: nowrap">Unit 9A</td>
<td>No email address</td>
<td><a href="#unit-9a">Open unit</a></td>
</tr>
</tbody>
</table>
</nana-bulk-progress> Accessibility
Section titled “Accessibility”The bar carries role="progressbar" with its current value, and the failure
list is a proper disclosure — a button with aria-expanded and aria-controls
over the region it opens.
The spoken summary is deliberately coarse. A live region that changed on every one of 120 items would talk over the page for the length of the run, so the announcement only moves at the milestones: the run starts, the run ends, the run is stopped. The count itself stays readable on screen and through the progress bar’s own value.
The React and Svelte props, read from the components’ own types:
BulkProgress
| Prop | Type | Default |
|---|---|---|
labelWhat the run is called. | string | '' |
totalHow many items the run covers; `0` while the count is still unknown. | number | 0 |
doneHow many items have come back successfully. | number | 0 |
failuresThe items that failed, newest last. | BulkFailure[] | [] |
status | BulkProgressStatus | 'idle' |
unitWhat the items are called, singular. | string | 'item' |
unitsPlural of `unit`, for the words an `s` will not fix. | string | |
renderFailuresReplaces the generated failure list. | ReactNode | |
onRetry | (failures: BulkFailure[]) => void | |
onCancel | () => void | |
retryLabel | string | 'Retry failed' |
cancelLabel | string | 'Cancel' |
actions | ReactNode | |
children | ReactNode |
Also accepts Omit<HTMLAttributes<HTMLDivElement>, 'children'>.
BulkProgress
| Prop | Type | Default |
|---|---|---|
labelWhat the run is called. | string | '' |
totalHow many items the run covers; `0` while the count is still unknown. | number | 0 |
doneHow many items have come back successfully. | number | 0 |
failuresThe items that failed, newest last. | BulkFailure[] | [] |
status | BulkProgressStatus | 'idle' |
unitWhat the items are called, singular. | string | 'item' |
unitsPlural of `unit`, for the words an `s` will not fix. | string | |
renderFailuresReplaces the generated failure list. | Snippet | |
onRetry | (failures: BulkFailure[]) => void | |
onCancel | () => void | |
retryLabel | string | 'Retry failed' |
cancelLabel | string | 'Cancel' |
actions | Snippet | |
children | Snippet |
Also accepts Omit<HTMLAttributes<HTMLDivElement>, 'class' | 'children'>.
The attribute names, events and CSS hooks below are the Web Components reference.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
cancelLabel | cancel-label | string | 'Cancel' | |
cancellable | cancellable | boolean | false | Offer to stop the run while it is going |
done | done | number | 0 | How many items have come back successfully |
failures | failures | BulkFailure[] | [] | The items that failed, newest last. Set the property to an array, or the attribute to the same array as JSON. |
label | label | string | '' | What the run is called |
noRetry | no-retry | boolean | false | Hide the retry button |
open | open | boolean | false | Show the failure list expanded |
retryLabel | retry-label | string | 'Retry failed' | |
status | status | BulkProgressStatus | 'idle' | Where the run has got to |
total | total | number | 0 | How many items the run covers; 0 while the count is still unknown |
unit | unit | string | 'item' | What the items are called, singular |
units | units | string | '' | Plural of unit, for the words an s will not fix |
Events
| Event | Description |
|---|---|
nana-retry | The reader asked to run the failed items again (detail.failures) |
nana-cancel | The reader asked to stop the run |
Slots
| Slot | Description |
|---|---|
(default) | Extra detail under the summary |
failures | Replaces the generated failure list |
actions | Extra buttons beside the retry control |
CSS parts
| Part | Description |
|---|---|
::part(base) | The container |
::part(header) | The title row |
::part(title) | The run's name |
::part(status) | The status chip |
::part(track) | The progress bar's track, forwarded from nana-progress-bar |
::part(fill) | The progress bar's fill, forwarded from nana-progress-bar |
::part(summary) | The count line under the bar |
::part(failures) | The failure region |
::part(failure) | One failed item |
::part(toggle) | The disclosure button for the failure list |
::part(actions) | The actions row |
::part(retry) | The retry button |
::part(cancel) | The cancel button |
CSS custom properties
| Property | Description |
|---|---|
--nana-bulk-progress-bg | Container background |
--nana-bulk-progress-border | Container border colour |
--nana-bulk-progress-list-height | Height the failure list scrolls after |
--nana-press-scale | How far the buttons depress on press, shared with the other pressable controls |
Events
Section titled “Events”| Event | Type | Description |
|---|---|---|
nana-retry | CustomEvent | The reader asked to run the failed items again (detail.failures) |
nana-cancel | CustomEvent | The reader asked to stop the run |
| Slot | Description |
|---|---|
(default) | Extra detail under the summary |
failures | Replaces the generated failure list |
actions | Extra buttons beside the retry control |
CSS Parts
Section titled “CSS Parts”| Part | Description |
|---|---|
base | The container |
header | The title row |
title | The run’s name |
status | The status chip |
summary | The count line under the bar |
failures | The failure region |
failure | One failed item |
toggle | The disclosure button for the failure list |
actions | The actions row |
retry | The retry button |
cancel | The cancel button |