HTML forms · 7 / 9
lesson 7

File inputs and uploads

input type=file, accept, multiple, capture — and the File and FileList shape the browser hands you in JavaScript.

~ 15 min read·lesson 7 of 9
0 / 9

A file input is the one form control where the user does not type. Click it, the OS file picker opens, the user picks one or more files, and the form has them. The browser does the picker, the preview, the size warning. You write one tag.

This lesson covers the markup, the shape of the data the browser hands you, and the small details that decide whether the upload reaches the server intact.

The basic file input

<input type="file"> is the simplest form. The form needs enctype="multipart/form-data" (lesson 1 of this course) so the file's bytes can fit in the request body.

upload.html
<form action="/upload" method="post" enctype="multipart/form-data">
<label for="avatar">Profile picture</label>
<input id="avatar" name="avatar" type="file">
<button type="submit">Upload</button>
</form>

Submit the form and the browser sends a multipart request with the file's bytes, name, and content type. The server reads it as a file, not as a string.

The user sees a "Choose file" button and the filename of whatever they pick. Browsers vary in how this button is styled — Safari, Chrome, and Firefox all draw it differently and do not let CSS reach inside it. The standard workaround is to hide the input and trigger it from a styled <label>:

styled.html
<label class="upload-button" for="avatar">Choose photo</label>
<input id="avatar" name="avatar" type="file" hidden>

The <input> is hidden (no visible UI). The <label for="avatar"> is the visible click target. Click the label and the OS picker opens — the label still triggers the input. The label can be styled like any other element.

A useful picture: the file input is the door to the OS's file picker. You do not control the picker; you control the door. Hiding the input and putting your own door (the label) in front of it is how you customize the look.

accept and the file picker

The accept attribute hints to the picker which files to show. It takes a comma-separated list of MIME types or extensions.

accept.html
<!-- Only image files in the picker -->
<input type="file" accept="image/*">

<!-- Only PDFs and Word docs -->
<input type="file" accept="application/pdf,.docx">

<!-- Photos and videos -->
<input type="file" accept="image/*,video/*">

image/* is "any image format". .docx is the file extension as a fallback when the MIME type is not standardized.

The user can usually still pick "all files" in the picker — accept is a hint, not a hard filter. Validate the file's actual type on the server (and ideally before uploading too).

Watch out

Never trust accept as security. The user can change the filter, rename a file, or send the request with a tool like curl. Always check the file's real type on the server (sniff the bytes — the file's magic number is more reliable than its extension).

multiple and capture

multiple lets the user pick several files at once.

multi.html
<input type="file" name="photos" multiple accept="image/*">

The picker now accepts Cmd-click / Shift-click / Ctrl-click. The submitted field name (photos) carries multiple files; the server sees an array.

capture tells mobile browsers to use the device camera or microphone instead of the file gallery. Two values:

  • capture="user" — the front camera.
  • capture="environment" — the rear camera.
capture.html
<input type="file" name="selfie" accept="image/*" capture="user">
<input type="file" name="document" accept="image/*" capture="environment">

On mobile, capture="user" jumps straight to the front camera, ready to take a photo. The user can usually still escape to the gallery, but the default is the camera. Useful for selfie-based ID checks, document scanning, voice-note recording (accept="audio/*" capture).

On desktop, capture is ignored and the regular picker opens.

check your understanding
You add a file input that should let the user pick up to 10 photos at once: <input type="file" name="photos" multiple accept="image/*">. Your form has method="post". What else must be true for the upload to work?

Files in JavaScript

When the user picks a file, the input's files property is a FileList — an array-like list of File objects.

files-shape.js
const input = document.querySelector('input[type=file]');

input.addEventListener('change', () => {
const list = input.files;          // FileList
console.log(list.length);          // 3 (when multiple)
const first = list[0];             // File

console.log(first.name);           // "vacation.jpg"
console.log(first.size);           // bytes
console.log(first.type);           // "image/jpeg"
console.log(first.lastModified);   // timestamp
});

A File object extends the Blob interface — you can read it as text, as binary, or as a data URL.

preview.js
const file = input.files[0];
const url = URL.createObjectURL(file);

const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);

// When you're done with the preview, free the URL.
img.onload = () => URL.revokeObjectURL(url);

URL.createObjectURL(file) gives you a temporary URL that points at the file in memory — perfect for previewing an image before upload. The browser holds the file in memory until you call URL.revokeObjectURL (or the page unloads). For one-off previews, revoke after the image loads.

To upload via fetch, wrap the form (or just the file) in FormData:

upload.js
const formData = new FormData();
formData.append('avatar', input.files[0]);

await fetch('/upload', {
method: 'POST',
body: formData,
// Don't set Content-Type — fetch picks it for FormData,
// including the multipart boundary.
});

The trick that bites people: do not set Content-Type manually when sending FormData. The browser needs to set it itself because the multipart boundary is generated per-request. Set it yourself and the request body becomes unparseable.

Tip

Validating file size in the browser before upload is good UX. Read file.size and refuse anything over your limit; show an error inline. The server still has to validate too — never trust the client. But catching a 50MB file before it spends three minutes uploading saves users time.

Drag and drop, briefly

The file input is one source of files. Drag-and-drop is another. Any element can become a drop target by listening for drag events:

drop-zone.js
const zone = document.getElementById('drop');

zone.addEventListener('dragover', (e) => {
e.preventDefault(); // required, otherwise drop won't fire
});

zone.addEventListener('drop', (e) => {
e.preventDefault();
const files = e.dataTransfer.files; // a FileList again
console.log(files);
});

Two things to know. First, you must preventDefault() on the dragover event, otherwise the browser refuses to fire the drop event. Second, the dropped files arrive as a FileList — the same shape you get from a file input, so the rest of your code does not change.

For the best UX, support both: a <input type="file"> for keyboard and click users, and a drop zone that targets the same upload pipeline. The drop zone is the enhancement; the file input is the floor.

check your understanding
You build a drop zone that listens for the drop event but never receives one. The page just opens the file in a new tab when you drop. The most likely fix is:
check your understanding
You upload a file via fetch using FormData. You manually set Content-Type: multipart/form-data in the headers. The server reports the request body is malformed. Why?
check your understanding
You want the user to take a photo with their phone's rear camera and upload it. The smallest correct markup is:
← prevnext lesson →
KeepLearningcertificate
for completing
HTML forms
0 of 9 read