jQuery Custom File Upload Input: from the book Designing with Progressive Enhancement

Posted by Scott on 07/12/2010

As websites expand content-sharing and collaboration features, users are electing to upload more and more files—photos, videos, documents, even secure data—via web applications to store them in “cloud”-based systems rather than locally on their computers. The HTML input element with a type="file" attribute gives web developers a native control to handle this file upload functionality. But browsers offer almost no control over how file inputs are presented visually, making it difficult to incorporate them into a uniform interface design.

Today, we’re releasing a jQuery file input plugin that uses progressive enhancement to transform a standard HTML file input into a visually customizable control. With a few lines of CSS and JavaScript, this widget allows you to create a custom-styled file input that leverages all of the accessibility features the native input provides.

This widget is one of the 12 fully-accessible, project-ready, progressive enhancement-driven widgets we created to accompany our book, Designing with Progressive Enhancement. (Purchasers of the book can access all twelve widgets immediately).

A quick demo

Before we get into the mechanics of the custom-styled input control, here’s a demo that highlights key features. The input box and buttons have distinct, custom visual style; and when the user selects a file, the custom input updates to display the file name and an icon representing the file’s extension. To see the feedback in action, browse and choose any file. (FYI: The demo is completely non-functional—i.e., it will not upload files to any server. )

jQuery accessible custom file input widget

Note: If you click the “view low-bandwidth” link, just remember that you’ll need to click it again to view the enhanced version of this site on future views.

To ensure an accessible experience for the widest audience possible, this demo and all our widgets use our EnhanceJS framework, which tests browsers’ CSS and JavaScript capabilities and applies enhancements only to capable browsers. In practice, browsers that don’t pass receive a basic experience with the fully-fuctional native control; for capable browsers that do pass the test, the script applies advanced CSS and JS enhancements, adds a “View low-bandwidth version” link for users to toggle from a basic to enhanced view, and drops a cookie on change to record the user’s preference. EnhanceJS is used in the demo page, but it is not required to use the widget in your projects. (To learn more about EnhanceJS, check out Introducing EnhanceJS: A smarter, safer way to apply progressive enhancement)

How does this widget actually work?

The approach we’ve used piggybacks on top of the native file input: The widget creates a custom-styled file control using div and span elements, and then uses JavaScript to set the native input's opacity to zero and dynamically position it invisibly between the cursor and the custom control. The user actually interacts only with the native input without knowing it. The following infographic demonstrates the concept:

Infographic: the native file input is positioned under the user’scursor to open the browse dialog.

In addition, the widget script applies custom styling, and parses the filename’s extension to recognize the file format so it can display a file-specific icon (the CSS includes a generic file icon and several specific icons—image, multimedia, zip folder—which are easily customizable).

The best part of this approach is that we get great accessibility support for keyboard users and those with disabilities automatically. Since we’re always using the native file input control, we can leverage all the functionality and accessibility that it provides—the custom file input is really a visual feedback mechanism only. And though the native input is invisible, it’s still fully keyboard-accessible, so there’s no need to add any additional accessibility features or ARIA attributes. (We do, however, add one ARIA attribute to the custom widget to hide it from screen readers, since it’s relevant only to sighted users.)

A tip of the hat

This solution was inspired by the work of Michael McGrady and Shaun Inman, who developed this tricky bit of CSS and JavaScript to position the native file input under the user’s cursor to create custom file inputs. You can find that article here: Styling File Inputs with CSS and the DOM

How do I use it?

To use this script in your page, you’ll need to download and reference both jQuery and jQuery.fileinput.js, as well as the associated CSS and images for the control (download instructions are found below), and call the customFileInput method on any list element on the page. For example, to create a custom input control from a input element with an id of file, find it using jQuery and call the customFileInput method on it:

<input type="file" name="file" id="file" />

JavaScript (jQuery):

Note: be sure to use jQuery’s $(document).ready event to wait until the DOM is ready before running the script above.

Also, you can disable and enable the control by calling either $('#file').trigger('disable'); or $('#file').trigger('enable'); on the native input. Once disabled, the custom control will be styled to appear disabled as well. If the input is disabled at load, the custom control will naturally inherit that state, so you won’t need to call the “disable” method when you first create the control.

