Styling File Inputs — The Best Way

1 Comment

Join the Conversation

I’ve spent countless hours styling file inputs using a number of different techniques out there. End result is always the same — frustration. A common (IMO worst) technique to style file inputs is faking buttons with extra, non-semantic HTML, CSS and JS. Dirty, dirty, dirty, not to mention the drawbacks for usability and touch. Don’t fret! There is a better way.

See the Pen Beautiful CSS-Only File Inputs by Ben Marshall (@bmarshall511) on CodePen.

View Demo View Code

Styling clean, semantic and accessible upload buttons requires two things: CSS & . I’ll go over the technique and show how a little extra JS (optional) can enhance the UX. The input file CSS possibilities are limitless once you understand how this technique works!

CSS File Input

Trying and failing to style a <input type="file" /> control is a twisted rite of passage in the web dev world. There’s a (very) long thread on Stack Overflow dedicated to it. Problem is every browser has a different way of implementing <input type="file" /> and few can be touched with CSS.

Styling File Inputs

This is a great technique to style file inputs since pressing a <label> triggers the focus event for the bound input. Since we’re dealing with file inputs, it works out as a click event. This results with the file browser pop-up and is a great semantic solution! No JavaScript, no other complex solutions like cursor position tracking, just these two lines.

Firstly, hide the native button.

Bet you’re thinking display: none or visibility: hidden? Nope. Those won’t work because the input value won’t be sent to the server when submitted. It’ll also be excluded out of the tab order making it no longer accessible. In order to keep it accessible, use this CSS:

