How to open one or multiple files
Dealing with files is one of the most common operations for apps on the web. Traditionally, users needed to upload a file, make some changes to it, and then download it again, resulting in a copy in the Downloads folder. With the File System Access API, users can now open files directly, make modifications, and save back the changes to the original file.
The modern way #
Using the File System Access API's showOpenFilePicker()
method #
To open a file, call showOpenFilePicker()
, which returns a promise with an array of the picked file or files. If you need multiple files, you can pass { multiple: true, }
to the method.
- Chrome 86, Supported 86
- Firefox, Not supported
- Edge 86, Supported 86
- Safari, Not supported
The classic way #
Using the <input type="file">
element #
The <input type="file">
element on a page allows the user to click it and open one or multiple files. The trick now consists of inserting the element invisibly into a page with JavaScript and clicking it programmatically.
- Chrome 1, Supported 1
- Firefox 1, Supported 1
- Edge 12, Supported 12
- Safari 1, Supported 1
Progressive enhancement #
The method below uses the File System Access API when it's supported and else falls back to the classic approach. In both cases the function returns an array of files, but in case of where the File System Access API is supported, each file object also has a FileSystemFileHandle
stored in the handle
property, so you can optionally serialize the handle to disk.
const openFileOrFiles = async (multiple = false) => {
// Feature detection. The API needs to be supported
// and the app not run in an iframe.
const supportsFileSystemAccess =
"showOpenFilePicker" in window &&
(() => {
try {
return window.self === window.top;
} catch {
return false;
}
})();
// If the File System Access API is supported…
if (supportsFileSystemAccess) {
let fileOrFiles = undefined;
try {
// Show the file picker, optionally allowing multiple files.
const handles = await showOpenFilePicker({ multiple });
// Only one file is requested.
if (!multiple) {
// Add the `FileSystemFileHandle` as `.handle`.
fileOrFiles = await handles[0].getFile();
fileOrFiles.handle = handles[0];
} else {
fileOrFiles = await Promise.all(
handles.map(async (handle) => {
const file = await handle.getFile();
// Add the `FileSystemFileHandle` as `.handle`.
file.handle = handle;
return file;
})
);
}
} catch (err) {
// Fail silently if the user has simply canceled the dialog.
if (err.name !== 'AbortError') {
console.error(err.name, err.message);
}
}
return fileOrFiles;
}
// Fallback if the File System Access API is not supported.
return new Promise((resolve) => {
// Append a new `<input type="file" multiple? />` and hide it.
const input = document.createElement('input');
input.style.display = 'none';
input.type = 'file';
document.body.append(input);
if (multiple) {
input.multiple = true;
}
// The `change` event fires when the user interacts with the dialog.
input.addEventListener('change', () => {
// Remove the `<input type="file" multiple? />` again from the DOM.
input.remove();
// If no files were selected, return.
if (!input.files) {
return;
}
// Return all files or just one file.
resolve(multiple ? Array.from(input.files) : input.files[0]);
});
// Show the picker.
if ('showPicker' in HTMLInputElement.prototype) {
input.showPicker();
} else {
input.click();
}
});
};
Further reading #
Demo #
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>đź“‚</text></svg>"
/>
<title>How to open one or multiple files</title>
</head>
<body>
<h1>How to open one or multiple files</h1>
<button type="button">Open file</button>
<button class="multiple" type="button">Open files</button>
<pre></pre>
</body>
</html>
const button = document.querySelector('button');
const buttonMultiple = document.querySelector('button.multiple');
const pre = document.querySelector('pre');
const openFileOrFiles = async (multiple = false) => {
// Feature detection. The API needs to be supported
// and the app not run in an iframe.
const supportsFileSystemAccess =
"showOpenFilePicker" in window &&
(() => {
try {
return window.self === window.top;
} catch {
return false;
}
})();
// If the File System Access API is supported…
if (supportsFileSystemAccess) {
let fileOrFiles = undefined;
try {
// Show the file picker, optionally allowing multiple files.
fileOrFiles = await showOpenFilePicker({ multiple });
if (!multiple) {
// Only one file is requested.
fileOrFiles = fileOrFiles[0];
}
} catch (err) {
// Fail silently if the user has simply canceled the dialog.
if (err.name !== 'AbortError') {
console.error(err.name, err.message);
}
}
return fileOrFiles;
}
// Fallback if the File System Access API is not supported.
return new Promise((resolve) => {
// Append a new `<input type="file" multiple? />` and hide it.
const input = document.createElement('input');
input.style.display = 'none';
input.type = 'file';
document.body.append(input);
if (multiple) {
input.multiple = true;
}
// The `change` event fires when the user interacts with the dialog.
input.addEventListener('change', () => {
// Remove the `<input type="file" multiple? />` again from the DOM.
input.remove();
// If no files were selected, return.
if (!input.files) {
return;
}
// Return all files or just one file.
resolve(multiple ? input.files : input.files[0]);
});
// Show the picker.
if ('showPicker' in HTMLInputElement.prototype) {
input.showPicker();
} else {
input.click();
}
});
};
button.addEventListener('click', async () => {
const file = await openFileOrFiles();
if (!file) {
return;
}
pre.textContent += `${file.name}\n`;
});
buttonMultiple.addEventListener('click', async () => {
const files = await openFileOrFiles(true);
if (!files) {
return;
}
Array.from(files).forEach((file) => (pre.textContent += `${file.name}\n`));
});
:root {
color-scheme: dark light;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 1rem;
font-family: system-ui, sans-serif;
}
button {
margin: 1rem;
}