webdevpuneet.com

Custom File Input — Styled Upload Field Snippet

Custom File Input · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="field">
  <span class="field-label">Attach a file</span>

  <label class="file">
    <input type="file" class="file-native" accept=".pdf,.png,.jpg,.jpeg,.doc,.docx">
    <span class="file-btn">
      <svg viewBox="0 0 24 24"><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>
      Choose file
    </span>
    <span class="file-name">No file selected</span>
    <button type="button" class="file-clear" aria-label="Remove file" hidden>
      <svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
    </button>
  </label>

  <p class="file-hint">PDF, Word, or image · up to 10MB</p>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Styled "Choose file" button replacing the unstylable native control
Shows the selected file name and a human-readable size (B/KB/MB)
Clear (×) button resets the field; calls preventDefault so it won't re-open the picker
Real <input type="file"> kept (hidden via opacity) — uploads and keyboard work
Whole control is a <label>, so clicking anywhere opens the OS file picker
accept attribute guides the picker; focus-within ring for keyboard users
Long filenames truncate with an ellipsis instead of breaking layout
Submits natively with the form under its name attribute
Ready to extend for image previews or multiple files
Export as HTML file, React JSX, or React + Tailwind CSS

About this UI Snippet

Custom File Input — Styled Button, File Name + Size & Clear Control

Screenshot of the Custom File Input snippet rendered live

A custom file input is one of the most-searched form snippets because the default <input type="file"> is notoriously ugly and unstylable — its button text and appearance are controlled by the browser and OS, and look different everywhere. For a drag-and-drop area instead, see the file dropzone; for upload feedback, the upload progress card. This snippet replaces it with a clean, branded control that shows a "Choose file" button, the selected file's name and size, and a clear button to remove it — while keeping a real, accessible <input type="file"> underneath so uploads and keyboard support keep working.

The accessible hide-and-replace pattern

The trick to styling a file input without breaking it is to keep the native input and hide it visually rather than replace it. The <input type="file"> is wrapped in a <label> and hidden with position: absolute; width: 1px; height: 1px; opacity: 0 — not display: none, which would remove it from the tab order and from assistive tech. Because the whole control is a <label>, clicking anywhere on it (the styled button, the filename area) opens the OS file picker natively. This is the same reliable pattern used for the accessible custom checkbox, applied to file inputs: the browser handles the file dialog, validation, and form submission; CSS only handles appearance.

Showing the file name and size

When the user picks a file, a single change listener reads input.files[0] and writes the file's name and a human-readable size into the .file-name element ("report.pdf · 248 KB"). The fmtSize helper converts the raw byte count into B, KB, or MB so the size is readable. The filename area uses text-overflow: ellipsis with overflow: hidden so a very long filename truncates gracefully instead of breaking the layout. A .has-file class on the wrapper darkens the text and bolds it so a chosen file looks visibly different from the "No file selected" placeholder.

The clear button

Once a file is selected, a clear (×) button appears. Clicking it resets the input value (input.value = ''), restores the placeholder text, removes the .has-file class, and hides itself again. One important detail: the clear button calls e.preventDefault() because it lives inside the <label> — without it, clicking the × would also trigger the label and re-open the file picker. This is exactly the kind of subtlety that breaks naive custom file inputs, and handling it is what makes this control feel solid. Resetting input.value is the correct way to clear a file input; you cannot set its value to a filename for security reasons, but you can clear it.

Restricting accepted file types

The native accept attribute (.pdf,.png,.jpg,.jpeg,.doc,.docx) tells the OS file picker to highlight matching file types, which guides users toward valid selections. Note that accept is a hint, not enforcement — users can still override it — so always validate the file type and size on the client (in the change handler) and again on the server. The hint line under the control ("PDF, Word, or image · up to 10MB") sets expectations before the user opens the dialog.

The focus and hover states