[type="file"] {
  border: 0;
  clip: rect(0, 0, 0, 0);
  height: 1px;
  overflow: hidden;
  padding: 0;
  position: absolute !important;
  white-space: nowrap;
  width: 1px;  

Why 1x1px? Setting property values to zero ends up throwing the element out of tab order in some browsers. position: absolute guarantees the element does not interfere with the sibling elements. Don’t forget when styling file inputs, accessibility is an important factor.

Now the fun part.

With this technique to style file inputs, we’ll use the <label> element as the upload button. From there, use your creative CSS juices on it! Here’s a basic example:

[type="file"] + label {
  background-color: #000;
  border-radius: 4rem;
  color: #fff;
  cursor: pointer;
  display: inline-block;
  font-family: 'Poppins', sans-serif;
  font-size: 1rem;
  font-weight: 700;
  height: 4rem;
  line-height: 4rem;
  padding-left: 2rem;
  padding-right: 2rem;
  transition: background-color 0.3s;

[type="file"]:focus + label,
[type="file"] + label:hover {
    background-color: #f15d22;

[type="file"]:focus + label {
  outline: 1px dotted #000;
  outline: -webkit-focus-ring-color auto 5px;

The accessible part.

How do you know that an element on the website is accessible? Firstly, the element should communicate a feeling that you can tap or click on it. Secondly, the cursor icon should change to an appropriate one when hovering the element. Both of which we’ve solved above.

Another important part to accessibility is keyboard navigation. If users are unable to navigate on your website using just a keyboard, you are doing something wrong. Hiding the input itself in a correct manner was one thing, the other is indicating when the element is focused, i.e. rendering [type="file"]:focus on the label. That is also taken care of with the code above!

-webkit-focus-ring-color auto 5px is a little trick for obtaining default outline looks on Chrome, Opera and Safari. The style in the line above is for browsers that do not understand the -webkit… expression.

Touch Issues with FastClick

In case you’ve been using FastClick (a library for eliminating the 300ms tap-pause on touch-capable devices) and have plans to add some extra markup to the content of a label, the button won’t work as it should, unless you use pointer-events: none, respectively:

[type="file"] + label * {
  pointer-events: none;

File Selection Indication Fix

Lastly, we need to add a little functionality to indicate which files were selected. Since we’re hiding the native file input, the user doesn’t have a way to tell that a file was selected. Don’t fret, there’s a fix with a small JavaScript enhancement. When the user selects a file, the text of a label will become the name of the selected file. If there were multiple files selected, the text will tell us how many of them were selected.

<input type="file" name="file" id="file" data-multiple-caption="{count} files selected" multiple />
var inputs = document.querySelectorAll( '.inputfile' ); inputs, function( input ) {
  var label = input.nextElementSibling,
              labelVal = label.innerHTML;

  input.addEventListener( 'change', function( e ) {
    var fileName = '';
    if ( this.files &amp;&amp; this.files.length > 1 ) {
      fileName = ( this.getAttribute( 'data-multiple-caption' ) || '' ).replace( '{count}', this.files.length );
    } else {
      fileName = '\\' ).pop();

    if ( fileName ) {
      label.querySelector( 'span' ).innerHTML = fileName;
    } else {
      label.innerHTML = labelVal;

There is also a jQuery version of this code presented you can download here.

Having the native [multiple] attribute allows users to select more than one file per upload. Whereas [data-multiple-caption] is a fictive attribute for expressing the message if multiple files were selected. Here you can set a custom message. The use of the {count} phrase is optional and the fragment is replaced with the number of files selected.

[multiple] is not supported in IE 9 and below and neither is the files property of JavaScript. For the latter case, we simply rely on value. Since it usually has a value of C:\fakepath\filename.jpg format, the split( '\\' ).pop() extracts what’s actual — the name of the file.

An interesting thing is that you can unset a value of the input by pressing the ESC button while in the file browser. This is possible only in Chrome and Opera. Therefore, we use labelVal for storing the default value of the label and bringing it back when necessary.

No JavaScript, no problem!

Since there is no JavaScript-less way to indicate if any files were selected, it would be better to rely on the default looks of the file input for the sake of usability. All we need to do is to add a .no-js class name to the <html> element and then use JavaScript and replace it with .js — that’s how we will know if JavaScript is available. Many frameworks like Foundation and Bootstrap already do this.

<html class="no-js">
    <!-- remove this if you use Modernizr -->
    <script>(function(e,t,n){var r=e.querySelectorAll("html")[0];r.className=r.className.replace(/(^|\s)no-js(\s|$)/,"$1js$2")})(document,window,0);</script>
.js [type="file"] {
  border: 0;
  clip: rect(0, 0, 0, 0);
  height: 1px;
  overflow: hidden;
  padding: 0;
  position: absolute !important;
  white-space: nowrap;
  width: 1px; 

.no-js [type="file"] + label {
  display: none;

Known Firefox Bug

It is quite unexpected that Firefox completely ignores the input[type="file"]:focus expression, whereas :hover and :active work just fine! Surprisingly, Firefox allows to catch the focus event in JavaScript, so the workaround is adding a class to the file input element that let’s us control the focus style:

input.addEventListener( 'focus', function(){ input.classList.add( 'has-focus' ); });
input.addEventListener( 'blur', function(){ input.classList.remove( 'has-focus' ); });
[type="file"]:focus + label,
[type="file"].has-focus + label {
  outline: 1px dotted #000;
  outline: -webkit-focus-ring-color auto 5px;

Using a CSS framework?

Many popular CSS frameworks like Foundation already have a way to customize upload buttons. Check out the examples below.

Styling File Inputs with Foundation

Customizing the file upload button in Foundation uses the same technique above. To hide the native upload button it uses the visibility class for screen readers (.show-for-sr).

Use Foundation’s settings file to update variables to customize the button to your liking. The HTML structure follows the same pattern shown above:

<label for="file" class="button">Upload File</label>
<input type="file" id="file" class="show-for-sr" />

Styling File Inputs with Bootstrap

Bootstrap’s method for styling file inputs isn’t as clean or optimized as this technique or Foundations’ — just another reason I prefer Foundation over Bootstrap. It requires additional JavaScript in order for it to override the native upload button.

<div class="custom-file">
  <input type="file" class="custom-file-input" id="file">
  <label class="custom-file-label" for="file">Choose file</label>

This method may not be as clean, but it does provide a little more flexibility for translations however.

$custom-file-text: (
  en: "Browse",
  es: "Elegir"
<div class="custom-file">
  <input type="file" class="custom-file-input" id="file" lang="es">
  <label class="custom-file-label" for="file">Seleccionar Archivo</label>

The :lang() pseudo-class is used to allow for translation of the “Browse” text into other languages. Override or add entries to the $custom-file-text Sass variable with the relevant language tag and localized strings. English strings can be customized the same way.

Styling File Input Alternatives

Like with most things in web development, there’s more than one way to skin a cat. Here’s some other popular techniques to style file inputs:

Wrapping Up

Styling file inputs is a problem web developers have been trying to solve for years. With this workaround we finally have a solid, semantic and accessible solution.


  1. Christie
    at 6:53 am

    This is awesome, thanks for the post 🙂 However, I have a bunch of dynamically created upload buttons and when I use this with them, only the first instance of the upload field changes. I’m not super comfortable with Javascript, so how would I iterate through an array of inputs and have them each update with the name of the file that’s uploaded? I hope I am making sense here.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

All comments are held for moderation. I'll publish all comments that are on topic and not rude. You'll even get little stars if you do an extra good job.

You may write comments in Markdown. This is the best way to post any code, inline like `<div>this</div>` or multiline blocks within triple backtick fences (```) with double new lines before and after.

Want to tell me something privately, like pointing out a typo or stuff like that? Contact Me.

%d bloggers like this: