How to Work with Files and FileReader in JavaScript
Users interact with files constantly: uploading photos, attaching documents, importing spreadsheets, dragging images into editors. JavaScript provides two core APIs for handling these interactions on the client side: the File object, which represents a file selected by the user, and FileReader, which reads the contents of that file into memory so your code can process it.
The File object gives you metadata about the file (name, size, type, last modified date) and, since it extends Blob, provides all the Blob methods you already know. FileReader is the classic event-based API for reading file contents as text, binary data, or Data URLs. Together with the HTML <input type="file"> element and the drag-and-drop API, these tools let you build rich file-handling experiences entirely in the browser, without uploading anything to a server.
This guide covers the File object and its relationship to Blob, how to use file inputs to let users select files, every FileReader method with practical examples, and how to implement drag-and-drop file uploads.
The File Object (Inherits from Blob)โ
A File object represents a file from the user's file system. It is a subclass of Blob, which means it inherits all Blob properties and methods while adding file-specific metadata.
File Propertiesโ
// Assuming a file was selected via <input type="file">
let fileInput = document.getElementById("fileInput");
fileInput.addEventListener("change", (event) => {
let file = event.target.files[0];
// Inherited from Blob
console.log(file.size); // 524288 (size in bytes)
console.log(file.type); // "image/png" (MIME type)
// File-specific properties
console.log(file.name); // "vacation-photo.png"
console.log(file.lastModified); // 1710500000000 (timestamp in ms)
console.log(file.lastModifiedDate); // Date object (deprecated, use lastModified)
// File IS a Blob
console.log(file instanceof Blob); // true
console.log(file instanceof File); // true
});
| Property | Type | Description |
|---|---|---|
name | string | The file's name including extension |
size | number | Size in bytes (inherited from Blob) |
type | string | MIME type, e.g. "image/jpeg" (inherited from Blob) |
lastModified | number | Last modification timestamp in milliseconds |
Since File Extends Blob, All Blob Methods Workโ
Because File inherits from Blob, you can use every Blob method directly on a File object:
fileInput.addEventListener("change", async (event) => {
let file = event.target.files[0];
// Blob methods work on File objects
let text = await file.text(); // Read as UTF-8 string
let buffer = await file.arrayBuffer(); // Read as ArrayBuffer
let stream = file.stream(); // Get a ReadableStream
// Slicing works too
let firstKB = file.slice(0, 1024, file.type);
console.log(firstKB.size); // 1024 (or less if file is smaller)
// Create a Blob URL
let url = URL.createObjectURL(file);
console.log(url); // "blob:http://localhost/..."
});
Creating File Objects Programmaticallyโ
While File objects usually come from user input, you can create them manually using the File constructor:
// new File(fileParts, fileName, options?)
let file = new File(
["Hello, World!\nLine 2\nLine 3"], // Array of parts (like Blob constructor)
"greeting.txt", // File name
{
type: "text/plain", // MIME type
lastModified: Date.now() // Last modified timestamp
}
);
console.log(file.name); // "greeting.txt"
console.log(file.size); // 28
console.log(file.type); // "text/plain"
console.log(file.lastModified); // current timestamp
// You can read it like any other File
let text = await file.text();
console.log(text); // "Hello, World!\nLine 2\nLine 3"
This is useful for testing, creating files to upload programmatically, or converting a Blob to a File (which adds a name):
// Convert a Blob to a File (adding a name)
let blob = new Blob(["CSV data here"], { type: "text/csv" });
let file = new File([blob], "data.csv", { type: blob.type });
console.log(file.name); // "data.csv"
console.log(file instanceof File); // true
// Upload it via fetch
let formData = new FormData();
formData.append("file", file); // FormData needs a File, not a Blob, for the filename
File Input: <input type="file">โ
The <input type="file"> element is the standard way for users to select files from their device. When the user selects one or more files, the input's files property contains a FileList of File objects.
Basic File Inputโ
<input type="file" id="singleFile">
let input = document.getElementById("singleFile");
input.addEventListener("change", (event) => {
let file = event.target.files[0]; // First (and only) selected file
if (!file) {
console.log("No file selected");
return;
}
console.log("Name:", file.name);
console.log("Size:", (file.size / 1024).toFixed(1), "KB");
console.log("Type:", file.type);
console.log("Last Modified:", new Date(file.lastModified).toLocaleString());
});
Multiple File Selectionโ
Add the multiple attribute to allow selecting more than one file:
<input type="file" id="multipleFiles" multiple>
let input = document.getElementById("multipleFiles");
input.addEventListener("change", (event) => {
let files = event.target.files; // FileList (array-like)
console.log(`${files.length} file(s) selected`);
for (let file of files) {
console.log(`- ${file.name} (${(file.size / 1024).toFixed(1)} KB, ${file.type})`);
}
// Convert FileList to a real array for array methods
let fileArray = Array.from(files);
let totalSize = fileArray.reduce((sum, f) => sum + f.size, 0);
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
});
Filtering by File Typeโ
The accept attribute restricts which file types the user can select. It does not prevent all invalid files (the user can override it), so always validate in JavaScript too:
<!-- Accept only images -->
<input type="file" accept="image/*">
<!-- Accept only specific image formats -->
<input type="file" accept="image/png, image/jpeg, image/webp">
<!-- Accept only PDF files -->
<input type="file" accept=".pdf, application/pdf">
<!-- Accept documents -->
<input type="file" accept=".doc, .docx, .pdf, .txt">
<!-- Accept CSV and Excel -->
<input type="file" accept=".csv, .xlsx, .xls, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
Selecting Directoriesโ
The webkitdirectory attribute lets users select an entire directory:
<input type="file" id="dirInput" webkitdirectory>
let dirInput = document.getElementById("dirInput");
dirInput.addEventListener("change", (event) => {
let files = event.target.files;
for (let file of files) {
// webkitRelativePath shows the path relative to the selected directory
console.log(file.webkitRelativePath, file.size);
}
// Example output:
// "my-project/index.html" 1234
// "my-project/styles/main.css" 5678
// "my-project/scripts/app.js" 9012
});
Validating Filesโ
Always validate files in JavaScript, regardless of the accept attribute:
function validateFile(file, options = {}) {
let errors = [];
// Check file type
if (options.allowedTypes && options.allowedTypes.length > 0) {
let isAllowed = options.allowedTypes.some(type => {
if (type.endsWith("/*")) {
// Wildcard: "image/*" matches "image/png", "image/jpeg", etc.
return file.type.startsWith(type.slice(0, -1));
}
if (type.startsWith(".")) {
// Extension: ".pdf" matches files ending in .pdf
return file.name.toLowerCase().endsWith(type.toLowerCase());
}
return file.type === type;
});
if (!isAllowed) {
errors.push(`File type "${file.type || "unknown"}" is not allowed`);
}
}
// Check file size
if (options.maxSize && file.size > options.maxSize) {
let maxMB = (options.maxSize / 1024 / 1024).toFixed(1);
let fileMB = (file.size / 1024 / 1024).toFixed(1);
errors.push(`File is ${fileMB} MB, maximum is ${maxMB} MB`);
}
if (options.minSize && file.size < options.minSize) {
errors.push(`File is too small (${file.size} bytes)`);
}
return {
valid: errors.length === 0,
errors
};
}
// Usage
let input = document.getElementById("imageInput");
input.addEventListener("change", (event) => {
let file = event.target.files[0];
if (!file) return;
let result = validateFile(file, {
allowedTypes: ["image/png", "image/jpeg", "image/webp"],
maxSize: 5 * 1024 * 1024, // 5 MB
});
if (!result.valid) {
alert("Invalid file:\n" + result.errors.join("\n"));
input.value = ""; // Clear the input
return;
}
console.log("File is valid, proceeding...");
});
Programmatically Triggering File Selectionโ
You can trigger the file dialog from JavaScript without the user clicking the actual input element. This is useful for custom-styled upload buttons:
<button id="uploadBtn">Choose Photo</button>
<input type="file" id="hiddenInput" accept="image/*" style="display: none;">
let uploadBtn = document.getElementById("uploadBtn");
let hiddenInput = document.getElementById("hiddenInput");
uploadBtn.addEventListener("click", () => {
hiddenInput.click(); // Opens the file dialog
});
hiddenInput.addEventListener("change", (event) => {
let file = event.target.files[0];
if (file) {
console.log("Selected:", file.name);
}
});
Resetting a File Inputโ
To clear a file input and allow re-selecting the same file:
// Clear the input
fileInput.value = "";
// or
fileInput.value = null;
// Now the "change" event will fire even if the user selects the same file again
FileReader: Reading File Contentsโ
FileReader is an event-based API for asynchronously reading the contents of File or Blob objects. It provides methods for reading data in different formats: as text, as a Data URL (base64), or as an ArrayBuffer.
FileReader Lifecycleโ
Every FileReader operation follows the same pattern:
- Create a
FileReaderinstance. - Attach event handlers (
onload,onerror, etc.). - Call a read method (
readAsText,readAsDataURL,readAsArrayBuffer). - Wait for the result via events.
let reader = new FileReader();
// Events (in order of the lifecycle)
reader.onloadstart = () => console.log("Started reading");
reader.onprogress = (event) => {
if (event.lengthComputable) {
let percent = (event.loaded / event.total * 100).toFixed(1);
console.log(`Progress: ${percent}%`);
}
};
reader.onload = () => {
console.log("Reading complete!");
console.log("Result:", reader.result); // The file contents
};
reader.onerror = () => {
console.error("Error:", reader.error);
};
reader.onloadend = () => {
console.log("Finished (success or failure)");
};
// Start reading
reader.readAsText(file);
FileReader Eventsโ
| Event | When It Fires |
|---|---|
loadstart | Reading starts |
progress | Periodically during reading (for large files) |
load | Reading completed successfully. Result is in reader.result. |
error | An error occurred. Details in reader.error. |
abort | Reading was cancelled via reader.abort(). |
loadend | Reading finished (after load, error, or abort). Always fires. |
FileReader Propertiesโ
let reader = new FileReader();
// Before reading:
console.log(reader.readyState); // 0 (EMPTY)
console.log(reader.result); // null
// During reading:
// reader.readyState === 1 (LOADING)
// After reading:
// reader.readyState === 2 (DONE)
// reader.result contains the data
// reader.error is null (on success) or a DOMException (on failure)
| Constant | Value | Description |
|---|---|---|
FileReader.EMPTY | 0 | No data loaded yet |
FileReader.LOADING | 1 | Data is being read |
FileReader.DONE | 2 | Reading is complete |
readAsText, readAsDataURL, readAsArrayBufferโ
FileReader provides three main methods for reading file contents, each producing the result in a different format.
readAsText(blob, encoding?): Read as Stringโ
Reads the file as a text string. The optional encoding parameter specifies the character encoding (defaults to UTF-8):
let input = document.getElementById("textFileInput");
input.addEventListener("change", (event) => {
let file = event.target.files[0];
if (!file) return;
let reader = new FileReader();
reader.onload = () => {
let text = reader.result; // String
console.log("File contents:", text);
console.log("Length:", text.length, "characters");
// Display in a textarea
document.getElementById("output").value = text;
};
reader.onerror = () => {
console.error("Error reading file:", reader.error.message);
};
reader.readAsText(file); // Default: UTF-8
});
Reading with a specific encoding:
// Read a file encoded in Windows-1251 (Cyrillic)
reader.readAsText(file, "windows-1251");
// Read a file encoded in Shift_JIS (Japanese)
reader.readAsText(file, "shift_jis");
// Read a file encoded in ISO-8859-1 (Latin-1)
reader.readAsText(file, "iso-8859-1");
Practical example: CSV file parser
function parseCSVFile(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => {
let text = reader.result;
let lines = text.split("\n").filter(line => line.trim());
let headers = lines[0].split(",").map(h => h.trim());
let rows = lines.slice(1).map(line => {
let values = line.split(",").map(v => v.trim());
let row = {};
headers.forEach((header, i) => {
row[header] = values[i] || "";
});
return row;
});
resolve({ headers, rows });
};
reader.onerror = () => reject(reader.error);
reader.readAsText(file);
});
}
// Usage
input.addEventListener("change", async (event) => {
let file = event.target.files[0];
let { headers, rows } = await parseCSVFile(file);
console.log("Headers:", headers);
console.table(rows);
});
readAsDataURL(blob): Read as Base64 Data URLโ
Reads the file and produces a Data URL string in the format data:[type];base64,[encoded data]. This is the standard way to display user-selected images immediately without uploading them to a server:
let imageInput = document.getElementById("imageInput");
let preview = document.getElementById("preview");
imageInput.addEventListener("change", (event) => {
let file = event.target.files[0];
if (!file || !file.type.startsWith("image/")) return;
let reader = new FileReader();
reader.onload = () => {
let dataUrl = reader.result; // "data:image/png;base64,iVBORw0KGgo..."
// Use directly as image source
preview.src = dataUrl;
console.log("Data URL length:", dataUrl.length, "characters");
};
reader.readAsDataURL(file);
});
Multiple image preview:
let multiInput = document.getElementById("multiImageInput");
let gallery = document.getElementById("gallery");
multiInput.addEventListener("change", (event) => {
gallery.innerHTML = ""; // Clear previous previews
for (let file of event.target.files) {
if (!file.type.startsWith("image/")) continue;
let reader = new FileReader();
reader.onload = () => {
let img = document.createElement("img");
img.src = reader.result;
img.style.cssText = "max-width: 200px; max-height: 200px; margin: 8px; border-radius: 8px;";
gallery.append(img);
};
reader.readAsDataURL(file);
}
});
For image previews, URL.createObjectURL(file) is generally more efficient than readAsDataURL because it does not encode the entire file into a base64 string (which is 33% larger than the original binary data):
// More efficient for previews (no base64 encoding overhead)
let url = URL.createObjectURL(file);
preview.src = url;
preview.onload = () => URL.revokeObjectURL(url);
// readAsDataURL is better when you need to:
// - Store the image in localStorage
// - Embed the image in JSON data
// - Send the image as part of a text-based payload
// - Use the image data in a context that requires a string
readAsArrayBuffer(blob): Read as Raw Binaryโ
Reads the file as an ArrayBuffer, giving you access to the raw bytes. Use this when you need to process binary file formats, inspect file headers, or perform byte-level operations:
let fileInput = document.getElementById("binaryInput");
fileInput.addEventListener("change", (event) => {
let file = event.target.files[0];
if (!file) return;
let reader = new FileReader();
reader.onload = () => {
let buffer = reader.result; // ArrayBuffer
let bytes = new Uint8Array(buffer);
console.log("File size:", buffer.byteLength, "bytes");
console.log("First 10 bytes:", Array.from(bytes.slice(0, 10)));
// Detect file type from magic bytes
let type = detectFileType(bytes);
console.log("Detected type:", type);
};
reader.readAsArrayBuffer(file);
});
function detectFileType(bytes) {
if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {
return "PNG";
}
if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {
return "JPEG";
}
if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {
return "GIF";
}
if (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46) {
return "PDF";
}
if (bytes[0] === 0x50 && bytes[1] === 0x4B) {
return "ZIP";
}
return "Unknown";
}
Practical example: Reading EXIF data from a JPEG
function readJPEGDimensions(file) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => {
let view = new DataView(reader.result);
// Check JPEG magic bytes
if (view.getUint16(0) !== 0xFFD8) {
reject(new Error("Not a JPEG file"));
return;
}
// Walk through JPEG segments to find SOF (Start of Frame)
let offset = 2;
while (offset < view.byteLength) {
let marker = view.getUint16(offset);
if (marker === 0xFFC0 || marker === 0xFFC2) {
// SOF0 or SOF2: contains dimensions
let height = view.getUint16(offset + 5);
let width = view.getUint16(offset + 7);
resolve({ width, height });
return;
}
// Skip to next segment
let segmentLength = view.getUint16(offset + 2);
offset += 2 + segmentLength;
}
reject(new Error("Could not find image dimensions"));
};
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
// Usage
imageInput.addEventListener("change", async (event) => {
let file = event.target.files[0];
try {
let { width, height } = await readJPEGDimensions(file);
console.log(`Image dimensions: ${width} x ${height}`);
} catch (error) {
console.error(error.message);
}
});
Method Comparisonโ
| Method | reader.result Type | Best For |
|---|---|---|
readAsText(blob, encoding?) | string | Text files, CSV, JSON, HTML, code |
readAsDataURL(blob) | string (Data URL) | Image previews, embedding in HTML/CSS, storing as string |
readAsArrayBuffer(blob) | ArrayBuffer | Binary files, file format parsing, hashing, byte-level processing |
Wrapping FileReader in Promisesโ
The event-based API of FileReader becomes verbose quickly. Wrapping it in Promises produces cleaner code:
function readAsText(blob, encoding = "utf-8") {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsText(blob, encoding);
});
}
function readAsDataURL(blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}
function readAsArrayBuffer(blob) {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(blob);
});
}
// Clean async usage
async function processFile(file) {
if (file.type.startsWith("text/")) {
let text = await readAsText(file);
console.log("Text content:", text);
} else if (file.type.startsWith("image/")) {
let dataUrl = await readAsDataURL(file);
document.getElementById("preview").src = dataUrl;
} else {
let buffer = await readAsArrayBuffer(file);
let bytes = new Uint8Array(buffer);
console.log("Binary, first bytes:", bytes.slice(0, 20));
}
}
Modern Blob/File objects have built-in Promise-based methods (file.text(), file.arrayBuffer(), file.bytes()) that often eliminate the need for FileReader entirely:
// Modern approach (no FileReader needed):
let text = await file.text();
let buffer = await file.arrayBuffer();
// FileReader is still needed for:
// - readAsDataURL (no built-in equivalent on Blob)
// - Progress tracking (onprogress event)
// - Aborting reads (reader.abort())
// - Supporting older browsers
Tracking Progress for Large Filesโ
For large files, the progress event gives you real-time feedback:
function readWithProgress(file, method = "readAsArrayBuffer") {
return new Promise((resolve, reject) => {
let reader = new FileReader();
reader.onprogress = (event) => {
if (event.lengthComputable) {
let percent = (event.loaded / event.total * 100).toFixed(1);
let loadedMB = (event.loaded / 1024 / 1024).toFixed(1);
let totalMB = (event.total / 1024 / 1024).toFixed(1);
console.log(`Reading: ${percent}% (${loadedMB}/${totalMB} MB)`);
// Update a progress bar
let progressBar = document.getElementById("progress");
if (progressBar) {
progressBar.style.width = `${percent}%`;
progressBar.textContent = `${percent}%`;
}
}
};
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader[method](file);
});
}
// Usage
let buffer = await readWithProgress(largeFile, "readAsArrayBuffer");
Aborting a Readโ
You can cancel an in-progress read with reader.abort():
let reader = new FileReader();
let abortBtn = document.getElementById("abortBtn");
reader.onload = () => console.log("Done:", reader.result.byteLength, "bytes");
reader.onabort = () => console.log("Read aborted by user");
reader.onerror = () => console.error("Error:", reader.error);
// Start reading a large file
reader.readAsArrayBuffer(veryLargeFile);
// User can abort
abortBtn.addEventListener("click", () => {
reader.abort();
});
Drag-and-Drop File Uploadโ
Drag-and-drop provides a more intuitive file selection experience than the standard file input. Users can drag files directly from their desktop or file manager into a designated area on the page.
The Drop Zoneโ
<div id="dropZone" class="drop-zone">
<p>Drag and drop files here</p>
<p class="hint">or click to browse</p>
<input type="file" id="fallbackInput" multiple hidden>
</div>
<style>
.drop-zone {
width: 400px;
height: 200px;
border: 3px dashed #ccc;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: system-ui, sans-serif;
color: #666;
cursor: pointer;
transition: all 0.2s;
}
.drop-zone.dragover {
border-color: #2196f3;
background-color: #e3f2fd;
color: #1565c0;
}
.drop-zone .hint {
font-size: 14px;
color: #999;
}
</style>
Core Drag-and-Drop Eventsโ
Four events are essential for implementing drag-and-drop:
let dropZone = document.getElementById("dropZone");
let fallbackInput = document.getElementById("fallbackInput");
// Click to browse (fallback)
dropZone.addEventListener("click", () => {
fallbackInput.click();
});
fallbackInput.addEventListener("change", (event) => {
handleFiles(event.target.files);
});
// Prevent default drag behaviors on the entire document
// (prevents the browser from opening the file)
document.addEventListener("dragover", (event) => {
event.preventDefault();
});
document.addEventListener("drop", (event) => {
event.preventDefault();
});
// Highlight the drop zone when a file is dragged over it
dropZone.addEventListener("dragenter", (event) => {
event.preventDefault();
dropZone.classList.add("dragover");
});
dropZone.addEventListener("dragover", (event) => {
event.preventDefault(); // Required to allow drop
dropZone.classList.add("dragover");
});
dropZone.addEventListener("dragleave", (event) => {
// Only remove highlight if we actually left the drop zone
// (dragleave fires when entering child elements)
if (!dropZone.contains(event.relatedTarget)) {
dropZone.classList.remove("dragover");
}
});
// Handle the drop
dropZone.addEventListener("drop", (event) => {
event.preventDefault();
dropZone.classList.remove("dragover");
let files = event.dataTransfer.files; // FileList
if (files.length > 0) {
handleFiles(files);
}
});
You must call event.preventDefault() in both the dragover and drop event handlers. If you forget preventDefault() on dragover, the browser will not recognize the element as a valid drop target and the drop event will not fire. If you forget it on drop, the browser will try to navigate to the dropped file.
Processing Dropped Filesโ
function handleFiles(fileList) {
let files = Array.from(fileList);
console.log(`${files.length} file(s) dropped`);
files.forEach(file => {
console.log(`- ${file.name} (${formatFileSize(file.size)}, ${file.type})`);
});
// Process each file based on type
files.forEach(processFile);
}
function formatFileSize(bytes) {
if (bytes === 0) return "0 B";
let units = ["B", "KB", "MB", "GB"];
let i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
}
async function processFile(file) {
if (file.type.startsWith("image/")) {
displayImagePreview(file);
} else if (file.type === "text/plain" || file.type === "text/csv") {
let text = await file.text();
console.log("Text file content:", text.substring(0, 200));
} else if (file.type === "application/json") {
let text = await file.text();
let data = JSON.parse(text);
console.log("JSON data:", data);
} else {
console.log(`Binary file: ${file.name} (${file.type})`);
}
}
function displayImagePreview(file) {
let url = URL.createObjectURL(file);
let container = document.createElement("div");
container.style.cssText = "display: inline-block; margin: 8px; position: relative;";
let img = document.createElement("img");
img.src = url;
img.style.cssText = "max-width: 200px; max-height: 150px; border-radius: 8px; display: block;";
img.onload = () => URL.revokeObjectURL(url);
let label = document.createElement("small");
label.textContent = `${file.name} (${formatFileSize(file.size)})`;
label.style.cssText = "display: block; text-align: center; margin-top: 4px; color: #666;";
container.append(img, label);
document.getElementById("previews").append(container);
}
Validating Dropped Filesโ
dropZone.addEventListener("drop", (event) => {
event.preventDefault();
dropZone.classList.remove("dragover");
let files = Array.from(event.dataTransfer.files);
// Validate each file
let allowedTypes = ["image/png", "image/jpeg", "image/webp"];
let maxSize = 10 * 1024 * 1024; // 10 MB
let validFiles = [];
let errors = [];
files.forEach(file => {
if (!allowedTypes.includes(file.type)) {
errors.push(`"${file.name}": type "${file.type}" not allowed`);
} else if (file.size > maxSize) {
errors.push(`"${file.name}": ${formatFileSize(file.size)} exceeds ${formatFileSize(maxSize)} limit`);
} else {
validFiles.push(file);
}
});
if (errors.length > 0) {
alert("Some files were rejected:\n" + errors.join("\n"));
}
if (validFiles.length > 0) {
handleFiles(validFiles);
}
});
Reading Dropped Directoriesโ
When a user drops a folder, you can access its contents using the DataTransferItem API:
dropZone.addEventListener("drop", async (event) => {
event.preventDefault();
dropZone.classList.remove("dragover");
let items = event.dataTransfer.items;
let allFiles = [];
for (let item of items) {
let entry = item.webkitGetAsEntry();
if (entry) {
let files = await readEntry(entry);
allFiles.push(...files);
}
}
console.log(`Found ${allFiles.length} files total`);
allFiles.forEach(f => console.log(f.fullPath, formatFileSize(f.file.size)));
});
async function readEntry(entry, path = "") {
if (entry.isFile) {
let file = await new Promise(resolve => entry.file(resolve));
return [{ file, fullPath: path + entry.name }];
}
if (entry.isDirectory) {
let dirReader = entry.createReader();
let entries = await new Promise(resolve => dirReader.readEntries(resolve));
let files = [];
for (let childEntry of entries) {
let childFiles = await readEntry(childEntry, path + entry.name + "/");
files.push(...childFiles);
}
return files;
}
return [];
}
Complete Practical Exampleโ
Here is a comprehensive file manager component that combines file input, drag-and-drop, validation, and preview:
<!DOCTYPE html>
<html>
<head>
<title>File Manager</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; }
.upload-area {
border: 3px dashed #ccc; border-radius: 12px; padding: 40px;
text-align: center; cursor: pointer; transition: all 0.2s;
margin: 20px 0;
}
.upload-area.dragover { border-color: #2196f3; background: #e3f2fd; }
.upload-area p { margin: 8px 0; }
.upload-area .primary { font-size: 18px; font-weight: bold; color: #333; }
.upload-area .secondary { font-size: 14px; color: #999; }
.file-list { list-style: none; padding: 0; }
.file-item {
display: flex; align-items: center; gap: 12px;
padding: 12px; margin: 8px 0; background: #f8f9fa;
border-radius: 8px; border: 1px solid #e0e0e0;
}
.file-item .preview {
width: 60px; height: 60px; object-fit: cover; border-radius: 6px;
background: #e0e0e0; display: flex; align-items: center;
justify-content: center; font-size: 24px; flex-shrink: 0;
}
.file-item .preview img { width: 100%; height: 100%; object-fit: cover; border-radius: 6px; }
.file-item .info { flex-grow: 1; }
.file-item .name { font-weight: bold; word-break: break-all; }
.file-item .meta { font-size: 13px; color: #666; margin-top: 4px; }
.file-item .content-preview {
font-size: 12px; color: #888; margin-top: 4px;
max-height: 40px; overflow: hidden; font-family: monospace;
}
.file-item .remove {
background: none; border: none; font-size: 20px;
cursor: pointer; color: #999; padding: 8px; flex-shrink: 0;
}
.file-item .remove:hover { color: #f44336; }
.stats { padding: 12px; background: #e8f5e9; border-radius: 8px; margin-top: 16px; font-size: 14px; }
</style>
</head>
<body>
<h1>File Manager</h1>
<div class="upload-area" id="uploadArea">
<p class="primary">Drop files here or click to browse</p>
<p class="secondary">Images, text files, JSON, CSV (max 10 MB each)</p>
<input type="file" id="fileInput" multiple hidden>
</div>
<ul class="file-list" id="fileList"></ul>
<div class="stats" id="stats" hidden></div>
<script>
let uploadArea = document.getElementById("uploadArea");
let fileInput = document.getElementById("fileInput");
let fileList = document.getElementById("fileList");
let statsEl = document.getElementById("stats");
let managedFiles = [];
// Click to browse
uploadArea.addEventListener("click", () => fileInput.click());
fileInput.addEventListener("change", (e) => addFiles(e.target.files));
// Drag and drop
document.addEventListener("dragover", e => e.preventDefault());
document.addEventListener("drop", e => e.preventDefault());
uploadArea.addEventListener("dragenter", (e) => {
e.preventDefault();
uploadArea.classList.add("dragover");
});
uploadArea.addEventListener("dragover", (e) => {
e.preventDefault();
uploadArea.classList.add("dragover");
});
uploadArea.addEventListener("dragleave", (e) => {
if (!uploadArea.contains(e.relatedTarget)) {
uploadArea.classList.remove("dragover");
}
});
uploadArea.addEventListener("drop", (e) => {
e.preventDefault();
uploadArea.classList.remove("dragover");
addFiles(e.dataTransfer.files);
});
// Core logic
function addFiles(fileListObj) {
let files = Array.from(fileListObj);
let maxSize = 10 * 1024 * 1024;
let errors = [];
for (let file of files) {
if (file.size > maxSize) {
errors.push(`"${file.name}" exceeds 10 MB limit`);
continue;
}
managedFiles.push(file);
renderFileItem(file, managedFiles.length - 1);
}
if (errors.length > 0) {
alert("Some files were skipped:\n" + errors.join("\n"));
}
updateStats();
fileInput.value = "";
}
async function renderFileItem(file, index) {
let li = document.createElement("li");
li.className = "file-item";
li.dataset.index = index;
// Preview
let previewEl = document.createElement("div");
previewEl.className = "preview";
if (file.type.startsWith("image/")) {
let img = document.createElement("img");
let url = URL.createObjectURL(file);
img.src = url;
img.onload = () => URL.revokeObjectURL(url);
previewEl.append(img);
} else if (file.type.startsWith("text/") || file.type === "application/json") {
previewEl.textContent = "๐";
} else if (file.type === "application/pdf") {
previewEl.textContent = "๐";
} else {
previewEl.textContent = "๐";
}
// Info
let infoEl = document.createElement("div");
infoEl.className = "info";
let nameEl = document.createElement("div");
nameEl.className = "name";
nameEl.textContent = file.name;
let metaEl = document.createElement("div");
metaEl.className = "meta";
metaEl.textContent = `${formatSize(file.size)} ยท ${file.type || "unknown type"} ยท ${new Date(file.lastModified).toLocaleDateString()}`;
infoEl.append(nameEl, metaEl);
// Text preview for text files
if (file.type.startsWith("text/") || file.type === "application/json") {
try {
let text = await file.text();
let contentPreview = document.createElement("div");
contentPreview.className = "content-preview";
contentPreview.textContent = text.substring(0, 150) + (text.length > 150 ? "..." : "");
infoEl.append(contentPreview);
} catch (e) {
// Ignore read errors for preview
}
}
// Remove button
let removeBtn = document.createElement("button");
removeBtn.className = "remove";
removeBtn.textContent = "โ";
removeBtn.title = "Remove file";
removeBtn.addEventListener("click", () => {
managedFiles.splice(index, 1);
li.remove();
updateStats();
// Re-index remaining items
fileList.querySelectorAll(".file-item").forEach((item, i) => {
item.dataset.index = i;
});
});
li.append(previewEl, infoEl, removeBtn);
fileList.append(li);
}
function updateStats() {
if (managedFiles.length === 0) {
statsEl.hidden = true;
return;
}
let totalSize = managedFiles.reduce((sum, f) => sum + f.size, 0);
let typeGroups = {};
managedFiles.forEach(f => {
let category = f.type.split("/")[0] || "other";
typeGroups[category] = (typeGroups[category] || 0) + 1;
});
let typeSummary = Object.entries(typeGroups)
.map(([type, count]) => `${count} ${type}`)
.join(", ");
statsEl.textContent = `${managedFiles.length} files ยท ${formatSize(totalSize)} total ยท ${typeSummary}`;
statsEl.hidden = false;
}
function formatSize(bytes) {
if (bytes === 0) return "0 B";
let units = ["B", "KB", "MB", "GB"];
let i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + " " + units[i];
}
</script>
</body>
</html>
This example demonstrates:
Fileobject properties:name,size,type,lastModified<input type="file">withmultipleand hidden input for custom stylingfile.text()(modern Blob method) for text file previewsURL.createObjectURL(file)for image previews with proper revocation- Drag-and-drop with
dragenter,dragover,dragleave,dropevent handling - File validation (size limits)
event.preventDefault()on bothdragoveranddrop- Dynamic file list management with add/remove functionality
Summaryโ
The File and FileReader APIs let you work with user-selected files entirely on the client side.
File Object:
- Inherits from
Blob, addingname,lastModified, andwebkitRelativePath. - All Blob methods work on Files:
text(),arrayBuffer(),bytes(),stream(),slice(). - Created by
<input type="file">, drag-and-drop, or theFileconstructor. File instanceof Blobistrue.
<input type="file">:
event.target.filesreturns aFileList(array-like) ofFileobjects.multipleallows selecting multiple files.acceptfilters file types (but always validate in JavaScript too).webkitdirectoryallows selecting directories.- Clear with
input.value = "".
FileReader Methods:
| Method | Result Type | Use Case |
|---|---|---|
readAsText(blob, encoding?) | string | Text, CSV, JSON, source code |
readAsDataURL(blob) | string (Data URL) | Image previews, embedding in HTML, localStorage |
readAsArrayBuffer(blob) | ArrayBuffer | Binary formats, file headers, hashing |
FileReader Key Points:
- Event-based: use
onloadfor results,onerrorfor failures,onprogressfor large files. - Result is in
reader.resultafteronloadfires. - Wrap in Promises for cleaner async code.
- Consider modern
file.text()andfile.arrayBuffer()as simpler alternatives when you do not need progress or abort.
Drag-and-Drop:
event.preventDefault()is required on bothdragoveranddrop.- Files are in
event.dataTransfer.files(aFileList). - Use
dragenter/dragleavefor visual feedback (highlight the drop zone). - Check
event.relatedTargetindragleaveto avoid false triggers from child elements. - Use
item.webkitGetAsEntry()for directory traversal.
Choosing How to Read Files:
| Need | Best Approach |
|---|---|
| Simple text reading | file.text() (modern) or readAsText |
| Image preview display | URL.createObjectURL(file) (most efficient) |
| Image data for localStorage | readAsDataURL |
| Binary processing | file.arrayBuffer() (modern) or readAsArrayBuffer |
| Large file progress | FileReader with onprogress |
| Cancellable read | FileReader with abort() |