Primitives
File Upload
A headless drag-and-drop / dialog file-selection zone: a visually-hidden native <input type="file"> stays the accessible control while a trigger button opens the picker, and dropping files emits the same change. Supports multiple, accept filters and whole-folder (directory) selection.
Click the zone or drop a file on it. The zone sets data-dragging while a file hovers it, and the chosen files arrive as a FileList you render yourself.
or drag and drop
Any file type
No ARIA role is imposed on the drop zone, which is a plain container. The <input type="file"> remains the accessible form control; the trigger is a native <button>.
Anatomy
<div forFileUpload accept="image/*,.pdf" (filesChange)="onFiles($event)">
<input forFileUploadInput aria-label="Upload files" class="sr-only" />
<button forFileUploadTrigger>Choose files</button>
<p>or drag and drop</p>
</div>
Examples
Multiple files
multiple lets the picker (and a drop) accept more than one file at once, and accept narrows the chooser to the MIME types you list.
or drag and drop
Images only — select as many as you like
States
One class and one directive, two states. disabled blocks the dialog and drops alike. It also reflects data-disabled on the zone, so the zone dims and ignores pointer events from the same stylesheet that styles data-dragging, without the input leaving the DOM.
or drag and drop
Any file type
or drag and drop
Uploads are paused
Folder selection
Set directory to switch the native picker into folder mode. The input mirrors it as webkitdirectory, which modern Chromium, Firefox and WebKit all support despite the prefix. Choose a folder and the emitted FileList holds every file inside it, each carrying a webkitRelativePath to rebuild the tree from. Dropping a folder is out of scope: a drop surfaces DataTransfer.files only, with no webkitGetAsEntry traversal.
The whole folder’s contents are read, recursively.
Handling rejections
Every file the zone refuses arrives on filesRejected with the reason it was refused, and never reaches filesChange or the native input's files. A file outside accept is rejected with 'accept', and a file larger than maxSize bytes with 'size'; a file of exactly maxSize bytes is accepted. With multiple off, the zone keeps the first accepted file and surfaces the extras with the reason 'multiple'. That cap is applied last, so a refused file never takes the one slot from a valid file behind it. Nothing is discarded silently, so you can tell the user why a file did not go through. Combining directory with multiple off is noisy by design: every file in the chosen folder past the first is reported as a 'multiple' rejection.
<div forFileUpload accept="image/*" [maxSize]="5000000" (filesRejected)="onRejected($event)">
<input forFileUploadInput aria-label="Upload an image" class="sr-only" />
<button forFileUploadTrigger>Choose an image</button>
</div>
onRejected(rejections: ForFileUploadRejection[]): void {
for (const { file, reason } of rejections) {
console.warn(`${file.name} rejected: ${reason}`);
}
}
Disabled
<div forFileUpload [disabled]="isDisabled()">
<input forFileUploadInput aria-label="Upload files" class="sr-only" />
<button forFileUploadTrigger>Choose files</button>
</div>
API
ForFileUpload
| Property | Type | Description |
|---|---|---|
accept | string | null | MIME types or file extensions accepted by the chooser (e.g. "image/*,.pdf").Default: null |
multiple | boolean | Whether multiple files can be selected at once. Default: false |
directory | boolean | Whether the picker selects a whole folder (mirrored as webkitdirectory).Default: false |
maxSize | number | null | Largest accepted file size, in bytes; a file of exactly this size is accepted. null sets no limit.Default: null |
disabled | boolean | Whether the zone and all its pieces are disabled. Default: false |
| Output | Type | Description |
|---|---|---|
filesChange | FileList | Files chosen via the dialog or dropped onto the zone, filtered against accept and maxSize before emission through either path. |
filesRejected | ForFileUploadRejection[] | Files refused by accept, by maxSize or by the single-file cap of multiple="false", each paired with the reason ('accept' / 'size' / 'multiple'). Fires only when at least one file was refused; every selected file lands in exactly one output. |
| Data attribute | Values | |
|---|---|---|
data-dragging | present while files are dragged over the drop zone, else absent | |
data-disabled | present when the zone (and all its pieces) is disabled, else absent |
Accessibility
- The
<input type="file">is the accessible control. Keep it reachable with a visually-hidden utility class (sr-only/visually-hidden) rather thandisplay: noneorvisibility: hidden, which would remove it from the tab order and from assistive technology. - Label the input. Supply
aria-labeldirectly on[forFileUploadInput](as in the examples above), or wrap it in a<label>. - The trigger is a native
<button>. It receives focus, is announced as a button, and activates the file dialog via click / Enter / Space. No ARIA role augmentation is needed.
Styling
forty-cdk ships no styles: put your own class on each piece and key your CSS off the data-* attributes listed under API, not off the for* selectors (Styling forty-cdk explains why).
Wrapping in a design system
Subclass the root and re-provide FOR_FILE_UPLOAD_CONTEXT with useExisting pointing at the subclass, since Angular does not inherit a directive's providers; Wrapping non-form roots walks the pattern.