How to upload an image with a form?
Upload an image with a multipart form or with fetch and FormData, show a preview, check size and type, send multiple files, and track progress.
Use a form with method="post", enctype="multipart/form-data" and a file input with a name. Without the enctype, the browser sends only the file name, not the file.
<form
class="avatar-form"
action="/api/avatar"
method="post"
enctype="multipart/form-data"
>
<label for="avatar">Profile photo</label>
<input type="file" id="avatar" name="avatar" accept="image/*" required>
<button type="submit">Upload</button>
</form>The server reads the file by the input's name, here avatar. accept="image/*" makes the file picker show images, but it doesn't stop anyone from choosing another file. There's more on that in how to accept only certain file types.
Upload with JavaScript
To upload without leaving the page, stop the normal submit and send the same data with fetch. new FormData(form) collects every field, files included.
const form = document.querySelector('.avatar-form');
form.addEventListener('submit', async (event) => {
event.preventDefault();
const response = await fetch(form.action, {
method: 'POST',
body: new FormData(form),
});
if (!response.ok) {
// Show an error message next to the form
}
});Don't set the Content-Type header yourself. The browser sets it to multipart/form-data with a boundary value that separates the fields. If you set the header by hand, the boundary is missing and the server can't read the body.
You don't need a <form> element at all if the file comes from somewhere else, such as a drop zone:
const formData = new FormData();
formData.append('avatar', file);Show a preview before uploading
URL.createObjectURL() gives you a temporary URL for the chosen file, so you can show it right away without uploading anything.
<img class="avatar-preview" alt="" width="120" height="120" hidden>const input = document.querySelector('#avatar');
const preview = document.querySelector('.avatar-preview');
input.addEventListener('change', () => {
const file = input.files[0];
if (!file) return;
const url = URL.createObjectURL(file);
// Free the memory once the image has been displayed
preview.onload = () => URL.revokeObjectURL(url);
preview.src = url;
preview.hidden = false;
});Each object URL keeps the file in memory until you revoke it or the page is closed, so revoke it once the image has loaded.
Check the size and type
Checking on the client saves people from waiting for an upload that the server will reject anyway.
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
function validateImage(file) {
if (!file.type.startsWith('image/')) {
return 'Please choose an image.';
}
if (file.size > MAX_SIZE) {
return 'The image must be 5 MB or smaller.';
}
return '';
}
input.addEventListener('change', () => {
const file = input.files[0];
input.setCustomValidity(file ? validateImage(file) : '');
input.reportValidity();
});setCustomValidity() with a non-empty message marks the input as invalid, so the browser shows the message and blocks the form from submitting.
This is for user experience only. file.type is guessed from the file extension and can be empty or wrong, and anyone can skip your JavaScript. The server has to check again; see how to receive an uploaded image on the server.
Multiple files
Add multiple to the input:
<input type="file" name="photos" accept="image/*" multiple>new FormData(form) includes every selected file under the same name. To build the data yourself, append each file with that name:
const formData = new FormData();
for (const file of input.files) {
formData.append('photos', file);
}Show upload progress
fetch doesn't report upload progress, so for a progress bar use XMLHttpRequest, which has upload progress events:
function upload(url, formData, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url);
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
onProgress(event.loaded / event.total);
}
});
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Network error'));
});
xhr.send(formData);
});
}Use it in place of the fetch call, together with a <progress> element, which goes from 0 to 1 by default:
<progress class="upload-progress" value="0" aria-label="Upload"></progress>const bar = document.querySelector('.upload-progress');
form.addEventListener('submit', async (event) => {
event.preventDefault();
await upload(form.action, new FormData(form), (ratio) => {
bar.value = ratio;
});
});