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.

HtmlFile Upload

Code

snippet
<!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>