Accessible File Input with Live Feedback
Style a native multiple file input with a focusable label button, announce selected files, and reject oversized files before upload.
Code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accessible supporting documents upload</title>
<style>
body {
margin: 0;
padding: 2rem;
font-family: system-ui, sans-serif;
}
form {
max-width: 32rem;
display: grid;
gap: 0.75rem;
}
.file-picker {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
.file-input {
position: absolute;
inline-size: 1px;
block-size: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
.file-button {
display: inline-block;
padding: 0.55rem 1rem;
border: 1px solid currentColor;
border-radius: 0.4rem;
cursor: pointer;
}
.file-input:focus-visible + .file-button {
outline: 3px solid #4f46e5;
outline-offset: 2px;
}
.field-error {
margin: 0;
color: #b91c1c;
}
</style>
</head>
<body>
<main>
<h1>Supporting documents</h1>
<form id="upload-form">
<label for="documents">Supporting documents</label>
<p id="documents-help">PDF or PNG, up to 5 MB per file. Multiple files are allowed.</p>
<div class="file-picker">
<input
class="file-input"
id="documents"
name="documents"
type="file"
accept=".pdf,image/png"
multiple
aria-describedby="documents-help documents-error">
<label class="file-button" for="documents">Choose files</label>
<span id="file-count" aria-live="polite">No files selected</span>
</div>
<p class="field-error" id="documents-error" role="alert" hidden></p>
<button type="submit">Upload</button>
<p id="status" role="status" aria-live="polite"></p>
</form>
</main>
<script>
const form = document.querySelector('#upload-form');
const input = document.querySelector('#documents');
const count = document.querySelector('#file-count');
const error = document.querySelector('#documents-error');
const status = document.querySelector('#status');
const MAX_BYTES = 5 * 1024 * 1024;
function showError(message) {
error.textContent = message;
error.hidden = false;
input.setAttribute('aria-invalid', 'true');
}
function clearError() {
error.textContent = '';
error.hidden = true;
input.removeAttribute('aria-invalid');
}
input.addEventListener('change', () => {
clearError();
const files = [...input.files];
if (files.length === 0) {
count.textContent = 'No files selected';
return;
}
const tooLarge = files.filter((file) => file.size > MAX_BYTES).map((file) => file.name);
if (tooLarge.length > 0) {
showError('These files exceed 5 MB: ' + tooLarge.join(', ') + '. Remove them before uploading.');
}
const names = files.map((file) => file.name).join(', ');
count.textContent = files.length + ' file' + (files.length === 1 ? '' : 's') + ' selected: ' + names;
});
form.addEventListener('submit', (event) => {
event.preventDefault();
const files = [...input.files];
if (files.length === 0) {
showError('Choose at least one file to upload.');
input.focus();
return;
}
if (files.some((file) => file.size > MAX_BYTES)) {
showError('Remove files larger than 5 MB before uploading.');
input.focus();
return;
}
status.textContent = files.length + ' file(s) ready to upload. This demo does not send data.';
});
</script>
</body>
</html>Report a problem with this exampleOpens a pre-filled issue on GitHub.