That’s all there is to it!

If you’d like to learn more, we explain in detail how this widget works under the covers and how to apply these principles in different interactions or more complex scenarios in our book, Designing with Progressive Enhancement.

Download (and help us improve) the code

The custom file input plugin code is open source and available in a git repository, jQuery-Custom-File-Input. If you think you can help on a particular issue, please submit a pull request and we’ll review it as soon as possible.

If you’ve already purchased Designing with Progressive Enhancement, you can download all twelve widgets at the code examples download page.


<div id="commentNumber1" class="commentEntry">
<p>This is great, just very great!

	<p class="posted"><a href="#commentNumber1">Comment</a> by

santhos webdesign on 08/06  at  04:35 PM

<div id="commentNumber2" class="commentEntry">
<p>Why not start this off with tighter ThemeRoller integration?&nbsp; Adding ui-widget-content to the div, and ui-state-default, etc to the button makes it work well with a few exceptions where the text was the same color in the lighter themes.&nbsp; I can appreciate the idea of keeping new widgets separate at first, but I would consider wrapping it into ThemeRoller as an option maybe.

	<p class="posted"><a href="#commentNumber2">Comment</a> by

Robert Goula on 08/09  at  04:03 PM

<div id="commentNumber3" class="commentEntry fg">
<p>Thanks Robert, we’ll give it some thought. Of course, if it becomes a jQuery UI component, it’ll have ThemeRoller integration by default.

	<p class="posted"><a href="#commentNumber3">Comment</a> by

Scott (Filament) on 08/12  at  09:36 AM

<div id="commentNumber4" class="commentEntry fg">
<p>Quick update: we’ve added support for a disabled state and two methods you can call on the native input to disable and enable it:


Naturally, if the input is disabled by default, it will automatically have this state applied.
The zip is updated!

	<p class="posted"><a href="#commentNumber4">Comment</a> by

Scott (Filament) on 08/12  at  03:09 PM

<div id="commentNumber5" class="commentEntry">
<p>Hi guys,  looks good. What changes have you made so it works in IE6? I bought the book, but the examples didn’t work for this browser when I last checked.

I agree with Robert as well, this should be themeroller (and probably ARIA as well) capable I’d certainly recommend it goes into JQuery UI, I want to use it!


	<p class="posted"><a href="#commentNumber5">Comment</a> by

Jamie on 08/17  at  02:20 AM

<div id="commentNumber6" class="commentEntry">
<p>Ahh I see further to my comment, you’ve added a ‘click’ hack for IE and Opera. Looks pretty good, you’ve obviously made some improvements since I bought the book.

	<p class="posted"><a href="#commentNumber6">Comment</a> by

Jamie on 08/17  at  06:53 AM

<div id="commentNumber7" class="commentEntry">
<p>any chance that at some point some security software is going to consider this clickjacking - positioning hidden/transparent elements underneath your cursor could be leveraged for various nefarious purposes. how will browsers and security packages discriminate between valid implementations and scams/hacks?

	<p class="posted"><a href="#commentNumber7">Comment</a> by

Andy Bright on 08/17  at  05:32 PM

<div id="commentNumber8" class="commentEntry">
<p>Looks really awesome!

Here in FF 3.6.8 the mouse pointer doesn’t change when I hover it, it just flickers really quickly when I enter and leave the file input area.

	<p class="posted"><a href="#commentNumber8">Comment</a> by

Marcus Dalgren on 08/18  at  07:39 AM

<div id="commentNumber9" class="commentEntry">
<p>Looks great. One minor issue: just opened this page in IE8, and it indeed pops up the dialog, but when I select a file, the display of the filename is not updated until I unfocus by clicking somewhere else.

	<p class="posted"><a href="#commentNumber9">Comment</a> by

Kyle Simpson on 08/18  at  10:08 AM

<div id="commentNumber10" class="commentEntry">
<p>When the scroll is situated at the bottom, the component don’t capture the event of click, and the event of over. Any idea? any solution?


	<p class="posted"><a href="#commentNumber10">Comment</a> by

eindra on 09/21  at  06:48 AM

