Skip to content

@absoluteui/services/download

Downloads of Blobs, text, JSON, CSV, and URLs with progress, save pickers, and safe file names.

import { ... } from '@absoluteui/services/download';

Saves files to the user’s device: Blobs, text, JSON, CSV, data: URLs, and remote files, with safe file names, automatic object-URL cleanup, a save picker for large files, and status text for live regions. Every function returns a DownloadStatus instead of throwing for user-level outcomes. The Files and downloads guide covers CSV exports, remote downloads with progress, and the safety rules; the recipes below cover the remaining tasks.

How do I export data and tell screen-reader users it worked?

Section titled “How do I export data and tell screen-reader users it worked?”

Every save returns a status; describeDownloadStatus turns it into a sentence for a polite live region. Override labels to translate.

import { describeDownloadStatus, downloadJson } from '@absoluteui/services/download';
const region = document.createElement('div');
region.setAttribute('aria-live', 'polite');
const status = downloadJson({ period: '2026-09', headcount: 214 }, 'headcount-2026-09.json', { pretty: true });
region.textContent = describeDownloadStatus(status); // e.g. 'Download started: headcount-2026-09.json'
import { describeDownloadStatus } from '@absoluteui/services/download';
describeDownloadStatus({ ok: false, method: 'none', fileName: 'payslip.pdf', reason: 'error' }); // => 'Download failed: payslip.pdf. Try again.'
describeDownloadStatus({ ok: true, method: 'anchor', fileName: 'payslip.pdf' }, { labels: { started: 'Téléchargement lancé : {fileName}' } }); // => 'Téléchargement lancé : payslip.pdf'

How do I save a large export straight to disk?

Section titled “How do I save a large export straight to disk?”

saveWithPicker streams into the file the user picks (File System Access API) and falls back to a normal download elsewhere. Pass a function as the source so the export starts only after the user has chosen a file; call it from a click handler.

import { describeDownloadStatus, saveWithPicker } from '@absoluteui/services/download';
const button = document.createElement('button');
button.textContent = 'Export all employees';
button.addEventListener('click', async () => {
const status = await saveWithPicker(async () => (await fetch('/api/exports/employees.csv')).blob(), {
suggestedName: 'employees-2026-10-05.csv',
types: [{ description: 'CSV file', accept: { 'text/csv': ['.csv'] } }],
});
if (!status.cancelled) console.info(describeDownloadStatus(status));
});

Browsers block or prompt for bursts of downloads. createDownloadQueue spaces them out (one at a time, 400 ms apart by default); for many files, offer one ZIP from @absoluteui/services/zip instead.

import { createDownloadQueue, downloadUrl } from '@absoluteui/services/download';
const months = ['2026-07', '2026-08', '2026-09'];
const queue = createDownloadQueue({ concurrency: 1, delayMs: 500 });
const statuses = await Promise.all(months.map((m) => queue.add(() => downloadUrl(`/api/payslips/${m}.pdf`), `payslip-${m}.pdf`)));
const failed = statuses.filter((s) => !s.ok).map((s) => s.fileName);
  • Names from servers and users are cleaned by sanitizeDownloadName: path parts, control characters, and bidirectional overrides that disguise extensions are removed, and reserved Windows names are prefixed.
  • downloadUrl refuses non-HTTP URLs (SVC_DOWNLOAD_UNSAFE_URL) and other origins unless you pass allowCrossOrigin: true (SVC_DOWNLOAD_CROSS_ORIGIN). Network and HTTP failures resolve to { ok: false, reason: 'error' } rather than rejecting.
import { downloadUrl, sanitizeDownloadName } from '@absoluteui/services/download';
sanitizeDownloadName('../../payroll/September.csv'); // => 'September.csv'
sanitizeDownloadName('CON.txt'); // => '_CON.txt' // reserved device names are changed
await downloadUrl('javascript:alert(1)'); // => throws SVC_DOWNLOAD_UNSAFE_URL
await downloadUrl('https://files.example.net/report.pdf'); // => throws SVC_DOWNLOAD_CROSS_ORIGIN

createDownloadQueue

Runs downloads with limited concurrency and a delay between starts.

function createDownloadQueue(options?: DownloadQueueOptions): DownloadQueue;

Example:

const queue = createDownloadQueue();
const files = [{ url: '/api/payslips/2026-08.pdf' }, { url: '/api/payslips/2026-09.pdf' }];
for (const f of files) void queue.add(() => downloadUrl(f.url));
await queue.onIdle();

