Skip to content

Upload Queue

UploadSince 0.3.0
import { UploadQueue } from '@absoluteui/components/upload-queue';
import '@absoluteui/components/upload-queue/style.css';

Using Angular? See Upload Queue for Angular.

A list of files being uploaded. It provides:

  • a Progress Bar per item, with size and status text (queued, uploading, done, failed, cancelled);
  • cancel, retry, and remove actions per item;
  • a transport-agnostic upload interface: you supply upload(file, { signal, onProgress, attempt, id }); the queue schedules calls with a concurrency limit, cancels through AbortController, and retries with exponential backoff;
  • polite announcements for failures, cancellations, removals, and batch completion;
  • optional image previews through object URLs, revoked when items are removed or the queue is destroyed.

The queue never performs a network request. Only your upload function does. It works with any source of File objects; File Upload can feed it, but neither needs the other.

  • Uploading files with visible progress and the ability to cancel or retry.
  • Chunked or resumable uploads (the consumer function loops over chunks and reports progress).
  • Together with File Upload, or on its own with files from any source.
  • A single small file submitted with the form: submit it natively with File Upload.
  • Background sync that must survive page reloads: the queue lives in memory.

Queue fed by File Upload, uploading with fetch

Section titled “Queue fed by File Upload, uploading with fetch”
import { FileUpload, type FileUploadChangeDetail } from '@absoluteui/components/file-upload';
import { UploadQueue } from '@absoluteui/components/upload-queue';
const pickerHost = document.querySelector<HTMLElement>('#picker');
const queueHost = document.querySelector<HTMLElement>('#queue');
if (pickerHost && queueHost) {
const picker = FileUpload.create(pickerHost, { label: 'Documents', multiple: true, maxFileSize: 50_000_000 });
const queue = UploadQueue.create(queueHost, {
label: 'Uploads',
upload: async (file, { signal }) => {
const response = await fetch('/api/files', { method: 'POST', body: file, signal });
if (!response.ok) throw new Error(String(response.status));
return response.json();
},
});
picker.host.addEventListener('aui-change', (e) => {
queue.add((e as CustomEvent<FileUploadChangeDetail>).detail.added);
picker.setValue([]);
});
}
import { UploadQueue, type UploadFunction } from '@absoluteui/components/upload-queue';
const CHUNK = 5_000_000;
const upload: UploadFunction = async (file, { signal, onProgress, id }) => {
for (let offset = 0; offset < file.size; offset += CHUNK) {
const chunk = file.slice(offset, offset + CHUNK);
const response = await fetch(`/api/uploads/${id}?offset=${String(offset)}`, { method: 'PUT', body: chunk, signal });
if (!response.ok) throw new Error(String(response.status));
onProgress(Math.min(offset + CHUNK, file.size));
}
return null;
};
const host = document.querySelector<HTMLElement>('#queue');
if (host) UploadQueue.create(host, { label: 'Uploads', upload, concurrency: 3, retries: 2, errorMessage: () => 'The file could not be uploaded. Try again.' });
import { UploadQueue, type UploadQueueCompleteDetail } from '@absoluteui/components/upload-queue';
import '@absoluteui/components/progress-bar/style.css';
import '@absoluteui/components/upload-queue/style.css';
const host = document.querySelector<HTMLElement>('#queue');
if (host) {
const queue = UploadQueue.create(host, {
label: 'Photos',
preview: true,
retries: 1,
upload: (file, { signal, onProgress }) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/photos');
xhr.upload.onprogress = (e) => { onProgress(e.loaded, e.total); };
xhr.onload = () => { if (xhr.status < 300) resolve(xhr.response); else reject(new Error(String(xhr.status))); };
xhr.onerror = () => { reject(new Error('network')); };
signal.addEventListener('abort', () => { xhr.abort(); reject(new DOMException('Aborted', 'AbortError')); });
xhr.send(file);
}),
});
queue.host.addEventListener('aui-complete', (e) => {
const { failed } = (e as CustomEvent<UploadQueueCompleteDetail>).detail;
if (failed === 0) queue.clearCompleted();
});
}
import { UploadQueue } from '@absoluteui/components/upload-queue';
const host = document.createElement('div');
document.body.append(host);
const queue = UploadQueue.create(host, {
label: 'Uploads',
upload: async (file, { signal }) => {
const r = await fetch('/api/files', { method: 'POST', body: file, signal });
if (!r.ok) throw new Error(String(r.status));
return r.json();
},
});
queue.add([new File(['hello'], 'hello.txt', { type: 'text/plain' })]);

