How to drag and drop directories
The HTML Drag and Drop interfaces enable web applications to accept dragged and dropped files on a web page. During a drag and drop operation, dragged file and directory items are associated with file entries and directory entries respectively. When it comes to dragging and dropping files into the browser, there are two ways of doing it: the modern and the classic way.
The modern way #
Using the File System Access API's DataTransferItem.getAsFileSystemHandle()
method #
The DataTransferItem.getAsFileSystemHandle()
method returns a promise with a FileSystemFileHandle
object if the dragged item is a file, and a promise with a FileSystemDirectoryHandle
object if the dragged item is a directory. These handles let you read, and optionally write back to the file or directory. Note that the Drag and Drop interface's DataTransferItem.kind
will be "file"
for both files and directories, whereas the File System Access API's FileSystemHandle.kind
will be "file"
for files and "directory"
for directories.
- Chrome 86, Supported 86
- Firefox, Not supported
- Edge 86, Supported 86
- Safari, Not supported
The classic way #
Using the non-standard DataTransferItem.webkitGetAsEntry()
method #
The DataTransferItem.webkitGetAsEntry()
method returns the drag data item's FileSystemFileEntry
if the item is a file, and FileSystemDirectoryEntry
if the item is a directory. While you can read the file or directory, there is no way to write back to them. This method has the disadvantage that is not on the standards track, but has the advantage that it supports directories.
- Chrome 13, Supported 13
- Firefox 50, Supported 50
- Edge 14, Supported 14
- Safari 11.1, Supported 11.1
Progressive enhancement #
The snippet below uses the modern File System Access API's DataTransferItem.getAsFileSystemHandle()
method when it is supported, then falls back to the non-standard DataTransferItem.webkitGetAsEntry()
method, and finally falls back to the classic DataTransferItem.getAsFile()
method. Be sure to check the type of each handle
, since it could be either of:
FileSystemDirectoryHandle
when the modern code path is chosen.FileSystemDirectoryEntry
when the non-standard code path is chosen.
All types have a name
property, so logging it is fine and will always work.
// Run feature detection.
const supportsFileSystemAccessAPI =
'getAsFileSystemHandle' in DataTransferItem.prototype;
const supportsWebkitGetAsEntry =
'webkitGetAsEntry' in DataTransferItem.prototype;
// This is the drag and drop zone.
const elem = document.querySelector('main');
// Prevent navigation.
elem.addEventListener('dragover', (e) => {
e.preventDefault();
});
// Visually highlight the drop zone.
elem.addEventListener('dragenter', (e) => {
elem.style.outline = 'solid red 1px';
});
// Visually unhighlight the drop zone.
elem.addEventListener('dragleave', (e) => {
elem.style.outline = '';
});
// This is where the drop is handled.
elem.addEventListener('drop', async (e) => {
// Prevent navigation.
e.preventDefault();
if (!supportsFileSystemAccessAPI && !supportsWebkitGetAsEntry) {
// Cannot handle directories.
return;
}
// Unhighlight the drop zone.
elem.style.outline = '';
// Prepare an array of promises…
const fileHandlesPromises = [...e.dataTransfer.items]
// …by including only files (where file misleadingly means actual file _or_
// directory)…
.filter((item) => item.kind === 'file')
// …and, depending on previous feature detection…
.map((item) =>
supportsFileSystemAccessAPI
// …either get a modern `FileSystemHandle`…
? item.getAsFileSystemHandle()
// …or a classic `FileSystemFileEntry`.
: item.webkitGetAsEntry(),
);
// Loop over the array of promises.
for await (const handle of fileHandlesPromises) {
// This is where we can actually exclusively act on the directories.
if (handle.kind === 'directory' || handle.isDirectory) {
console.log(`Directory: ${handle.name}`);
}
}
});
Further reading #
Demo #
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>How to drag and drop directories</title>
</head>
<body>
<main>
<h1>How to drag and drop directories</h1>
<p>Drag and drop one or multiple files or directories onto the page.</p>
<pre></pre>
</main>
</body>
</html>
const supportsFileSystemAccessAPI =
"getAsFileSystemHandle" in DataTransferItem.prototype;
const supportsWebkitGetAsEntry =
"webkitGetAsEntry" in DataTransferItem.prototype;
const elem = document.querySelector("main");
const debug = document.querySelector("pre");
elem.addEventListener("dragover", (e) => {
// Prevent navigation.
e.preventDefault();
});
elem.addEventListener("dragenter", (e) => {
elem.style.outline = "solid red 1px";
});
elem.addEventListener("dragleave", (e) => {
elem.style.outline = "";
});
elem.addEventListener("drop", async (e) => {
e.preventDefault();
elem.style.outline = "";
const fileHandlesPromises = [...e.dataTransfer.items]
.filter((item) => item.kind === "file")
.map((item) =>
supportsFileSystemAccessAPI
? item.getAsFileSystemHandle()
: supportsWebkitGetAsEntry
? item.webkitGetAsEntry()
: item.getAsFile()
);
for await (const handle of fileHandlesPromises) {
if (handle.kind === "directory" || handle.isDirectory) {
console.log(`Directory: ${handle.name}`);
debug.textContent += `Directory: ${handle.name}\n`;
} else {
console.log(`File: ${handle.name}`);
debug.textContent += `File: ${handle.name}\n`;
}
}
});
:root {
color-scheme: dark light;
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
padding: 1rem;
font-family: system-ui, sans-serif;
line-height: 1.5;
min-height: 100vh;
display: flex;
flex-direction: column;
}
img,
video {
height: auto;
max-width: 100%;
}
main {
flex-grow: 1;
}
footer {
margin-top: 1rem;
border-top: solid CanvasText 1px;
font-size: 0.8rem;
}