createObjectUrl

Creates a tracked object URL; revokeAll() releases every URL still alive.

function createObjectUrl(blob: Blob): ObjectUrlHandle;

Example:

const blob = new Blob(['<svg xmlns="http://www.w3.org/2000/svg"/>'], { type: 'image/svg+xml' });
const { url, revoke } = createObjectUrl(blob);
const img = document.createElement('img');
img.onload = revoke;
img.src = url;

describeDownloadStatus

Text for a live region describing a download status.

function describeDownloadStatus(value: DownloadStatus, options?: {
readonly labels?: Partial<DownloadStatusLabels>;
}): string;

Example:

describeDownloadStatus({ ok: true, method: 'anchor', fileName: 'report.csv' }); // => 'Download started: report.csv'
describeDownloadStatus({ ok: true, method: 'picker', fileName: 'report.csv' }); // => 'Saved: report.csv'
describeDownloadStatus({ ok: false, method: 'picker', fileName: 'report.csv', cancelled: true }); // => 'Download cancelled.'

downloadBlob

Saves a Blob through a detached &lt;a download> click. The object URL is revoked right after the click; the anchor is never attached to the document and focus does not move.

function downloadBlob(blob: Blob, fileName: string, options?: DownloadBlobOptions): DownloadStatus;

Example:

declare const blob: Blob; // for example from a report endpoint
const status = downloadBlob(blob, 'report.pdf');
console.info(describeDownloadStatus(status)); // e.g. 'Download started: report.pdf'

downloadCsv

Saves rows as CSV. Formula-like cells are always neutralized (CSV injection protection).

function downloadCsv<T extends object>(rows: readonly T[] | readonly (readonly unknown[])[], fileName: string, options?: DownloadCsvOptions<T>): DownloadStatus;

Example:

const employees = [{ name: 'Ada Lovelace', email: 'ada@example.com' }];
downloadCsv(employees, 'employees.csv', { columns: [{ key: 'name', header: 'Name' }, 'email'] });

downloadDataUrl

Saves a data: URL (decoded to a Blob first; top-level data: navigation is blocked by browsers).

function downloadDataUrl(dataUrl: string, fileName: string, options?: DownloadDataUrlOptions): DownloadStatus;

Example:

const canvas = document.createElement('canvas');
downloadDataUrl(canvas.toDataURL('image/png'), 'chart.png');

downloadJson

Saves a value as JSON using safeJsonStringify (cycles, BigInt, Map and Set are handled). Throws SVC_DOWNLOAD_NOT_SERIALIZABLE for values JSON cannot represent.

function downloadJson(value: unknown, fileName: string, options?: DownloadJsonOptions): DownloadStatus;

Example:

downloadJson({ theme: 'dark', density: 'compact' }, 'settings.json', { pretty: true });

downloadText

Saves text as a file.

function downloadText(text: string, fileName: string, options?: DownloadTextOptions): DownloadStatus;

Example:

downloadText('2026-09-30 09:00 Sync started', 'log.txt');

downloadUrl

Fetches a URL with progress and abort, then saves it. Same-origin only unless allowCrossOrigin; only http(s). The file name comes from fileName, then a sanitized Content-Disposition name, then the URL, then download. Invalid or disallowed URLs throw; network, HTTP and abort outcomes resolve to a failed status carrying error.

function downloadUrl(url: string, options?: DownloadUrlOptions): Promise<DownloadStatus>;

Example:

const bar = document.createElement('progress');
bar.max = 100;
const controller = new AbortController();
const status = await downloadUrl('/api/reports/42/pdf', { onProgress: (p) => { bar.value = p.percent ?? 0; }, signal: controller.signal });

revokeAll

Revokes every object URL created by createObjectUrl that is still alive (for example on route change).

function revokeAll(): void;

Example:

window.addEventListener('pagehide', revokeAll);

sanitizeDownloadName

Makes a name safe to save: strips bidirectional overrides (extension spoofing), path separators, control characters and reserved device names, and limits the length while preserving the extension.

function sanitizeDownloadName(name: string, options?: SanitizeDownloadNameOptions): string;

Example:

sanitizeDownloadName('invoice\u202Efdp.exe'); // => 'invoicefdp.exe'
sanitizeDownloadName('../../report.csv'); // => 'report.csv'

saveWithPicker

