Skip to main content

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
});
PropertyTypeDescription
namestringThe file's name including extension
sizenumberSize in bytes (inherited from Blob)
typestringMIME type, e.g. "image/jpeg" (inherited from Blob)
lastModifiednumberLast 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:

  1. Create a FileReader instance.
  2. Attach event handlers (onload, onerror, etc.).
  3. Call a read method (readAsText, readAsDataURL, readAsArrayBuffer).
  4. 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โ€‹

EventWhen It Fires
loadstartReading starts
progressPeriodically during reading (for large files)
loadReading completed successfully. Result is in reader.result.
errorAn error occurred. Details in reader.error.
abortReading was cancelled via reader.abort().
loadendReading 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)
ConstantValueDescription
FileReader.EMPTY0No data loaded yet
FileReader.LOADING1Data is being read
FileReader.DONE2Reading 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);
}
});
tip

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โ€‹

Methodreader.result TypeBest For
readAsText(blob, encoding?)stringText files, CSV, JSON, HTML, code
readAsDataURL(blob)string (Data URL)Image previews, embedding in HTML/CSS, storing as string
readAsArrayBuffer(blob)ArrayBufferBinary 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));
}
}
info

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);
}
});
caution

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:

  • File object properties: name, size, type, lastModified
  • <input type="file"> with multiple and hidden input for custom styling
  • file.text() (modern Blob method) for text file previews
  • URL.createObjectURL(file) for image previews with proper revocation
  • Drag-and-drop with dragenter, dragover, dragleave, drop event handling
  • File validation (size limits)
  • event.preventDefault() on both dragover and drop
  • 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, adding name, lastModified, and webkitRelativePath.
  • All Blob methods work on Files: text(), arrayBuffer(), bytes(), stream(), slice().
  • Created by <input type="file">, drag-and-drop, or the File constructor.
  • File instanceof Blob is true.

<input type="file">:

  • event.target.files returns a FileList (array-like) of File objects.
  • multiple allows selecting multiple files.
  • accept filters file types (but always validate in JavaScript too).
  • webkitdirectory allows selecting directories.
  • Clear with input.value = "".

FileReader Methods:

MethodResult TypeUse Case
readAsText(blob, encoding?)stringText, CSV, JSON, source code
readAsDataURL(blob)string (Data URL)Image previews, embedding in HTML, localStorage
readAsArrayBuffer(blob)ArrayBufferBinary formats, file headers, hashing

FileReader Key Points:

  • Event-based: use onload for results, onerror for failures, onprogress for large files.
  • Result is in reader.result after onload fires.
  • Wrap in Promises for cleaner async code.
  • Consider modern file.text() and file.arrayBuffer() as simpler alternatives when you do not need progress or abort.

Drag-and-Drop:

  • event.preventDefault() is required on both dragover and drop.
  • Files are in event.dataTransfer.files (a FileList).
  • Use dragenter/dragleave for visual feedback (highlight the drop zone).
  • Check event.relatedTarget in dragleave to avoid false triggers from child elements.
  • Use item.webkitGetAsEntry() for directory traversal.

Choosing How to Read Files:

NeedBest Approach
Simple text readingfile.text() (modern) or readAsText
Image preview displayURL.createObjectURL(file) (most efficient)
Image data for localStoragereadAsDataURL
Binary processingfile.arrayBuffer() (modern) or readAsArrayBuffer
Large file progressFileReader with onprogress
Cancellable readFileReader with abort()