Skip to content

File Upload

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

Using Angular? See File Upload for Angular.

A file picker for forms. It provides:

  • a native <input type="file"> opened by a real <button> (keyboard and screen-reader path);
  • an optional drag-and-drop zone around the button (an enhancement, never the only way in);
  • validation of MIME type and extension (both, when both are configured), per-file size, total size, and file count. The accept attribute is only a hint: every file is re-checked;
  • rejected files are not added; the reason is shown next to the field and announced;
  • a list of selected files with remove buttons;
  • the FormControl contract: Form validates, serializes (File[]), resets, and focuses it.

It never reads file contents and never performs network requests. To transfer files with progress, feed an Upload Queue.

  • Attaching one or more files to a form.
  • Feeding an Upload Queue that transfers files as soon as they are chosen.
  • Any file selection that needs consistent, localized validation messages.
  • Choosing a folder tree or very large batches (thousands of files): use a dedicated tool.
  • Capturing photos or media in-page: use native capture APIs.
  • As a security control: client-side type checks can be bypassed; validate on the server.
import { FileUpload } from '@absoluteui/components/file-upload';
const host = document.querySelector<HTMLElement>('#attachments');
if (host) {
const upload = FileUpload.create(host, {
label: 'Attachments',
name: 'attachments',
multiple: true,
acceptTypes: ['application/pdf', 'image/*'],
acceptExtensions: ['.pdf', '.png', '.jpg'],
maxFileSize: 5_000_000,
maxFiles: 3,
});
upload.host.addEventListener('aui-change', () => {
const data = new FormData();
for (const file of upload.getFormValue()) data.append('attachments', file);
});
}
import { FileUpload } from '@absoluteui/components/file-upload';
const host = document.querySelector<HTMLElement>('#avatar');
if (host) {
FileUpload.create(host, { label: 'Profile photo', required: true, acceptTypes: ['image/png', 'image/jpeg'], maxFileSize: 2_000_000, dropZone: false });
}
import { FileUpload, type FileUploadRejectDetail } from '@absoluteui/components/file-upload';
import '@absoluteui/components/file-upload/style.css';
const host = document.querySelector<HTMLElement>('#photos');
if (host) {
const upload = FileUpload.create(host, { label: 'Photos', multiple: true, acceptTypes: ['image/*'], maxTotalSize: 20_000_000 });
upload.host.addEventListener('aui-reject', (e) => {
console.info(`${String((e as CustomEvent<FileUploadRejectDetail>).detail.rejected.length)} files rejected`);
});
}
import { FileUpload } from '@absoluteui/components/file-upload';
const host = document.createElement('div');
document.body.append(host);
FileUpload.create(host, { label: 'Attachments', name: 'attachments', multiple: true, acceptExtensions: ['.pdf'], maxFileSize: 5_000_000 });
<div class="aui-field aui-file-upload [--dragover] [--invalid] [--disabled] [--no-zone]">
<label class="aui-field__label" for="B" id="L">Attachments<span aria-hidden="true">*</span></label>
<div class="aui-field__description" id="D">help text</div>
<div class="aui-file-upload__zone"> (drop target)
<span class="aui-file-upload__zone-icon"><svg aria-hidden="true"/></span>
<span class="aui-file-upload__prompt">Drag files here or</span>
<button type="button" id="B" class="aui-file-upload__button" aria-labelledby="L T" aria-describedby="D H R E X">
<span id="T">Choose files</span>
</button>
<div class="aui-file-upload__hint" id="H">Allowed file types: .pdf. Maximum 5 MB per file.</div>
<span class="aui-visually-hidden" id="R">Required</span>
<input type="file" hidden tabindex="-1" name="attachments" multiple accept=".pdf">
</div>
<div class="aui-field__error" id="E">...</div>
<div class="aui-file-upload__rejections" id="X"><ul><li>report.exe: This file type is not allowed.</li></ul></div>
<ul class="aui-file-upload__list" aria-label="Selected files">
<li class="aui-file-upload__item [--invalid]">
<svg aria-hidden="true"/>
<span class="aui-file-upload__file"><span class="__name">a.pdf</span><span class="__size">1.2 MB</span><span class="__issue"></span></span>
<button class="aui-file-upload__remove" aria-label="Remove a.pdf"><svg aria-hidden="true"/></button>
</li>
</ul>
</div>
FileUpload.create(host: HTMLElement, options: FileUploadOptions): FileUpload

