Home / Snippets / UI Components /

File upload

A drag-and-drop-style upload zone built with a hidden file input and a styled label — fully accessible, zero JavaScript.

Widely Supported
uino-js

Quick implementation

.file-upload {
  position: relative;
}

/* Hide the native input visually — keep it accessible */
.file-upload input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.file-upload label {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 2.5rem 2rem;
  border: 2px dashed oklch(0.38 0.04 260);
  border-radius: 0.75rem;
  background: oklch(0.17 0.02 260);
  color: oklch(0.63 0.02 260); /* --muted */
  font-size: 0.9rem;
  cursor: pointer;
  transition: border-color 0.2s ease-out,
              background 0.2s ease-out,
              color 0.2s ease-out;
  text-align: center;
}

.file-upload label:hover {
  border-color: oklch(0.72 0.19 265);
  background: oklch(0.52 0.22 265 / 0.06);
  color: oklch(0.93 0.01 260); /* --text */
}

/* Keyboard focus on the hidden input → style the label */
.file-upload input[type="file"]:focus-visible + label {
  border-color: oklch(0.72 0.19 265);
  outline: 2px solid oklch(0.72 0.19 265);
  outline-offset: 2px;
  background: oklch(0.52 0.22 265 / 0.06);
}

Prompt this to your LLM

Includes role, constraints, hidden-input technique, focus forwarding, and drag-and-drop enhancement notes.

You are a senior frontend engineer building a form component library.

Goal: A styled file upload dropzone using a hidden <input type="file"> paired with a visible <label>.

Technical constraints:
- The <input type="file"> must be visually hidden but NOT display:none or visibility:hidden — use the accessible clip pattern (position: absolute; width: 1px; height: 1px; opacity: 0; clip: rect(0 0 0 0)).
- The label must be the clickable, visible target — pair them with a matching id/for.
- Use oklch() for all colors — no hex, no rgba().
- Dropzone border: 2px dashed oklch(0.38 0.04 260).
- Background: oklch(0.17 0.02 260) — slightly lighter than page bg.
- Border-radius: 0.75rem.
- Padding: 2.5rem 2rem — generous space for an icon + text.
- Hover: border-color → oklch(0.72 0.19 265), background → oklch(0.52 0.22 265 / 0.06).
- Keyboard focus: use input:focus-visible + label selector to show focus ring on label when input is focused.
- Include a file icon (inline SVG), a "Choose file or drag here" cta, and a hint line for accepted formats.

Framework variants (pick one):
A) Plain HTML/CSS — static markup, CSS-only hover and focus states.
B) React component — add dragover/dragleave/drop handlers to set an :is(.dragging) class on the wrapper for enhanced visual feedback.

Edge cases to handle:
- The input must remain accessible — screen readers must be able to announce and activate it.
- Tab key must reach the input and trigger the file picker on Enter/Space.
- The zone must not break at narrow widths — use flex column layout.
- Accept attribute can restrict file types — pass it through as a prop in the React variant.

Return CSS only (and minimal HTML structure as a comment).

Why this matters in 2026

Custom file upload UIs are almost always over-built: hidden inputs replaced by JavaScript-driven dropzones with complex drag-event handlers. For the majority of use cases — a single file picker with styled feedback — a hidden native input paired with a CSS-styled label achieves the same visual result. The native input handles the file picker dialog, OS-level drag-and-drop in most browsers, keyboard activation, and form serialisation, all with zero JavaScript.

The logic

The <input type="file"> is visually hidden using the accessible clip technique — unlike display: none, the element remains in the tab order and is announced by screen readers. The <label for="..."> forwards all click and keyboard activation events to the hidden input, making the entire styled zone a functional file picker trigger. The input:focus-visible + label adjacent sibling selector applies the focus ring to the visible label when the invisible input receives keyboard focus.

Accessibility & performance

Screen readers announce the label text as the accessible name of the file input. The accept attribute filters files at the OS picker level and should be paired with server-side validation. The focus ring uses :focus-visible to appear only for keyboard users — the adjacent sibling combinator (+) makes this work even though the ring is applied to the label, not the input itself. This pattern requires the label to immediately follow the input in source order.