More examples (File Upload integration, chunked upload), type-checked in CI: metadata/upload-queue/examples.json.

<div class="aui-upload-queue">
<div class="aui-upload-queue__label" id="L">Uploads</div>
<div class="aui-upload-queue__summary">1 of 2 uploaded</div>
<ul class="aui-upload-queue__list" aria-labelledby="L" tabindex="-1">
<li class="aui-upload-queue__item" data-status="uploading">
<span class="aui-upload-queue__media"><svg aria-hidden="true"/> | <img alt="" src="blob:…"></span>
<div class="aui-upload-queue__progress"> (Progress Bar: label = file name, description = "2 MB · Uploading")</div>
<div class="aui-upload-queue__actions">
<button class="aui-upload-queue__action aui-upload-queue__cancel" aria-label="Cancel upload of a.pdf">
<button class="… aui-upload-queue__retry" aria-label="Retry upload of a.pdf" hidden>
<button class="… aui-upload-queue__remove" aria-label="Remove a.pdf" hidden>
</div>
</li>
</ul>
</div>
UploadQueue.create(host: HTMLElement, options: UploadQueueOptions): UploadQueue

Global configuration key: uploadQueue (see Configuration).

Option Type Default Description
label string Visible label and accessible name of the list. Required.
upload (file: File, context: { signal: AbortSignal; onProgress: (loaded: number, total?: number) => void; attempt: number; id: string }) => Promise<unknown> Transport: performs one attempt. Resolve on success (the value becomes item.response), reject on failure, honour signal. Required.
preview boolean false Image previews via object URLs for image/* files (needs img-src blob:).
errorMessage (error: unknown, file: File) => string | null Map a rejection to a user-facing message. Default: generic localized message.
icons Partial<{ file: IconRef; cancel: IconRef; retry: IconRef; remove: IconRef }> Icon slot overrides.
concurrency number 2 Uploads running at the same time (1..16). Globally configurable.
retries number 0 Automatic retries after a failed attempt (0..10). Globally configurable.
retryDelay number 1000 Delay before the first automatic retry in ms (0..60000); doubles for each further retry. Globally configurable.
autoStart boolean true Start uploading as soon as files are added; when false, call start(). Globally configurable.
locale string "en-US" Message and size-formatting locale. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement Host element.
list HTMLUListElement The list element.
items readonly UploadItem[] Snapshots of all items in queue order.
busy boolean Any item queued or uploading.
options Readonly<UploadQueueOptions> Current options.
Method Description
add(files: Iterable<File>): string[] Queue files; returns item ids.
start(): void Start queued items (autoStart: false).
cancel(id: string): void Cancel a queued or uploading item (aborts its signal).
cancelAll(): void Cancel every active item.
retry(id: string): void Queue a failed or cancelled item again.
remove(id: string): void Remove an item (aborts it; revokes its preview URL).
clear(): void Remove every item.
clearCompleted(): void Remove finished items.
getItem(id: string): UploadItem | null One item snapshot.
update(options: Partial<UploadQueueOptions>): void Merge and apply options.
destroy(): void Abort all uploads, revoke preview URLs, remove rendering. Late results are ignored.
Event Detail Bubbles Cancelable Description
aui-change { items: readonly UploadItem[]; reason: 'added' | 'started' | 'succeeded' | 'failed' | 'cancelled' | 'retried' | 'removed'; item: UploadItem | null } Yes No Items were added or removed, or an item changed status (not emitted for progress).
aui-complete { succeeded: number; failed: number; cancelled: number } No No Nothing is queued or uploading any more (once per batch).

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-upload-queue Host.
aui-upload-queue__label Visible label.
aui-upload-queue__summary “{done} of {total} uploaded” or the empty text.
aui-upload-queue__list List (focus fallback, tabindex -1).
aui-upload-queue__item Row; data-status holds the status.
aui-upload-queue__media File icon or preview.
aui-upload-queue__preview Preview image.
aui-upload-queue__progress Progress Bar host.
aui-upload-queue__actions Row actions.
aui-upload-queue__action | __cancel | __retry | __remove Action buttons.
Slot Default icon
file file
cancel close
retry refresh
remove close

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border-strong | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-hover | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-muted | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-sm | --aui-font-weight-medium | --aui-icon-size-md | --aui-icon-size-sm | --aui-line-height-tight | --aui-radius-md | --aui-radius-sm | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-touch-target-min

type UploadStatus = 'queued' | 'uploading' | 'done' | 'failed' | 'cancelled';
interface UploadContext { readonly signal: AbortSignal; readonly onProgress: (loaded: number, total?: number) => void; readonly attempt: number; readonly id: string }
type UploadFunction = (file: File, context: UploadContext) => Promise<unknown>;
interface UploadItem { readonly id: string; readonly file: File; readonly status: UploadStatus; readonly loaded: number; readonly total: number; readonly attempts: number; readonly error: string | null; readonly response: unknown }
interface UploadQueueConfig { concurrency: number; retries: number; retryDelay: number; autoStart: boolean }
interface UploadQueueOptions extends Partial<UploadQueueConfig> {
label: string; upload: UploadFunction; preview?: boolean;
errorMessage?: (error: unknown, file: File) => string | null; locale?: string; icons?: Partial<UploadQueueIcons>;
}
interface UploadQueueChangeDetail { items: readonly UploadItem[]; reason: UploadQueueChangeReason; item: UploadItem | null }
interface UploadQueueCompleteDetail { succeeded: number; failed: number; cancelled: number }

Pattern: List of Progress Bars (ARIA progressbar) with native action buttons and a polite live region for outcomes.

Key Context Action
Tab / Shift+Tab Any Moves through the visible action buttons of each row.
Enter / Space Action focused Cancels, retries, or removes. When the focused action disappears, focus moves to the first action of the same row; after removal, to the next row, else the list.
Element Role
list list (native <ul>, aria-labelledby the label)
item progress progressbar (Progress Bar), named by the file name
actions button (native), named per file
preview image img with alt=“” (decorative; the name is adjacent)
Attribute Element Value
aria-labelledby list Label id
aria-describedby progressbar Size and status text, including the error
aria-label actions “Cancel upload of {file}”, “Retry upload of {file}”, “Remove {file}”

Actions show a :focus-visible outline. Focus is never moved by progress or status changes, only after the user’s own action removed the focused control.

Criterion Level Note
1.1.1 Non-text Content A
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
2.5.8 Target Size (Minimum) AA
4.1.2 Name, Role, Value A
4.1.3 Status Messages AA

Automated test results: Accessibility and compliance.

  • Announcements (polite): each failure and cancellation, removals, and one summary when the batch completes. Progress is never announced continuously.
  • Status is text in each row’s description, and done/failed add Progress Bar tone icons with labels: never color alone.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.

Per item: queued, uploading, done, failed, cancelled. Queue: empty, busy.

From Trigger To
- add() queued (starts at once with autoStart when a slot is free)
queued free slot uploading; upload() called with attempt 1
uploading promise resolves done; response stored
uploading promise rejects, retries left uploading again after retryDelay x 2^n (progress resets)
uploading promise rejects, no retries left failed; announced
queued / uploading cancel() cancelled; signal aborted; announced
failed / cancelled retry() queued
any remove() removed; signal aborted; preview revoked
  • Progress is never announced continuously. Announced (polite): each failure (“a.pdf failed to upload.”), each cancellation, removals, and one summary per batch (“All 3 files uploaded.” or “Uploads finished: 2 uploaded, 1 failed, 0 cancelled.”).
  • Status is text; done and failed rows add the Progress Bar tone icon with a label, so status never depends on color.
  • Focus moves only after the user’s own action removed the focused control.
  • Manual screen-reader verification is pending.

Full width. Media, progress (label and description wrap), and 24 px actions stay on one line; reflows at 320 CSS px.

Logical properties: media at the inline start, actions at the inline end; progress fills from the inline start.

Rows on color-surface with color-border-strong (color-error-fg when failed); Progress Bar tones: success when done, error when failed. Forced-colors mode draws CanvasText borders.

configureAbsoluteUI({ components: { uploadQueue: { concurrency: 4, retries: 2, retryDelay: 2000 } } });

Changes apply to existing queues (a higher concurrency starts waiting items).

Instance options override global and theme defaults. All options are dynamic.

icons.file (default file), icons.cancel (close), icons.retry (refresh), icons.remove (close).

  • No network access by the library; your function controls the endpoint, credentials, and headers.
  • Raw errors are never displayed (they may contain server internals): the default is “The upload did not complete.”; use errorMessage to map known errors.
  • File names and messages are rendered with textContent.
  • Previews: object URLs in <img> (SVG in an image cannot run script). They need img-src blob: in your Content-Security-Policy; without it previews are blocked by the browser and the rest works. URLs are revoked on remove, clear, and destroy.

Files, progress, and responses stay in memory until removed or destroyed. Nothing is persisted, logged, or transmitted by AbsoluteUI.

Upload contract:

type UploadFunction = (file: File, context: {
signal: AbortSignal; // aborted on cancel, remove, destroy
onProgress: (loaded: number, total?: number) => void; // bytes; total defaults to file.size
attempt: number; // 1, 2, … across automatic and manual retries
id: string; // stable item id (upload session key)
}) => Promise<unknown>; // resolve = done (value in item.response); reject = failed
  • Chunked uploads: call onProgress(bytesSentSoFar) after each chunk; use id and attempt to resume on the server.
  • Cancellation is cooperative: pass signal to fetch or call xhr.abort() on abort.
  • A synchronous throw counts as a failure; a non-promise return value counts as success.

Status changes render the affected row at once; progress is coalesced to one render per animation frame. Budget: add() of 200 files < 100 ms (headless Chromium, e2e performance suite).

Tested with 200 items. Keep concurrency modest (browsers limit parallel connections per host).

  • onProgress values that are not finite are ignored; values above the total are clamped. A total of 0 shows an indeterminate bar while uploading.
  • A result that arrives after cancel, remove, retry, or destroy is ignored.
  • A cancelled item keeps its concurrency slot until your function settles.
  • Adding an empty iterable does nothing; add() after destroy() returns [].
Code Category Cause
UPLOAD_QUEUE_OPTIONS configuration Missing label or upload function, invalid errorMessage/preview, non-File values passed to add().
CFG_INVALID_VALUE / CFG_UNKNOWN_OPTION configuration Invalid concurrency, retries, retry delay, or unknown configuration.

Upload failures are item states (failed), not exceptions. A failed construction leaves the host untouched.

  • Cancellation depends on your function honouring signal.
  • Retries restart the attempt; resuming is your function’s job.
  • Individual successes are included in the batch summary rather than announced one by one.
  • Manual screen-reader verification is pending.
  • Built-in transports (fetch, XHR, tus, S3 multipart).
  • Reordering items; persistence across reloads.
  • HTML in file names or error messages.