From our sponsor: Meco is a distraction-free space for reading and discovering newsletters, separate from the inbox.
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 thefiles
property of JavaScript. For the latter case, we simply rely onvalue
. Since it usually has a value ofC:fakepathfilename.jpg
format, thesplit( '\' ).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.
It seems that use a hand cursor in a button is not correct. :/ See:
https://medium.com/simple-human/buttons-shouldnt-have-a-hand-cursor-b11e99ca374b
I don’t know if it matters, but I only report it..
Great article — thanks a heap for putting the time into such a comprehensive, practical, and helpful piece.
thanks ….
Thanks you for covering this in great detail and for the source code!
Anyone has an idea how to create 2 buttons on one page? I changed id and class for another one, changed javascript code and still, it uploads to the first top button.
I noticed in your source that the svg used is referencing http://www.w3.org. Is the svg open source or are there copyright issues involved? I’ve only used svg on Muse sites, so don’t have any experience in svg for html.
I’m trying to get the js code to work in showing the file name once selected for upload. I’m not having a lot of luck with it. I copied and pasted the code from this site as well as from the source file inside my html file as well as tried to link to it locally.
Is there any sort of code that needs to go before and/or after to make the script work other than script tags?
Great article, but surprisingly you didn’t mention a major function of these buttons, “Drag and drop”, yes we can drag and drop files over those buttons, at least on chrome and firefox, tell me if it work on other browser.
what i want is, i want to select the 2 images of different location. how can i do that…anyone…
Thank you very much 🙂
great and useful
thanks!
That’s very clever. I didn’t get it at first but now I see that the label is just styled when the file input takes focus. It’s still the file input that’s listening to keyboard events.
For some reason this works great in jsfiddle, but in my project (which uses Bootstrap, a likely culprit I suppose) I can’t trigger the file upload by clicking the label. Any ideas?