splitforms.com
← Back to the journal

How to Style input type=file (Custom Upload Button, CSS & Tailwind)

Style a file input button with CSS or Tailwind, change its label accessibly, show selected filenames and add drag and drop or an image preview.

Start free — 500 submissionsSee pricing →No card, no time limit. Pro is $5 per month.
Short answer
Use ::file-selector-button to style the native button. For custom text, visually hide the input, style its label and show selected filenames with a change listener.
Try it on your own form

Point any HTML form at one endpoint. 500 free submissions — no card, no time limit — and unlimited forms.

Create free account

Why the native file input is hard to style

A file input is a browser-controlled picker, not a normal text box and button. Its button can be styled with a pseudo-element, but the browser controls the default button wording and the accompanying "No file chosen" text. See the file input reference for the underlying HTML.

Style the button with CSS

Use the standard selector in current browsers. Put the older WebKit rule separately first so an unsupported selector cannot invalidate the standard rule.

input[type=file]::-webkit-file-upload-button {
  border: 0; padding: .5rem 1rem; background: #e0e7ff; cursor: pointer;
}
input[type=file]::file-selector-button {
  border: 0; border-radius: .25rem; padding: .5rem 1rem;
  margin-right: 1rem; background: #e0e7ff; cursor: pointer;
}
input[type=file]::file-selector-button:hover { background: #c7d2fe; }
input[type=file]::-webkit-file-upload-button:hover { background: #c7d2fe; }

Change the button text with a label

For complete control over the text, visually hide the input and style its associated label. The input stays in the accessibility tree and tab order. Its focus indicator appears on the adjacent label; clicking the label opens the picker.

<input class="file-input" type="file" id="upload" name="files" multiple>
<label class="upload-button" for="upload">Select files</label>
<p id="file-status" aria-live="polite">No files selected</p>

<style>
.file-input {
  position: absolute; width: 1px; height: 1px; padding: 0;
  margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0);
  clip-path: inset(50%); white-space: nowrap; border: 0;
}
.upload-button { display: inline-block; padding: .5rem 1rem; background: #e0e7ff; cursor: pointer; }
.file-input:focus-visible + label { outline: 3px solid #4338ca; outline-offset: 3px; }
</style>

Show selected file names and sizes

Put this code in a <script> after the markup above. It handles one or multiple files and uses textContent so filenames are not interpreted as HTML.

const input = document.querySelector('#upload');
const status = document.querySelector('#file-status');
input.addEventListener('change', () => {
  const files = Array.from(input.files);
  const names = files.map(file =>
    file.name + ' (' + (file.size / 1024).toFixed(1) + ' KiB)'
  );
  status.textContent = names.length ? names.join(', ') : 'No files selected';
});

Add a drag-and-drop zone

Wrap the input, label and status above in <div id="drop-zone">. Keep the label so keyboard and touch users can open the picker. A drop replaces the current selection and dispatches change to update the status.

<style>#drop-zone { padding: 1rem; border: 2px dashed #888; }
#drop-zone.highlight { border-color: #4338ca; background: #eef2ff; }</style>
<script>
const zone = document.querySelector('#drop-zone');
zone.addEventListener('dragover', e => {
  e.preventDefault();
  zone.classList.add('highlight');
});
zone.addEventListener('dragleave', e => {
  if (!zone.contains(e.relatedTarget)) zone.classList.remove('highlight');
});
zone.addEventListener('drop', e => {
  e.preventDefault();
  zone.classList.remove('highlight');
  if (!e.dataTransfer.files.length) return;
  input.files = e.dataTransfer.files;
  input.dispatchEvent(new Event('change', { bubbles: true }));
});
</script>

Use multiple for multiple dropped files. The accept attribute guides the picker but does not validate dropped files; check type and size on the server too. See file size validation.

Preview an image

This independent example previews the first selected image. Revoke the old object URL when the selection changes or the page is left.

<label for="image">Choose an image</label>
<input id="image" type="file" accept="image/*">
<img id="preview" alt="Selected image preview" hidden>
<script>
const imageInput = document.querySelector('#image');
const preview = document.querySelector('#preview');
let previewUrl = null;
imageInput.addEventListener('change', () => {
  if (previewUrl) URL.revokeObjectURL(previewUrl);
  previewUrl = null;
  preview.hidden = true;
  preview.removeAttribute('src');
  const file = imageInput.files[0];
  if (file && file.type.startsWith('image/')) {
    previewUrl = URL.createObjectURL(file);
    preview.src = previewUrl;
    preview.hidden = false;
  }
});
window.addEventListener('pagehide', () => {
  if (previewUrl) URL.revokeObjectURL(previewUrl);
});
</script>

Tailwind file button classes

Tailwind's file: variant targets the native button:

<input type="file" name="attachment"
  class="file:mr-4 file:rounded file:border-0 file:bg-indigo-50 file:px-4 file:py-2 hover:file:bg-indigo-100">

A complete upload form

Replace the access key with your own splitforms key. The POST and multipart encoding send the file contents along with the other fields.

<form action="https://splitforms.com/api/submit" method="POST" enctype="multipart/form-data">
  <input type="hidden" name="access_key" value="YOUR_ACCESS_KEY">
  <label for="email">Email</label>
  <input id="email" type="email" name="email" required>
  <label for="attachment">Attachment</label>
  <input id="attachment" type="file" name="attachment" required>
  <button type="submit">Send</button>
</form>

File storage requires a paid plan with Storage connected; on Free, text fields are saved but files are dropped. The limit is five files of 5 MB each per submission. For setup and server-side checks, see how to add file uploads to a contact form.

FAQ

Can I change the No file chosen text?

The native text is controlled by the browser and cannot be changed with CSS. Visually hide the input, use a label as the button, and show your own status text updated from input.files.

Why does display: none break keyboard access?

An input with display: none is removed from keyboard navigation and the accessibility tree. Visually hide it instead, keep it focusable, and show a focus indicator on its label.

How do I reset a file input?

Set input.value to an empty string or call form.reset(). Also clear any custom filename text and revoke any preview object URL; resetting does not fire a change event.

Can I set a file input value?

You can clear value with an empty string, but cannot set it to a local file path. Users must choose files or drop them; a FileList from a drop can be assigned to input.files.

Related articles

More practical guidance from tutorials.

Browse the journal →
Tutorials

How to Remove the Arrows From a Number Input (CSS, Tailwind, React)

Hide the number input spin buttons with ::-webkit-inner-spin-button and appearance: textfiel…

7 min readRead →
Tutorials

How to Change the Autofill Background Color of an Input (CSS, Tailwind)

Remove Chrome's yellow autofill background with input:-webkit-autofill, inset box-shadow, -w…

7 min readRead →
Tutorials

input type=number maxlength Not Working? 4 Fixes That Work

Why maxlength is ignored on input type=number, plus four fixes: min/max, oninput slice, text…

8 min readRead →

Explore this topic

Start with the overview, then move into focused guides.

OverviewContact Forms for Every FrameworkStart here →GuideContact Form for Next.jsRead →GuideContact Form for ReactRead →GuideContact Form for VueRead →ReferenceContact form guideOpen →

Building forms with ChatGPT, Claude, Cursor, or v0? Connect the native MCP server and give your agent a production form backend.

Explore the MCP server →

Give your form a production backend.

One endpoint adds delivery, spam filtering, storage, and integrations. Start with 500 free submissions — no card, no time limit.

Create free accountRead the docs →
Secure checkoutSSL encryptionPrivacyProtected
VISAAMERICANEXPRESSstripe