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.
This helped me a lot on my job!!! Thanks so much.
You sir – are a champion
This is a good post and it was similar to the way I’ve been doing it for a long time now and wrote a jQuery plugin to handle it all. it was so long ago though, that recently I’ve been going through and ‘refreshing’ my plugins and found something interesting. The first way I attempted approached this problem way back when was similar except that I was attempting to use a regular button rather than a label, then using the onClick to then send a javascript ‘click’ to the file input. This worked in at least one of the browsers (FF I think) but for security reasons I guess did not work in IE so I settled on the label way. Well low and behold, it seems to work now in the most major browsers (tested IE, Edge, Chrome, and FF). You can in fact redirect a click of a regular button to click on a file input. The only drawback to this that I see wouldn’t work for the way the author was using it is that you can’t put ‘images’ on a button, so the nice little ‘arrow’ icon would not work (as far as I know you can’t anyway). But the positives are that it is an actual button and looks and acts like one out of the box (tabbing and whatnot) without any styling needed, and you can of course style it.
The other difference in the way I was doing it is that rather than hiding the file input using that styling, I just positioned it absolutely off the screen like so “position:fixed;top:-1000px” which makes it invisible and completely out of the picutre without all the styling needed. The problems I encountered not doing it this way back then were that depending on the browser, sometimes the file input itself was longer than the ‘label’ (it didn’t seem to respect the ‘width’ declaration at least back then) meaning you could actually click outside the label and still trigger the browse dialog, and I didn’t like this.
Anyway, just food for thought and some other ways to approach this. And as far as ‘not having javascript’ available, that has to be almost laughable at this stage of the game. If you don’t have javascript enabled (.01% of people browsing today) you’re just going to be out of luck when it comes to any website I work on.
One subtle thing I noticed was that the jquery and javascript are looking for a span and it wasn’t finding it with the sample you provided. I noticed that you had a glyphicon on one of the buttons in your article and so I added one around the label text in a span and tried it again and the caption changed after clicking on a file. Not sure if that is just user error on my part. Thanks for providing the article though it is a very good solution.
Great post
but…
This
<strong>Choose a file</strong>
should be this
Choose a file
How to make button static width? If file name is long – button became too long.
Awesome! Loved it
It’s working fine in normal html,css,file but
I am not able to integrate this thing into wordpress something went wrong
All styling is completly applied but when i chose file i am not able to see the filename.
Need help!
Thanks man!! absolutely everything needed is covered there.
yep, Marshall Alan Hill, the span is not in the post and is used by the js. The source code at the top of the article has the proper markup.
Awesome post, thanks a bunch
Very useful post. Nicely explained. Thanks a lot…!!!
A downside to this is that you can no longer drag files onto the input on OSX – I implemented this on a site and then immediately reached to do it… and… yeah.
Actually it was relatively trivial to fix by adding a container div and positioning the input over the button – you then have to move all of the hover styles over to the input rather than the label but it seems to work fine in Chrome, Firefox and Safari.
Very good
Hello! Very nice tutorial and code!! I’m trying to use in the asp:FileUpload (inside UpdatePanel) but it’s not working!
Great read. Definition helped me a ton!
Its really helpful thanks for help..
When I upload a file using this the name of the file doesn’t show because the input is hidden. How will the user know the file is ready to upload?
Thank you very much. I’ve used you solution in my project.
has it worked for you, as for me it aint working
Has anyone used this in a React component? It didn’t work for me.
Edit: Forgot the ‘id’!
Also, explanation here: http://stackoverflow.com/questions/32433594/how-to-trigger-input-file-event-reactjs-by-another-dom
I’m a FrontEnd designer and developer and we make customized systems… I’ve been looking for a solution for this subject, the ugly browser file-input is so outdated for our designs; this is sooooo easy and obvious. Thanks a lot! You are an angel.
Thanks alot
Someone can implement drag and drop in this demo?
I got it! If anyone wants to try i use it : https://www.devbridge.com/sourcery/components/drag-and-drop-uploader/#
I use the both examples and adjust for my needs.
I´m a newbie in JS so if i can, u can too! Good lucky!
Many thanks for this my friend.
This works excellently! Thanks a lot!!
Thanks, Osvaldas, this is up and running for me – but (as I’m not a javascript-literate guy) it took a while to see that for the feedback on selected files to work we need to add a (possibly empty) span element to the label, which is where the filename will be inserted by the script.
This is amazing!!! Thanks a lot Osvaldas Valutis 🙂
Hi Osvaldas;
Your solution is very good and helped me a lot. Thank you very much.
But, I had a small inconvenience working with plugins that contains native markup (bad markup), and <label> was not the “next” element.
So, I did a small improvement in your logical:
Sometimes, when we are working on native markups, the tag <label> can be before, sometimes after and sometimes (believe me) out of the imediate box. So, how to find the right <label> to apply the callback?
All input[type=file] have the attribute “name” and its <label> always have the attribute “for”; so I retrieve the attribute “name” in a variable $inputName and used it to find label[for=$inputName]
BTW, now, the fuck*** bad markup can put the label anywhere and I’ll can find it.
One more time, thank you very much for this great solution.
*** Please, remove another comment, because I forgot to use html entities. :-p
Very Good ! Thank you very much
Excellent step by step instructions
Thank you so much!
Hi, i tried to implemnt this but it is not hiding the default browse button. This is what I get.
http://kiinc.net/img/hosted/fileupload.png
I included the component.css file and the custom-file-input.js file.
Thank you bro! You help me hugely, all my proyect using your way
Thank you!
Great tips!! thanks for sharing…
Parabéns!!! Lindo, Lindo esses inputs customizados.
Great work! Thank you so much!
TY VERY MUCH ! REALLY NICE EXPLANATION
Greate Post
Thank you sooooooooooooo much!
Hey, great help! but how to make it working on the iphone (and possibly also other mobiles) on the ipad and computer it works but iphone (5 and 6 the button “choose” doesnt work at all.)
How to indicate that input is required on submit it empty.
Just add conditional classes using javascript, you’ll know if there is a file when you access the input.files array.
Thanks! The most efficient solution!
Thanks a lottttt!
Thank you!!
Thanks for a great article
Awesome Job thanks for sharing 🙂
Thank you very much, this is so helpful. But I realize after I download your source and I copy the .js file to my solutions. I can’t manage to call it. I have to create a new js file and copy the content over.
it doesn’t work with asp.net runat=”server”,can u help me?
Really great tutorial, but I have the same problem as Carlos earlier. I can’t get the filename to show up after selecting the file. I am using wordpress theme and I added span into the label, but still doesn’t working. Doesn’t anyone know what could be the problem?
I’ve solved problem with this simple JS:
$(document).ready(function(){
$(‘input[type=”file”]’).change(function(e){
var fileName = e.target.files[0].name;
document.getElementById(“spanid”).innerHTML = “File ‘” + fileName + “‘ is selected.”;
});
});
Thanks Nikky, I had the same problem and your code works!
Thank you, same problem here and your code works.
This is great except the fact that going this way we come up with a file input that does not support “drag files to upload”.
hi, how to make the file upload required * ?
Thanks, But about “The reasons are: the input value will not be sent to the server on form submit; ” that is not correct. the input value will submitted to the server if it is hidden with display:none or visibility:hidden, only when it is readonly or disabled when it is not submitted to the server.
Good tut, but you should include span to your label template (in sample html code)
coz when i try to run JS, its not founding that span inside this label 🙂