A lightweight, signal-based Angular directive for drag-and-drop file and folder uploads — with directory traversal, accept-type filtering, and a click-to-open file picker. No stylesheets, no XHR wrappers, no DOM mutation. Just the File objects.
This is the workspace repository. The published package lives in
projects/angular-file-drop.
Dropzone.js is great, but it often fights against modern Angular architecture by injecting its own CSS, mutating the DOM, and hijacking HTTP requests with its own XHR wrappers.
angular-file-drop is the "Angular-only" alternative. It does a portion of what Dropzone does, but strictly the Angular way:
- Native directive — binds to your existing elements using standard Angular syntax.
- Headless — handles the messy HTML5 drag-and-drop events and hands you raw
Fileobjects. - Zero network opinions — you upload with Angular's own
HttpClient, keeping interceptors and auth intact. - Bring your own UI — no forced stylesheets. Style your dropzone exactly how your app needs it.
- Drag-and-drop and click-to-open file picker on a single element
- Recursive folder/directory traversal (modern File System Access API, with legacy
webkitGetAsEntryand plainFileListfallbacks) accept-style filtering by extension or MIME (.png,image/*,application/pdf,*/*)- Single- or multiple-file enforcement
- Automatic hidden-file filtering (
.git,.DS_Store, …) - Signal-based drag-over state for easy template styling
- SSR-safe (guards all browser-only APIs)
- A ready-made
FILE_TYPESmap of commonacceptstrings
npm install @h-k-dev/angular-file-dropRequires Angular 17.3+ (standalone directives + signals).
The directive is standalone — import AngularFileDrop directly into your component.
import { Component } from '@angular/core';
import { AngularFileDrop, FileDropEvent } from '@h-k-dev/angular-file-drop';
@Component({
selector: 'app-uploader',
imports: [AngularFileDrop],
template: `
<div
dropZone
[class.is-dragging]="zone.isDragOver()"
(fileDrop)="onDrop($event)"
#zone="dropZone"
>
Drag files here, or click to browse
</div>
`,
})
export class UploaderComponent {
onDrop(event: FileDropEvent) {
for (const { file, relativePath } of event.files) {
console.log(relativePath, file.size);
}
}
}.is-dragging {
outline: 2px dashed #4f46e5;
background: #eef2ff;
}Pass any accept-style string. The FILE_TYPES map provides ready-made values.
import { FILE_TYPES } from '@h-k-dev/angular-file-drop';
@Component({
imports: [AngularFileDrop],
template: `
<div dropZone [acceptedFiles]="accept" (fileDrop)="onDrop($event)">
Images and PDFs only
</div>
`,
})
export class Component {
accept = `${FILE_TYPES.ANY_IMAGE},${FILE_TYPES.PDF}`; // "image/*,.pdf,application/pdf"
}You can also write the string by hand: [acceptedFiles]="'.png,.jpg,image/*'".
<div dropZone [multiple]="false" (fileDrop)="onDrop($event)">
Drop a single file
</div>directory (on by default) recursively walks dropped folders. Each DroppedFile carries a relativePath that preserves the folder structure.
template: `<div dropZone [directory]="true" (fileDrop)="onDrop($event)">Drop a folder</div>`;
onDrop(event: FileDropEvent) {
// e.g. "photos/2026/holiday/IMG_001.jpg"
event.files.forEach((f) => console.log(f.relativePath));
}Set directoryPicker to make the click-to-open dialog a folder chooser instead of a file chooser.
<div dropZone [directoryPicker]="true">Click to choose a folder</div>The isDragOver signal flips while a valid file drag is over the element. Grab the directive instance via the dropZone export.
<div
dropZone
#zone="dropZone"
[class.active]="zone.isDragOver()"
(dragEnter)="onEnter($event)"
(dragLeave)="onLeave($event)"
>
{{ zone.isDragOver() ? 'Release to upload' : 'Drag files here' }}
</div>Disable the built-in click handling with isManualActivation and open the picker yourself from a child control.
@Component({
imports: [AngularFileDrop],
template: `
<div dropZone #zone="dropZone" [isManualActivation]="true" (fileDrop)="onDrop($event)">
<p>Drag files here</p>
<button type="button" (click)="zone.openFilePicker($event)">Browse files</button>
<button type="button" (click)="zone.openDirectoryPicker($event)">Browse folder</button>
</div>
`,
})
export class Component {
onDrop(event: FileDropEvent) {
/* ... */
}
}<div dropZone [disabled]="isUploading" (fileDrop)="onDrop($event)">…</div>The directive stays out of your network layer — wire it up however you like.
onDrop(event: FileDropEvent) {
const body = new FormData();
event.files.forEach((f) => body.append('files', f.file, f.relativePath));
this.http.post('/api/upload', body).subscribe();
}| Input | Type | Default | Description |
|---|---|---|---|
multiple |
boolean |
true |
Allow more than one file. When false, only the first file is emitted. |
directory |
boolean |
true |
Recursively traverse dropped folders. |
directoryPicker |
boolean |
false |
Make the click-to-open dialog a folder picker (webkitdirectory) rather than files. |
acceptedFiles |
string |
'' |
accept-style filter, e.g. .png,image/*,application/pdf. Empty accepts everything. |
ignoreHiddenFiles |
boolean |
true |
Drop dotfiles and files inside dot-folders (.git, .DS_Store, …). |
clickable |
boolean |
true |
Open the file picker when the host element is clicked or activated via keyboard. |
disabled |
boolean |
false |
Ignore all drops, clicks, and keyboard activation. |
isManualActivation |
boolean |
false |
Disable built-in click/keyboard activation so you can call the open* methods yourself. |
| Output | Payload | Description |
|---|---|---|
fileDrop |
FileDropEvent |
Emitted after files are dropped or chosen and filtered. |
dragEnter |
DragEvent |
A valid file drag entered the element. |
dragOver |
DragEvent |
A valid file drag is moving over the element. |
dragLeave |
DragEvent |
A valid file drag left the element. |
| Member | Description |
|---|---|
isDragOver: Signal<boolean> |
true while a valid file drag is over the element. |
openPicker(event?, options?) |
Open the hidden file input. options.directory toggles folder mode. |
openFilePicker(event?) |
Open a file picker. |
openDirectoryPicker(event?) |
Open a folder picker. |
Access these in templates via the dropZone export: #zone="dropZone".
interface DroppedFile {
file: File;
relativePath: string; // preserves folder structure, e.g. "docs/report.pdf"
}
interface FileDropEvent {
files: DroppedFile[];
}The directive's pure helpers are exported for advanced use and testing: containsFiles, setDropEffect, isHiddenPath, filterHiddenFiles, isFileAccepted, filterAcceptedFiles, enforceMultiple, toDroppedFiles, readDroppedFiles, walkHandles, walkEntries, createHiddenFileInput, and the FILE_TYPES map.
npm install
npm test # run the vitest unit suite
npm run test:ci # single run (CI)
npm run build # build the library with ng-packagrWorks in all modern browsers. Folder traversal uses the File System Access API where available and falls back to webkitGetAsEntry and then a plain FileList. Server-side rendering is safe — all browser-only APIs are guarded.
MIT © h-k-dev