Because the native input is preserved, keyboard focus lands on it, and .file:focus-within styles the whole control with the brand border and a focus ring — using :focus-within so the ring appears when the hidden input is focused via keyboard. The control also has a hover state. These make the custom input feel like a first-class form field rather than a static button, and keep it operable for keyboard users who Tab to it and press Enter/Space to open the picker.

Customizing the control

Re-theme by changing the "Choose file" button color, the focus ring, and the clear-button hover (red here). Swap the upload and × SVGs for your own icons. Change the accept attribute to your allowed types and update the hint text to match. To show a thumbnail preview for images, read the file with URL.createObjectURL(file) in the change handler and set it as an <img> src (remember to revokeObjectURL when clearing). To accept multiple files, add the multiple attribute and render a small list of names instead of one. The structure — label, hidden input, button, name, clear — stays the same.

Why keep the native input

A common but broken approach builds a "file input" from a styled <button> plus JavaScript, which cannot actually open the OS file dialog without a hidden real input anyway, and risks losing keyboard support and form submission. Keeping the genuine <input type="file"> means the file is included in a normal form submit under its name, the OS file picker (including drag-from-desktop and recent files) works, screen readers announce it as a file upload, and it is keyboard-operable — all for free. The custom look is purely cosmetic CSS layered on top of a fully functional field.

Step by step

How to Use

  1. 1
    Copy the controlCopy the <label class="file"> with its hidden <input type="file">, the .file-btn, .file-name, and .file-clear button, plus the small script.
  2. 2
    Keep the native inputThe real <input type="file"> stays (hidden via opacity, not display:none) so uploads, keyboard, and form submission work. The whole label opens the picker.
  3. 3
    Set accepted typesChange the accept attribute to your allowed file types and update the hint text. Validate type and size in the change handler and on the server too.
  4. 4
    Add an image preview (optional)In the change handler, use URL.createObjectURL(file) as an <img> src to show a thumbnail; revoke it when clearing.
  5. 5
    Re-theme itChange the Choose-file button color, the focus ring, and the clear-button hover to match your form.
  6. 6
    Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.

Real-world uses

Common Use Cases

Upload fields in forms
Resume, document, and attachment uploads where the default file input clashes with the rest of the form design.
Profile and settings uploads
Avatar, logo, or document uploads in account settings with a clear filename readout and a one-click remove.
Image upload with preview
Extend the change handler with URL.createObjectURL to show a thumbnail of the chosen image before submitting.
Learn the styled-file-input trick
See how hiding the native input (not display:none) and wrapping it in a label gives a fully custom look without losing function.
Reusable upload component
Drop the label/input/name/clear structure into your design system as the standard file field across the app.
Accessible file upload
Keeps the native file input so the OS picker, keyboard activation, screen-reader announcement, and form submission all work.
Related: Loan EMI Calculator
See the Loan EMI Calculator for a related forms pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Keep the real <input type="file"> and hide it with position: absolute; width/height: 1px; opacity: 0 (never display: none, which removes it from the tab order). Wrap everything in a <label> so clicking the styled button opens the OS picker, and use a change listener to show the file name. The native input still handles uploads, keyboard, and submission.

In a change event listener, read input.files[0] and write its name and a formatted size into your label element. A small helper converts the byte count into KB or MB. A class on the wrapper styles the chosen-file state differently from the placeholder.

The clear button is inside the <label>, so a normal click would also activate the label and re-open the file picker. Calling e.preventDefault() stops that, so clicking × only clears the field. Clearing is done by setting input.value = "".

Set the accept attribute (e.g. accept=".pdf,.png,.jpg") to guide the OS picker toward matching types. It is a hint, not enforcement, so also validate the type and size in the change handler and again on the server.

In the change handler, call URL.createObjectURL(file) and set it as the src of an <img>. Revoke the object URL with URL.revokeObjectURL when the file is cleared or replaced to free memory.

Yes. Click "JSX" for a React component or "Tailwind" for a React + Tailwind version. In React, keep a ref or controlled state for the chosen file, render the name/size from it, and clear by resetting the input via a ref; the label/hidden-input structure is unchanged.