<div id="commentNumber11" class="commentEntry">
<p>Impressive- especially how you got it to work in IE 8, too. However, I find it ironic you didn’t style it using the jQuery UI CSS framework. Why reinvent the wheel, when you are already busy refining the first one? !!!

	<p class="posted"><a href="#commentNumber11">Comment</a> by

Duncan Kenzie on 10/08  at  11:05 AM

<div id="commentNumber12" class="commentEntry">
<p>Unfortunately doesn’t work in Firefox when the input control is a child of an jquery ui accordion pane.&nbsp; It works in Chrome.&nbsp; Argh.&nbsp; It is so hard to find a good file input widget that works and that looks good and uses ThemeRoller.&nbsp; I was hoping this would give me 2 out of 3…

	<p class="posted"><a href="#commentNumber12">Comment</a> by

Marvin Herbold on 12/14  at  06:15 PM

<div id="commentNumber13" class="commentEntry">
<p>Hi Marvin Herbold,

The problem in FireFox is that hidden input["file"] is running away on hover for some reason in some places because of dynamically added style to it that is not counted correctly (or because of something else as well).

The solution is to change a JS inside jQuery.fileinput.js a little.

Basically, all you need to do is search for “fileInput.css” there and replace “‘left’:...” and “‘right’:...” to:

‘left’: ‘0’,
‘top’: ‘0’

Keep in mind that this is fast and temporary solution that worked for me.

	<p class="posted"><a href="#commentNumber13">Comment</a> by

Ignaty Nikulin on 01/12  at  07:19 AM

<div id="commentNumber14" class="commentEntry">
<p>@Ignaty thank you! this resolve the problem on FF but not for Opera!

if you want fix the text into the ‘populate span’ when is too long, add this rules in the enhanced.css to .customfile-feedback-populated :

white-space: pre; padding-right:0; width:172px; overflow:hidden;

	<p class="posted"><a href="#commentNumber14">Comment</a> by

b4z81 on 01/27  at  05:23 AM

<div id="commentNumber15" class="commentEntry">
<p>@b4z81, thanks!

Will be useful when we will test in Opera, but this will be when Opera take over 3.5%+ of all web. :)

	<p class="posted"><a href="#commentNumber15">Comment</a> by

Ignaty Nikulin on 01/28  at  12:27 PM

<div id="commentNumber16" class="commentEntry">
<p>@Ignaty i find a temporary solution:


can you try?

	<p class="posted"><a href="#commentNumber16">Comment</a> by

b4z81 on 01/28  at  02:12 PM

<div id="commentNumber17" class="commentEntry">
<p>Thanks for the script, really handy, but I got a little problem:

The script does not seem to work well on FF4, is there any plans for an update soon?

Thanks a bunch.

	<p class="posted"><a href="#commentNumber17">Comment</a> by

Harvey on 04/26  at  10:53 AM

<div id="commentNumber18" class="commentEntry">
<p>I like your jQuery solution of the file input. I was looking for a nice solution, guess i found it :)

Did you ever focus the input file by tabulator. I use them always when completing a web form. Unfortunately the content of the file input dissapears completely.

Any suggestions why?

	<p class="posted"><a href="#commentNumber18">Comment</a> by

antonio on 05/05  at  06:49 AM

<div id="commentNumber19" class="commentEntry">
<p>Shouldn’t this plugin be wrapped in a closure calling against jQuery rather than $ ?

eg (function($){ ......})(jQuery);

At the moment it doesn’t play nicely with namespace conflicts - eg if both prototype and jQuery are loaded in the same page and prototype.

Also the “Change” button doesn’t appear to work in FF4.

	<p class="posted"><a href="#commentNumber19">Comment</a> by

Ben on 06/11  at  10:34 PM

<div id="commentNumber20" class="commentEntry">
<p>On further investigation it’s a lot deeper than the change button not working, try using the tab key on this page to tab into the field.

On FF4, Safari 5.0.5 and Chrome 12 (ie every browser I’ve tried) tabbing into the field triggers the button and text to shift out of position within the div. As overflow is hidden they virtually disappear.

This is the same problem I was having intermittently just by moving the cursor on and off the field but by using the tab key it’s easy to reproduce it.

	<p class="posted"><a href="#commentNumber20">Comment</a> by

Ben George on 06/14  at  07:26 AM