Global configuration key: fileUpload (see Configuration).

Option Type Default Description
label string Visible label and accessible name (part of the button name). Required.
hideLabel boolean false Visually hide the label (it stays in the accessible name).
description string Help text, referenced by aria-describedby.
name string Form field name; also set on the native input for native submission.
multiple boolean false Allow several files; a new selection appends. Single mode replaces the file.
value readonly File[] "[]" Initial files; reset() restores them.
required boolean false At least one file is required.
disabled boolean false Disable the button, drop zone, and remove buttons.
acceptTypes readonly string[] Accepted MIME types (“image/png”, “image/*”). Every file is checked.
acceptExtensions readonly string[] Accepted extensions including the dot (“.pdf”). When both lists are set, a file must match both.
maxFileSize number Maximum size of each file, in bytes.
maxTotalSize number Maximum combined size, in bytes.
maxFiles number Maximum number of files (requires multiple).
buttonText string Button text. Default: localized “Choose file” / “Choose files”.
validate (files: readonly File[]) => string | null Consumer validation; return a message to mark the value invalid.
icons Partial<{ upload: IconRef; file: IconRef; remove: IconRef; error: IconRef }> Icon slot overrides.
validationTiming 'blur' | 'input' | 'submit' "blur" When validation messages first appear; afterwards they update live. Globally configurable.
dropZone boolean true Render the drag-and-drop zone around the button. Globally configurable.
showConstraints boolean true Render a hint listing the type, size, and count limits. Globally configurable.
locale string "en-US" Message and size-formatting locale. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement Host element.
input HTMLInputElement The native file input (hidden; the button opens it).
button HTMLButtonElement The chooser button.
value readonly File[] Selected files in selection order.
validity FileUploadValidity Current validity.
rejections readonly FileRejection[] Files not added by the last selection, with reasons.
name string | null Form field name.
label string Accessible label (used by the error summary).
disabled boolean Whether the control is disabled.
dirty boolean Value differs from the initial value.
touched boolean Focus has left the control at least once.
options Readonly<FileUploadOptions> Current options.
Method Description
getFormValue(): File[] A copy of the selected files.
setValue(files: readonly File[]): void Replace the selection (no events, no filtering; violations make the value invalid).
removeFile(file: File): void Remove one file (no events).
openPicker(): void Open the native chooser (needs user activation).
update(options: Partial<FileUploadOptions>): void Merge and apply options.
reportValidity(): boolean Show messages now (marks the control submitted) and return validity.
setCustomError(message: string | null): void External error (server, cross-field rule); null clears it.
focus(): void Move focus to the primary focusable element.
reset(): void Restore the initial value and clear dirty/touched/submitted state.
destroy(): void Remove rendering, registration, listeners, and the configuration subscription.
Event Detail Bubbles Cancelable Description
aui-change { files: readonly File[]; added: readonly File[]; removed: readonly File[]; validity: FileUploadValidity } Yes No The user added (chooser or drop) or removed files.
aui-reject { rejected: readonly { file: File; reason: FileRejectionReason; message: string }[] } Yes No A selection contained files that were not added.

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

Class Purpose
aui-file-upload Host (also aui-field).
aui-file-upload--dragover | --invalid | --disabled | --no-zone State modifiers.
aui-file-upload__zone Drop zone around the button.
aui-file-upload__zone-icon Upload icon.
aui-file-upload__prompt “Drag files here or” text.
aui-file-upload__button Chooser button.
aui-file-upload__hint Constraint hint.
aui-file-upload__rejections Rejected files and reasons.
aui-file-upload__list Selected files.
aui-file-upload__item One file row (–invalid when it breaks a rule).
aui-file-upload__name | __size | __issue Row text parts.
aui-file-upload__remove Remove button.
Slot Default icon
upload upload
file file
remove close
error alert-circle

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

--aui-border-width | --aui-border-width-strong | --aui-color-border | --aui-color-border-strong | --aui-color-disabled-bg | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-hover | --aui-color-selected | --aui-color-selected-text | --aui-color-surface | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-md | --aui-control-padding-x | --aui-duration-fast | --aui-easing-standard | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-sans | --aui-font-size-md | --aui-font-size-sm | --aui-font-weight-medium | --aui-icon-size-lg | --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-space-4 | --aui-touch-target-min

type FileUploadErrorCode = 'value-missing' | 'file-too-large' | 'file-type' | 'too-many-files' | 'total-too-large' | 'custom';
type FileRejectionReason = 'file-too-large' | 'file-type' | 'too-many-files' | 'total-too-large' | 'folder';
interface FileUploadValidity { readonly valid: boolean; readonly error: FileUploadErrorCode | null; readonly message: string }
interface FileRejection { readonly file: File; readonly reason: FileRejectionReason; readonly message: string }
interface FileUploadConfig { validationTiming: ValidationTiming; dropZone: boolean; showConstraints: boolean }
interface FileUploadOptions extends Partial<FileUploadConfig> {
label: string; hideLabel?: boolean; description?: string; name?: string; multiple?: boolean;
value?: readonly File[]; required?: boolean; disabled?: boolean;
acceptTypes?: readonly string[]; acceptExtensions?: readonly string[];
maxFileSize?: number; maxTotalSize?: number; maxFiles?: number; buttonText?: string;
validate?: (files: readonly File[]) => string | null; locale?: string; icons?: Partial<FileUploadIcons>;
}
interface FileUploadChangeDetail { files: readonly File[]; added: readonly File[]; removed: readonly File[]; validity: FileUploadValidity }
interface FileUploadRejectDetail { rejected: readonly FileRejection[] }

Pattern: Native <input type=“file”> opened by a native <button>; the drop zone is an enhancement (WCAG 2.5.7: dragging is never required).

Key Context Action
Tab / Shift+Tab Any Moves between the chooser button and each remove button.
Enter / Space Chooser focused Opens the native file chooser.
Enter / Space Remove button focused Removes the file; focus moves to the next remove button, else the previous one, else the chooser.
Element Role
chooser button (native)
native input hidden; not in the tab order
selected files list (native <ul>, aria-label “Selected files”)
remove buttons button (native), named “Remove {file}”
icons none (aria-hidden=true)
Attribute Element Value
aria-labelledby chooser Label id and button-text id (“Attachments Choose files”)
aria-describedby chooser Help text, constraint hint, “Required”, visible error, and rejection ids
aria-label remove button Remove {file name}

The chooser button and remove buttons show a :focus-visible outline in color-focus-ring. After a drop, focus moves to the chooser button so the result is reachable.

Criterion Level Note
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.7 Focus Visible AA
2.5.7 Dragging Movements AA
2.5.8 Target Size (Minimum) AA
3.3.1 Error Identification A
3.3.2 Labels or Instructions A
4.1.2 Name, Role, Value A
4.1.3 Status Messages AA

Automated test results: Accessibility and compliance.

  • A button cannot carry aria-required, so “Required” is a visually hidden description.
  • Rejections are announced politely: “{file} was not added. {reason}” or “{count} files were not added.”
  • Removal is announced: “{file} removed”.
  • Errors are text with an icon, never color alone.
  • Manual screen-reader verification (NVDA, JAWS, VoiceOver) is pending.
  • Native drag and drop has no keyboard equivalent by design; the chooser button is the keyboard and screen-reader path.

empty, files selected, dragging over, files rejected, required, invalid, disabled.

From Trigger To
empty / selected chooser or drop with valid files selected (appended in multiple mode, replaced in single mode); aui-change
any selection with rule-breaking files those files are not added; rejections shown and announced; aui-reject
selected remove button file removed, focus to next remove button / previous / chooser; announced; aui-change
idle files dragged over the zone --dragover until they leave or drop
any update() with stricter rules existing files re-validated; value may become invalid (per-row reasons shown)
  • Dragging is never required (WCAG 2.5.7): the button is the primary path; after a drop, focus moves to it.
  • Rejections are announced politely (“report.exe was not added. This file type is not allowed.” or “3 files were not added.”), removals too (“a.pdf removed”).
  • Errors use text and an icon, never color alone. Remove buttons have a 24 px target (2.5.8).
  • Manual screen-reader verification is pending.

Full width of its container. The drop zone content wraps and centers; file rows wrap size and reason under the name; it reflows at 320 CSS px.

Logical properties throughout; icons and remove buttons follow the inline direction. File names render as text in their own direction.

Dashed color-border-strong zone on color-surface; drag-over switches to color-selected with color-selected-text; errors use color-error-fg. Forced-colors mode draws CanvasText borders and a Highlight drag-over border.

configureAbsoluteUI({ components: { fileUpload: { dropZone: false, showConstraints: true, validationTiming: 'submit' } } });

Changes apply to existing instances immediately.

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

icons.upload (default upload), icons.file (file), icons.remove (close), icons.error (alert-circle).

  • File names, reasons, and messages are rendered with textContent (never HTML).
  • The browser’s MIME type is usually derived from the file name and is not proof of content. Configure both acceptTypes and acceptExtensions for defense in depth, and always validate type (for example by magic bytes) and size on the server.
  • The accept attribute only filters the OS dialog; drops and “All files” choices are re-checked.
  • No file contents are read. CSP-safe (no inline styles or HTML sinks).

Files stay in memory as File objects and in the native input. Nothing is persisted, logged, or transmitted.

Serialization:

  • getFormValue() and Form.getValues() return File[] under the field name (always an array, also in single mode; [] when empty). File objects are not JSON-serializable: append them to a FormData to send them:
const data = new FormData();
for (const file of values.attachments as File[]) data.append('attachments', file);
  • The native input is kept in sync (through DataTransfer), so new FormData(formElement) and native form submission (submitMode: 'native', enctype="multipart/form-data") include exactly the selected files, including dropped ones.

File rows are rebuilt only when the selection changes; validation passes update text only. Budgets: FileUpload.create < 1 ms per instance; selecting 200 files (validate and render) < 100 ms (headless Chromium, e2e performance suite).

Tested with 200 files in one selection. For thousands of files, use a dedicated tool.

  • The same file (name, size, modification time) selected twice is ignored.
  • Single mode: a new file replaces the current one; if it is rejected, the current file is kept. Extra dropped files are rejected (“You can select only 1 file.”).
  • Count and total limits reject files in selection order; later smaller files can still fit.
  • Dropped folders are rejected (“Folders cannot be added.”).
  • A cancelled OS dialog changes nothing.
  • Sizes use decimal units: maxFileSize: 5_000_000 reads “5 MB”.
Code Category Cause
FILE_UPLOAD_OPTIONS configuration Missing label, malformed acceptTypes/acceptExtensions, non-positive sizes, maxFiles without multiple, non-File value.
CFG_INVALID_VALUE / CFG_UNKNOWN_OPTION configuration Invalid or unknown configuration.

Validity error codes: value-missing, file-type, file-too-large, too-many-files, total-too-large, custom. A failed construction leaves the host untouched.

  • Decimal size units only.
  • The drag-over highlight depends on the browser exposing Files during dragenter.
  • Browsers without a DataTransfer constructor cannot sync the native input; getFormValue() stays authoritative.
  • Manual screen-reader verification is pending.
  • Reading, previewing, or transforming file contents (Upload Queue offers image previews).
  • Folder upload (webkitdirectory), paste-to-upload.
  • HTML in labels, hints, or file names.