albert.walickihire me
← all solutions
css

How to style a file input?

Style the file input button with the file-selector-button pseudo-element, or build a fully custom upload button from a label that stays keyboard accessible.

Style the button part of the input with the ::file-selector-button pseudo-element. It works in all modern browsers, and the input keeps its native behavior, including keyboard access and the chosen file name.

<label for="resume">Upload your CV</label>
<input class="file-input" type="file" id="resume" name="resume">
.file-input {
  font: inherit;
  color: #4b5563;
}

.file-input::file-selector-button {
  margin-right: 12px;
  padding: 8px 16px;
  border: 0;
  border-radius: 6px;
  background: #2563eb;
  color: #fff;
  font: inherit;
  cursor: pointer;
}

.file-input::file-selector-button:hover {
  background: #1d4ed8;
}

.file-input:focus-visible {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

The rest of the input, the "No file chosen" text, is styled through the input itself with font and color. You can't change that text, and it's shown in the browser's language. ::file-selector-button replaced the old ::-webkit-file-upload-button, which you don't need anymore.

A fully custom upload button

For a completely different look, like a button with an icon, hide the input visually and style its <label> as the button. Clicking a label activates its input, so the file picker opens.

<div class="upload">
  <input type="file" id="avatar" name="avatar"
    class="visually-hidden" accept="image/*">
  <label for="avatar" class="upload-button">Choose a photo</label>
  <span class="upload-name">No file chosen</span>
</div>
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.upload {
  position: relative;
}

.upload-button {
  display: inline-block;
  padding: 8px 16px;
  border-radius: 6px;
  background: #2563eb;
  color: #fff;
  cursor: pointer;
}

.upload-button:hover {
  background: #1d4ed8;
}

.visually-hidden:focus-visible + .upload-button {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

Don't hide the input with display: none or visibility: hidden. Both remove it from the tab order, so keyboard users can't upload anything. The visually hidden input is still there: Tab focuses it, the keyboard opens the picker, and screen readers announce it with the label's text.

Because keyboard focus lands on an invisible input, the :focus-visible + .upload-button rule is what shows keyboard users where they are. The input has to come right before the label in the HTML for the + selector to work.

position: relative on the wrapper keeps the hidden input next to the button. If you add required, the browser's validation message then points at the right place.

Show the chosen file name

The native input shows the file name for you. With a hidden input, you have to do it yourself:

const input = document.querySelector('#avatar');
const fileName = document.querySelector('.upload-name');

input.addEventListener('change', () => {
  const file = input.files[0];
  fileName.textContent = file ? file.name : 'No file chosen';
});

input.files can be empty, so keep the fallback text. With the multiple attribute, show something like ${input.files.length} files instead of one name.

To limit the picker to images or PDFs, see how to accept only certain file types in a file input.

more solutions
work with me

Got something that needs building?

Frontend builds, full-stack features in Django, design-system work. Available for work.

See my workGet in touch