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.
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.
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
HTML
<!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>
CSS
: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;
}
JS
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`;
}
}
});