Telerik blogs
A Comprehensive Guide to Styling File Inputs

Trying, and failing to style an <input type="file"> control is a twisted rite of passage in the web development trade. If you’re not familiar with the problem, you could read this Stack Overflow thread to get up to speed, but you may consider a new career afterwards. (Law school did seem appealing, and you only need one browser there — probably IE.) To save you a few therapy lessons, the problem is every browser has a different way of implementing <input type="file">, and few of the rendered controls can be touched with CSS. The image below shows a scattering of browsers implementations:

Although the average developers might say “meh”, I can tell you that the average designer briefly considers a different career after seeing these controls. (There are no browsers in the print world.) In this article, I’d like to take a look at what your options are for styling these things, starting with a technology you probably didn’t expect: web components.

How are web components related to styling form elements?

The problem of styling file inputs (or any form element for that matter) is not a new problem. But lately, whenever one of these “OMG IT’S 2014 WHY CAN’T WE STYLE FORM ELEMENTS” conversations comes up, someone inevitably drops the web-components-panacea bomb. For example, after Nicolas Zakas tweeted this complaint about styling <select> elements…

…the Twitter-verse was quick to recommend web components as the magical solution:

People are quick to play the web-components-will-fix-that card, but few think about how this will actually work. Chrome 36 has “full support” for web components, so why aren’t we styling Chrome’s native form controls?

How to build your own form elements with web components

Although web components don’t give you a magical ability to style form elements, they do let you replace the browser’s implementation of an <input> with your own. The following code does just that (and works in Chrome 31+):

An input replacement

An input replacement

Here’s what happening there. When you call createShadowRoot() you tell the browser to attach a new shadow root to the selected element — in this case the <input>. Multiple shadow roots can exist simultaneously, but the last one added — aka the one createShadowRoot() built — is the one the browser uses.

Why does the <input> in the image above display as a tiny box? Because Chrome’s user-agent stylesheet applies padding, background, and border CSS rules to the input selector — and those rules still apply with the new shadow root.

The new shadow root is a blank slate you can use to build your own <input>, or whatever else you can dream up. Here I implement an <input> with a <marquee>. Because I can.

An input implemented with a marquee element

An input implemented with a marquee element

Although it’s cool that this is possible, it’s not very useful. For instance, suppose you want to do something worthwhile, such as styling an <input type="number"> consistently across browsers. You could use createShadowRoot() to implement the native control with a JavaScript-based widget, such as Kendo UI’s NumericTextBox. Here’s an implementation of this:

Using createShadowRoot to replace a number input control

Using createShadowRoot to replace a number input control

This gives you a consistent visual display, but if you try the example out (remembering that it currently only works in Chrome), you’ll see that the behavior is screwed up. You can’t type in the <input>; HTML5 form validation no longer works; you can’t give the <input> focus on mobile; and more.

Why? Well, the broader problem is that when you replace the functionality of an <input>, you lose all <input> behavior — even the stuff you want. And if you have to add a whole bunch of JavaScript to replicate native functionality, you’re better off using JS widgets directly; you don’t gain anything by introducing a new shadow root.

As an alternative, you could take the opposite approach. That is, instead of replacing an <input>‘s shadow DOM, you could extend the input element with a new custom element — e.g. <input type="file" is="my-super-duper-custom-file-uploader">. If you do that you don’t lose any of the input’s behavior, but you also don’t gain any special ability to customize the browser’s controls — so you’re no closer to styling the browser’s controls.

But there is some good news. Although there are still plenty of issues with replacing <input> elements in general, file inputs are one exception to this problem.

What about <select>s? Chrome currently forbids you from invoking createShadowRoot() on <select> and <textarea> elements. If you try, you’ll get a HierarchyRequestError.

Replacing File Inputs

File inputs are far easier to replace because they have no textbox the user can type in. Really, the file input does a single thing you care about: prompting the user to select a file. And in Chrome, you can replicate that functionality even after replacing the <input>‘s implementation. With that in mind, I present a Chrome-only, shadow DOM-based solution to styling file inputs:

Replacing a file input's shadow root in Chrome

Replacing a file input’s shadow root in Chrome

You can play with this example in Chrome at http://dojo.telerik.com/@tjvantoll/uViq.

What I do here is replace the <input>‘s shadow root with my own. The new shadow DOM contains a single button that I make pretty with some Bootstrap-inspired CSS (but you’re obviously free to use any CSS you’d like). Chrome magically makes clicks on the button open the file picker, but you do need to add an explicit keydown event handler to preserve keyboard accessibility. Because Chrome leaves the <input> focusable, you also need to set the button’s tabindex to "-1" to avoid having two focusable targets for one button.

It takes a bit of code, but if you think about what’s happening here it’s pretty cool. You are substituting the browser’s <input type="file"> implementation with your own — something a lot of web developers have wanted to do for a long time.

But what about other browsers?

There’s always a but, and in this case it’s a pretty big one: there’s nothing in the specification that dictates exactly what the browser should do when you replace the shadow root of a form element. So unfortunately, there’s nothing to guarantee that future browser implementations will behave as Chrome does today.

For example, if you turn on the “dom.webcomponents.enabled” flag in Firefox (which enables its in-progress shadow DOM implementation), you can’t give an <input> a new shadow root; Firefox ignores the call to createShadowRoot(). You also can’t use Polymer’s shadow DOM polyfill to replace an <input>‘s shadow root; Polymer also does nothing.

