Skip to content

File picker

The richer sibling of .re-file: a styled drop / browse area built by visually hiding the native <input type="file"> (which stays the form value) and driving a custom surface. Click-to-pick works with zero JavaScript — the <label> opens the OS picker. The optional enhanceFilePicker behavior (or the <re-file-picker> element) layers on the filename readout, drag-and-drop, clear, and validation.

Reach for .re-file when you just want the native button styled; reach for this when you want an icon, a drop zone, or a custom prompt.

Live example
<div class="re-file-picker" data-re-file-picker>
  <label class="re-file-picker__field">
    <input
      type="file"
      class="re-file-picker__input"
      name="docs"
      aria-label="Upload documents"
    />
    <span class="re-file-picker__ui">
      <svg
        class="re-file-picker__icon"
        aria-hidden="true"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
        <polyline points="17 8 12 3 7 8" />
        <line x1="12" y1="3" x2="12" y2="15" />
      </svg>
      <span class="re-file-picker__prompt">Choose a file or drag it here</span>
    </span>
  </label>
  <p class="re-file-picker__list" hidden></p>
  <button
    class="re-file-picker__clear"
    data-re-file-clear
    hidden
    type="button"
    aria-label="Clear selection"
  >
    Clear
  </button>
  <span class="re-file-picker__status re-sr-only" role="status" aria-live="polite"></span>
</div>
Live example
<div class="stack">
  <div class="re-file-picker" data-size="sm">
    <label class="re-file-picker__field">
      <input type="file" class="re-file-picker__input" aria-label="Small picker" />
      <span class="re-file-picker__ui">
        <span class="re-file-picker__prompt">Choose a file</span>
      </span>
    </label>
  </div>
  <div class="re-file-picker" data-size="lg">
    <label class="re-file-picker__field">
      <input type="file" class="re-file-picker__input" aria-label="Large picker" />
      <span class="re-file-picker__ui">
        <span class="re-file-picker__prompt">Choose a file or drag it here</span>
      </span>
    </label>
  </div>
</div>
Live example
<div class="stack">
  <div class="re-file-picker">
    <label class="re-file-picker__field">
      <input
        type="file"
        class="re-file-picker__input"
        aria-label="Invalid picker"
        aria-invalid="true"
      />
      <span class="re-file-picker__ui">
        <span class="re-file-picker__prompt">Choose a file</span>
      </span>
    </label>
  </div>
  <div class="re-file-picker">
    <label class="re-file-picker__field">
      <input
        type="file"
        class="re-file-picker__input"
        aria-label="Disabled picker"
        disabled
      />
      <span class="re-file-picker__ui">
        <span class="re-file-picker__prompt">Choose a file</span>
      </span>
    </label>
  </div>
</div>

enhanceFilePicker enforces accept, data-re-file-max-files, and data-re-file-max-size on dropped files (the OS picker already filters by accept); a rejected set sets aria-invalid and emits a re-error event with { reason, rejected, accepted }. Client checks are a convenience — the server is the only trust boundary.

Live example
<div
  class="re-file-picker"
  data-re-file-picker
  data-re-file-max-files="1"
  data-re-file-max-size="1000000"
>
  <label class="re-file-picker__field">
    <input
      type="file"
      class="re-file-picker__input"
      name="image"
      accept="image/*"
      multiple
      aria-label="Upload one image"
    />
    <span class="re-file-picker__ui">
      <span class="re-file-picker__prompt">One image, up to 1&nbsp;MB</span>
    </span>
  </label>
  <p class="re-file-picker__list" hidden></p>
  <button
    class="re-file-picker__clear"
    data-re-file-clear
    hidden
    type="button"
    aria-label="Clear selection"
  >
    Clear
  </button>
  <span class="re-file-picker__status re-sr-only" role="status" aria-live="polite"></span>
</div>
  • Keyboard — fully native. Tab moves focus to the (visually hidden but focusable) input; Space / Enter opens the OS picker. Drag-and-drop is a pointer-only enhancement (WCAG 2.5.7) — the picker is always the keyboard path, so nothing is keyboard-inaccessible.
  • Focus — the input is clipped offscreen, so the visible :focus-visible ring is drawn on __ui via :focus-within (and re-established as a Highlight outline under forced colors, where the box-shadow ring is stripped). The input’s own ring is suppressed so it can’t ghost.
  • Name vs. instruction — give the input an accessible name with aria-label (or an associated .re-field label) that says what to upload (“Resume”); the visible __prompt is the instruction (“Choose a file or drag it here”), not the name.
  • Selection announced — without the behavior the picker still works, but the chosen file isn’t echoed (the native text is on the hidden input). enhanceFilePicker writes the filenames into __list (visible) and announces a summary through the sr-only role="status" __status region — so the selection reaches assistive tech.
  • Clear — the __clear button is a real <button> placed outside the <label> (inside, every click would reopen the picker); it’s authored hidden and only revealed by the behavior, with an aria-label.
  • Notes[data-dragover] and [data-has-files] use distinct system colors under forced colors so the two states stay distinguishable; hover uses --re-color-bg-muted so it survives dark mode. See the accessibility guide.