Styling & Customizing File Inputs the Smart Way

A tutorial on how to style and customize <input type=”file”> in a semantic, accessible way using the <label> element and some JavaScript.

There are quite a few techniques for “customizing” the <input type="file" /> element. I tried most of them, but none was good enough to have on Readerrr (for importing feeds by uploading a file). Probably the worst technique was the one where the input element is put into a container (which imitates a button), and the input follows the cursor so that when you click anywhere on the container, you actually click the input. Sounds interesting and weird at the same time, right? Anyway, it had some unacceptable drawbacks (usability, touch).

As as result, I tried googling for an unseen solution. Once it seemed that there was nothing new, my I eyes were caught by a comment on StackOverflow. It had just a few up votes and was lost somewhere in the middle of the page, but most importantly it contained a magic word – <label>! As you may know, pressing a label basically triggers the focus event for the bound input. Interesting thing is that, if it is a file input, it works out as a click event, resulting in opening a file browser. This is great for crafting a semantic solution.

<input type="file" name="file" id="file" class="inputfile" />
<label for="file">Choose a file</label>

So, pressing any of these two elements gives us the same result. That means that the most difficult part is… solved! No JavaScript, no other complex solutions like cursor position tracking, just these two lines. See for yourself:

Now let’s just style it and make this look like a normal button.

Hiding the <input>

First off, we need to hide the ugly duckling. CSS properties such as display: none or visibility: hidden will not work out. The reasons are: the input value will not be sent to the server on form submit; the input will be excluded out of tab order (you want your website to be accessible, right?). I set up a combination of CSS properties/values for hiding the input visually but keeping it visible for the browser:

.inputfile {
	width: 0.1px;
	height: 0.1px;
	opacity: 0;
	overflow: hidden;
	position: absolute;
	z-index: -1;
}

I see you are wondering why width and height are set to 0.1px instead of just 0px. Setting the property values to zero ends up throwing the element out of tab party in some browsers. And position: absolute guarantees the element does not interfere with the sibling elements.

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

Styling the <label>

Since the <label> element is visually the button, you can use all of your creative CSS juices on it. I’m sticking to something very simple for now:

.inputfile + label {
    font-size: 1.25em;
    font-weight: 700;
    color: white;
    background-color: black;
    display: inline-block;
}

.inputfile:focus + label,
.inputfile + label:hover {
    background-color: red;
}

Accessibility

How do you know that an element on the website is pressable? 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. The former we’ve solved previously, let’s solve the latter, because labels do not trigger a cursor change by default:

.inputfile + label {
	cursor: pointer; /* "hand" cursor */
}

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 .inputfile:focus on the label:

.inputfile:focus + label {
	outline: 1px dotted #000;
	outline: -webkit-focus-ring-color auto 5px;
}

-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.

Possible Touch Issues

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:

<label for="file"><strong>Choose a file</strong></label>
.inputfile + label * {
	pointer-events: none;
}

JavaScript Enhancement

