功能说明
表单要求上传 PDF 或 PNG,支持多选,每个文件不超过 5MB。原生 input type=“file” 被视觉隐藏但仍可通过键盘聚焦,旁边的 label 被样式化为“选择文件”按钮。选择后脚本统计文件数量与名称,写入 aria-live 区域;若有超大文件,错误段落以 role=“alert” 显示并设置 aria-invalid。提交时再次检查空选择与大小。
关键代码
隐藏输入使用 1px 尺寸与 clip 技巧,而不是 display:none,否则会失去键盘焦点和辅助技术访问。input:focus-visible + .file-button 把焦点环画在可见标签上,因为输入本身不可见。accept 限制选择器过滤类型,multiple 允许一次选多个。aria-describedby 同时关联帮助文本与错误段落,使读屏器在聚焦时能读到要求。文件列表通过 textContent 写入,避免文件名被当作 HTML。
使用注意
accept 只是选择器提示,用户仍可切换“所有文件”,服务端必须校验 MIME 类型与文件内容。客户端大小检查可被绕过,不能替代服务端限制。示例不会真正发送数据,提交只更新状态文本。文件名可能很长或包含恶意字符,展示时应截断并转义。若使用拖放区域,还需提供键盘可操作的浏览按钮。
验证方式
用 Tab 聚焦文件选择,确认可见按钮出现焦点环,按回车或空格打开系统选择器。选择两个文件后,计数区域应读出数量与名称。选择一个超过 5MB 的文件,错误应立即出现且输入被标记无效;移除该文件后错误消失。空表单提交应聚焦文件输入并提示至少选择一个文件。
代码示例
<!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>