So, although you can use shadow DOM to implement your own file input in Chrome, there’s no way of knowing when (and if) you’ll be able to do this in other browsers. However, it’s worth noting that this approach degrades relatively nicely. In unsupporting browsers you can fall back to the same <input type="file"> you’re used to.

With that in mind, I present a snippet that finds all <input type="file"> elements, and gives each a new shadow root that contains a single <button>:

(function() {
    if ( typeof HTMLElement.prototype.createShadowRoot === "undefined" ) {
        return;
    }
    try {
        var root,
            input,
            fileInputs = document.querySelectorAll( "input[type=file]" );

        for ( var i = 0; i < fileInputs.length; i++ ) {
            input = fileInputs[ i ];
            root = input.createShadowRoot();

            root.innerHTML = "<button tabindex='-1'>Select File</button>";
            input.addEventListener( "keydown", function( event ) {
                if ( event.keyCode === 13 || event.keyCode === 32 ) {
                    input.click();
                }
            });
        }
    } catch( e ) { }
}());

There are two safeguards in place here. The first ensures that HTMLElement.prototype.createShadowRoot exists, because if it doesn’t there’s no point in continuing. The second is a try/catch around the code that replaces the <input>‘s shadow root. This is to protect against future browsers that may throw an error if you try to replace an <input>‘s shadow root. Chrome does that today with <select> and <textarea> elements, so it’s certainly possible other browsers will add that restriction to <input> elements in the future.

But can I actually use this?

Well you can, but for most sites it’s probably not the greatest idea. Shadow DOM is in its infancy, and how form elements work with shadow DOM is not even specified. Therefore it’s entirely possible that some other browser will have a different interpretation of how this should work.

I offer this as an example of something that is possible today in Chrome, and the type of solution that the shadow DOM specification should make possible as the web world gets around to specifying how these edge cases will work.

So what should I use today?

First of all, consider whether you really need to style the default file input UI at all. In addition to requiring zero implementation work, leaving the default UI alone presents the user with controls they’re familiar with and know how to use.

The argument to leave native controls in place has been made by smarter people than me. Here are some good reads on the topic from Bruce Lawson and Aaron Gustafson.

Assuming you do want to move forward with styling file inputs, start with the ::-ms-browse and ::-webkit-file-upload-button pseudo-elements. They’ll let you style the button portion of the <input type="file"> control in IE and WebKit-derived browsers — Chrome, the default Android browser, Safari, iOS Safari, and Opera (Firefox has no equivalent pseudo-element).

Building a complete file input replacement

If you want to completely replace a file input in all browsers things get more complex quickly. Creating a true <input type="file"> replacement involves accessibly hiding the browser’s <input> implementation, creating a replacement control, and ensuring the replacement control is keyboard and screen reader accessible.

Peter-Paul Koch has an excellent writeup on the most popular technique, which involves absolutely positioning both the <input> and a replacement control, and then adding z-index: 2 and opacity: 0 to the <input>. It’s clever, but it requires some extra markup, a hardcoded width (as the <input> and its replacement must occupy the exact same area), and some JavaScript to maintain keyboard accessibility. Here’s the full code I needed to make this happen:

Code for a complete file input replacement

Code for a complete file input replacement

You can play with this example at http://dojo.telerik.com/@tjvantoll/IQIqE.

Using an existing solution

If you don’t want to worry about testing and maintaining a hack across devices, another option is to use existing JavaScript widget solutions. After all, you likely have more important things to worry about — like making sure the user isn’t uploading viruses to your server. In addition to taking care of the UI for you, many JavaScript widgets offer behavior that goes above and beyond what the basic file input provides. Here are three popular solutions.

1) jQuery File Upload Widget

The most popular file input replacement (in terms of GitHub stars) is the jQuery File Upload widget. The File Upload widget’s basic setup is well documented, and using it is as simple as selecting <input type="file"> elements and invoking the fileupload() jQuery plugin.

Demos

2) FileAPI Plugin

Another popular jQuery plugin is the FileAPI. It addition to basic file upload functionality, it also provides an elegant API for cropping photos, drag-and-drop uploads and more.

Demos

3) Kendo UI Upload Widget

Finally, Kendo UI Professional includes an Upload widget that is a fully featured replacement for the <input type="file"> control. In addition to providing a customizable and accessible control, the upload widget supports more complex use cases such as asynchronous uploads. You can see the base functionality of the widget below.

Demos

Wrapping Up

Styling file inputs is a problem web developers have been trying to solve for years, and with shadow DOM we might finally have a solution. In Chrome today you can replace an <input type="file"> with an alternative control, but it’s unclear whether other browsers will allow this same behavior.

If you need to use a file input with a completely custom display today, you can use an opacity-based hack, or leverage existing solutions such as the jQuery File Upload widget or Kendo UI Upload widget. Better yet, consider leaving the existing file input display alone. Your designer may hate you, but you’ll give users a control they’re familiar with, and you won’t have any work to do.


TJ VanToll
About the Author

TJ VanToll

TJ VanToll is a frontend developer, author, and a former principal developer advocate for Progress.

Comments

Comments are disabled in preview mode.