Saves through showSaveFilePicker, streaming into the file (no full copy in memory), with an anchor fallback. Dismissing the picker resolves to { ok: false, cancelled: true }. Call from a user gesture; pass a function as source to start fetching only after the user chose a file.

function saveWithPicker(source: SaveSource, options: SaveWithPickerOptions): Promise<DownloadStatus>;

Example:

const status = await saveWithPicker(async () => (await fetch('/export')).body!, { suggestedName: 'export.csv' });

DEFAULT_DOWNLOAD_LABELS

const DEFAULT_DOWNLOAD_LABELS: DownloadStatusLabels;

DownloadBlobOptions

interface DownloadBlobOptions {
readonly signal?: AbortSignal;
readonly revokeDelayMs?: number;
}

DownloadCsvOptions

interface DownloadCsvOptions<T> extends DownloadBlobOptions {
readonly columns?: readonly (string | CsvColumn<T>)[];
readonly bom?: boolean;
readonly delimiter?: string;
readonly eol?: '\r\n' | '\n';
readonly header?: boolean;
}

DownloadDataUrlOptions

interface DownloadDataUrlOptions extends DownloadBlobOptions {
readonly maxBytes?: number;
}

DownloadJsonOptions

interface DownloadJsonOptions extends DownloadBlobOptions {
readonly pretty?: boolean;
}

DownloadProgress

Download progress. total and percent are undefined without Content-Length.

interface DownloadProgress {
readonly loaded: number;
readonly total: number | undefined;
readonly percent: number | undefined;
}

DownloadQueue

interface DownloadQueue {
add(task: DownloadTask, fileName?: string): Promise<DownloadStatus>;
onIdle(): Promise<void>;
clear(): void;
readonly pending: number;
readonly running: number;
}

DownloadQueueOptions

interface DownloadQueueOptions {
readonly concurrency?: number;
readonly delayMs?: number;
}

DownloadStatus

Outcome of a download, for announcements (describeDownloadStatus).

interface DownloadStatus {
readonly ok: boolean;
readonly method: DownloadMethod;
readonly fileName: string;
readonly cancelled?: boolean;
readonly reason?: DownloadFailureReason;
readonly error?: Error;
readonly bytes?: number;
}

DownloadStatusLabels

Live-region messages; {fileName} is replaced. Override for localization.

interface DownloadStatusLabels {
readonly started: string;
readonly saved: string;
readonly cancelled: string;
readonly aborted: string;
readonly unsupported: string;
readonly failed: string;
}

DownloadTextOptions

interface DownloadTextOptions extends DownloadBlobOptions {
readonly type?: string;
readonly bom?: boolean;
}

DownloadUrlOptions

interface DownloadUrlOptions extends DownloadBlobOptions {
readonly fileName?: string;
readonly onProgress?: (progress: DownloadProgress) => void;
readonly fetchInit?: Omit<RequestInit, 'signal'>;
readonly allowCrossOrigin?: boolean;
readonly maxBytes?: number;
readonly fetch?: typeof fetch;
}

ObjectUrlHandle

An object URL with an idempotent revoke.

interface ObjectUrlHandle {
readonly url: string;
revoke(): void;
}

SanitizeDownloadNameOptions

interface SanitizeDownloadNameOptions {
readonly maxLength?: number;
readonly fallback?: string;
}

SavePickerType

showSaveFilePicker accept types, as in the File System Access API.

interface SavePickerType {
readonly description?: string;
readonly accept: Readonly<Record<string, readonly string[]>>;
}

SaveWithPickerOptions

interface SaveWithPickerOptions {
readonly suggestedName: string;
readonly types?: readonly SavePickerType[];
readonly signal?: AbortSignal;
readonly fallback?: boolean;
}

DownloadFailureReason

Why a download did not complete.

type DownloadFailureReason = 'cancelled' | 'aborted' | 'unsupported' | 'error';

DownloadMethod

How the file was saved.

type DownloadMethod = 'anchor' | 'picker' | 'none';

DownloadTask

A download task: any function returning a status (for example () => downloadUrl(url)).

type DownloadTask = () => DownloadStatus | Promise<DownloadStatus>;

SaveSource

Data to save: a Blob, a stream, or a function producing one (called after the picker closes).

type SaveSource = Blob | ReadableStream<Uint8Array<ArrayBuffer>> | (() => Promise<Blob | ReadableStream<Uint8Array<ArrayBuffer>>>);