Probably and hopefully the last thing missing is indicating if files were selected. The file input does usually indicate that, but in our case the input is visually hidden. Luckily, there is a way out: a tiny JavaScript enhancement. The text of a label becomes 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" class="inputfile" data-multiple-caption="{count} files selected" multiple />
var inputs = document.querySelectorAll( '.inputfile' );
Array.prototype.forEach.call( inputs, function( input )
{
	var label	 = input.nextElementSibling,
		labelVal = label.innerHTML;

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

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

There is also a jQuery version of this code presented in the source of the demo files. Make sure to check them out.

A little explanation:

  • 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. The reason I use an additional HTML attribute instead of assigning this sentence as a value for a JavaScript variable is because it’s much easier to maintain the copy when it is in one place.
  • HTML attribute [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:fakepathfilename.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.

This is how the final result looks like:

What if JavaScript is not available?

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.

<html class="no-js">
    <head>
        <!-- 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>
    </head>
</html>

The CSS part accordingly:

.js .inputfile {
    width: 0.1px;
    height: 0.1px;
    opacity: 0;
    overflow: hidden;
    position: absolute;
    z-index: -1;
}

.no-js .inputfile + label {
    display: none;
}

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' ); });
.inputfile:focus + label,
.inputfile.has-focus + label {
    outline: 1px dotted #000;
    outline: -webkit-focus-ring-color auto 5px;
}

Check out the example styles in the demo to see how to style the file input element according to your needs. Make sure to take a look at the source code of the demo and feel free to use this technique in your projects. Happy uploading!

The icon in the demo is made by Daniel Bruce from www.flaticon.com and it is licensed under CC BY 3.0.

Osvaldas Valutis

Osvaldas is a web designer who thinks in code and a creator of Readerrr. Osvaldas is based in Klaipeda, Lithuania.

Stay in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!

Feedback 140

Comments are closed.
  1. If if you want your users to be able to drag and drop files on the button like on any “input file” you have to add some other dozens JS/CSS lines.

    • I know there are other JS libraries to to that, but my point is that the native input file already allows dropping of files on it, and that by adding all the JS and CSS of this article we lose this basic functionality.

      And i think it’s always bad when you add more and more code and at the same time remove functionalities.

    • Yes, it was the first thing I tried! And I was disappointed… For me, such component is crippled, and makes me cringe when I see this behavior on a real site!

    • There’s post coming up how to enrich file upload experience with drag&drop: no dozens of JS lines or large plugins, just some neat JS.

      Drag & drop for the <input type="file" /> is a sacrifice I make for a better look. However, dragging and dropping onto the file input is not intuitive – there’s no indication you can do this. Therefore the majority of users have no idea they drag & drop onto the input. This is more like a secret trick known by geeks like us, rather than usual Internet users.

  2. Instead of setting the dimensions of the input element to 0.1px we used to set it to normal dimensions and then set its opacity to almost zero, like 0.01, so that the other positioned element behind it would show up!

  3. The way I’ve been solving this problem is creating a styled fake input button with an input button absolutely positioned directly over it. The input button is then set to 0 opacity so all clicks go to the input button, but the user sees the stylized fake input button.

    Also dragging files onto an input button works in most browsers so you don’t need the FileReader API which isn’t available in older browsers.

  4. What do you think about hiding the input with opacity: 0 and user-select: none, then stretching it with position: absolute over its container?
    I think this should always trigger the native file selection dialog and avoid all issues. Am I missing something here?

    • Sounds good, however, looks like this requires a double-click for IE 10- users in most cases. Try clicking the left part of the button in IE 10-: http://jsfiddle.net/oejsb7bv

      Also, you would probably need some JS for :hover and :active styles of the button.

    • Nah, I’d wrap the label and the input inside a div.field and then go with .field:hover label {}
      Not sure about :active though.

      And you seem to be right about IE10 but I guess very few people are using it anyway.

  5. You can take this a step further and generate a file preview out of it by turning the file to base64 format using javascript’s FileReader API.
    The downside to this is it *might* not work on images larger than 48kb (base64 max length).
    Say, you are putting an onchange callback to the input file element, then the preview can be generated via:

    function readURL(input) { if(input.files && input.files[0]) { var reader = new FileReader(); // when file reader is loaded, show the result to designated image element. reader.onload = function (element) { document.getElementById('display').src = e.target.result; } // this will only pass the first file uploaded to onload callback reader.readAsdataURL(input.files[0]) } }

  6. I use this solution for a long time now. I use also one enhancement – I put inside element so there’s no need to add any ID to the input:
    <label class="upload"> Choose a file... <input type="file" /> </label>
    I set position: relative; and overflow: hidden; to .upload to position input relatively to .upload to make sure that input never pops up in any weird place on the page.

    • Way to go, however this requires some JS to indicate if there’s a :focus on the file input, as there’s not way to select parent element in CSS. I solved this with input:focus + label in my example.

  7. Thanks for posting. I enjoyed playing with your code and reading through some alternative ways of doing things in the comments. Keep up the good work.

  8. You can’t drag a file into the file element anymore, because it’s hidden. That’s not very accessible. Tab order does work, that’s extraordinary.

  9. Hi, thank you so much for this helpful tutorial, I always try to learn new things everyday. I’m beginner at and when I’ve seen the script there, and I thought that there’s a long way to write something like that in JavaScript. So any helpful resources to help me through my way?

  10. Hi, thank you for the great tutorial!

    How can I support selecting of several files from different folders?

    Cheers
    Ruslan

  11. This was a HUGE help! Thanks for putting this together and sharing. I’m certainly walking away with this in my favorites folder and with a heap of added knowledge. Thanks

  12. Very nice tutorial. Solved the problem of how to display file uploaded with a very nice file upload button. The only thing I changed was to get rid of the “color” and “background-color” settings for “body” in demo.css. They didn’t blend in with my website. Thanks.

    • Edit(2): It seems that the span element is a code remainder.

      label.innerHTML = fileName ? fileName : labelVal; //is a working replacement for line 15-18

  13. Excellent article, just saved me a lot of time and gave me an awesome file upload widget!! Thanks =)

  14. This is awesome thank you!

    Is there a way I can get your code to work for inputs that I create programmatically after the page has loaded?

    Many Thanks

  15. I keep getting the following:
    Uncaught TypeError: Cannot read property ‘innerHTML’ of null

  16. This was great, but within a form if I make this a required field I’m no longer able to see a validation message if a file isn’t selected. How do I make the validation message show up?

    • Hey Anon,
      Just wondering if you landed on a solution to the issue of validation.

  17. Hi,

    This looks interesting!

    I stumbled on this while I was looking for a solution to fix form validation if the file is chosen via Drag-n-drop way.

    So far, I do not see any workaround except to change my validation logic.

    Do you see any “smart” way to fix that too?

    Anyway, it was a really clever way.

  18. Great tutorial, everything is simple and straight-forward. I must be missing a step…or either there is a conflict with my wordpress theme and/or plugins, but I can’t get the filename to show up when I select files. It still has the placeholder text. Any suggestions?

    • Add span inside the label as shown below;

      Choose a file

      ******Then, right at the bottom of the body(still inside it), put your javascript code there. Sometimes, scripts work when placed at the bottom not top of the html file. (Keep that in mind). Script shown below just in case:******

      var inputs = document.querySelectorAll( ‘.inputfile’ );
      Array.prototype.forEach.call( inputs, function( input )
      {
      var label = input.nextElementSibling,
      labelVal = label.innerHTML;

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

      if( fileName )
      label.querySelector( ‘span’ ).innerHTML = fileName;
      else
      label.innerHTML = labelVal;
      });
      });

    • ******Edit: Add span to label(inside the label to be precise)***** Sorry Good luck!

  19. Hello,
    I have an accordion that has more than one forms on a single page. I want to implement this, but it does not work as it should. The selected files are only displayed in the first such custom field. Can someone help with this?

  20. Hello. This is a great tutorial. Wonderful website to learn tricks. I was customizing my forrm with Contact Form 7 in WordPress. And I have a question: How can I receive the file uploaded to my email. That is all. thank you in advance.

  21. Has anyone experienced issues with file uploads from mobile or tablet? The file is apparently added, but the upload is empty. This happens both on iOS and Android.

    Other than that, great file upload solution.

  22. Awesome post! Love that you explain the ins and outs of the solution. ^5

  23. Thank You for the tutorial.

    I followed the code line by line. I successfully styled the button but unable to click to perform upload file operation.

    I dont know what i did wrong. I also downloaded the demo files and copied the 3 js files to my js folder; included the scripts in my tag and still not able to upload file.

    Please help.

    Thank you

  24. Thank you for a well explained solution! 🙂

    I based my implementation on your code, but I wanted to disable the label-click while
    the upload was running in the background. But you can’t disable a label, as far as I know,
    but you can in fact disable the fileinput that the label points to, to get the same behaviour.

    So the only thing I did in my code, was add a disabled-attribute to the fileinput during the
    upload process, and this small snippet of css to make it look disabled.

    .inputfile:disabled + label {
    background-color: #ccc;
    }

    This way we stop the file-dialog window to appear during upload. 🙂