Fancy File Inputs with Mootools and CSS

DLB Guillotine logo

(Come from the blog? Click here to skip to the part you haven't read yet.)

God, I hate HTML DOM FileUpload Objects (<input type="file"> elements). They're displayed differently by every browser, and it's always ugly. Let's use some fancy AJAX and CSS to fix these horrible, horrible form objects.

Some time ago, we were working on a website that required a contact form with the ability to attach a file. This sounds like, at this point in web design, it should be incredibly simple. Imagine my chagrin as I learned that, in fact, styling the FileUpload Object had a smack of Holy Grail-ness to it. Well, at least as far as HTML form objects are concerned.

A lot of people have had similar ideas on how to handle this. I suppose it all started with Michael McGrady's solution, which was followed closely by an improvement on McGrady's technique from Peter-Paul Koch over at Quirksmode. The closest thing I found to a satisfactory solution, though, came from Shaun Inman. His solution was definitely the right idea, but it wasn't exactly full-featured, and I wanted it built in my favorite Javascript framework.

Like Shaun, I don't think that the text-box is an appropriate UI element for a file upload in 2008 (who wants to type c:\Documents and Settings\Paul\My Documents\My Photos\2008-06-02-Me.jpg into a field that only shows 25 characters?!). Further, every implementation I've ever seen that makes use of a text-box is wonky. So I wanted to do a Safari-style upload.

The default display for a file input in Safari.
I appreciate the cleanliness of Safari's implementation. It's the best default browser implementation out there...but it's still not good enough!

Shaun's really clever idea involves setting the input's opacity to zero and then using a little javascript function to keep the button under your mouse while you're hovering inside the input's parent container. With the hardest part figured out, I had two additional goals:

  1. First, I needed it to display the file name of the uploaded file. One problem with Shaun's is that I couldn't even verify if I'd successfully attached a file.
  2. Second, I agree with Peter on the importance of being able to clear the file from the form. No implementation I've seen allows you to do that, so I added the feature.

In Actu

Here it is:

 

I'll take you through how this all works below, but for those who don't need (or want) the explanation, here's everything you need to plug and play. Otherwise, let's get started:

HTML

First thing's first, and easiest. Here's the very simple HTML behind a fancy file upload:

<div id="filingsystem">
	<div id="filecabinet"><input id="attachment" class="file" type="file" /></div>
</div>
<div id="filename">&nbsp;</div>

Basically we're taking advantage of two nested <div> tags around our file input so we can use Javascript and the DOM to make the requisite changes to the HTML. Additionally, we have a <div> that represents the place where our file name and file icons will eventually sit.

CSS the First

Here's some CSS to get us started. The filecabinet <div> holds our background image, which is displayed, as the input inside this div has its opacity set to 0:

div#filecabinet
{
    width: 75px;	/* Set this to the width of your background image */
    height: 22px;	/* Set this to the height of your background image */
    display: block;
    overflow: hidden;
    background: url('browse.gif');
    background-repeat: no-repeat;
    background-position: 0 0;
    cursor: pointer !important;
}
 
div#filecabinet:hover
{
    background-position: 0 -22px; /* Set this to minus the height of your background image */
}

This is a very common technique for " pure CSS rollovers". The gist of it is that on hover, we reset the position of the background. The key here is understanding the image browse.gif:

Explaining the browse button.

CSS the Second

And now the rest of our CSS:

input.file
{
    position: relative;
    height: 100%;
    width: auto;
}

span#filename
{
	display: none;
	vertical-align: middle;
}

span#filename img
{
	margin-right: 5px;
}

This gets you the correct positioning effects. Fancier styles are available in our download, or else are left as an exercise to the reader.

With our HTML and CSS in place, its time to look at our Javascript.

Javascript the Elder

Our file input uses a near-identical variation on Shaun's Javascript class, called FancyFiles. I won't go into detail on its inner workings, you can easily understand those if you are interested, but here are its methods:

That's it! Easy. Now let's look at the method that is going to create the rest of our fancy file upload effects. I'll write pseudo-code here, which you can compare to the actual code in the file:

Javascript the Younger

function addEventToFileUploadElement(el)
{
	FancyFiles.stylize(el); 		/* Stylize the element from the incoming argument el. */
	el.addEvent("change", function(e) 	/* Add an anonymous "onchange" event to el. */
	{

Ok, so this far, we've stylized our file input, and started to add an event that will occur onChange. This means that when the attached file data changes (from nothing to something, or from something to something else) we'll fire the event we're about to write.

var filename = $('filename');	/* Get a handle on our filename <div> */
if (el.value != '')		/* If we've got a file attached */
{
	filename.setStyle('display', 'block'); 		 /* Set the filename <div> to display=block. */

	/***
	 * The next eight lines (cut for brevity) take the incoming file name, process it, 
	 * and create the new innerHTML for the filename <div> in the var len.
	 * Then, we need to clear the inner HTML from the <div> filename, and add
	 * our newly created stuff:
	 ***/

	filename.setHTML("");	
	new Element('span').addClass('filename').setHTML(len).injectInside(filename);

Ok! So now we've got a file name displaying, along with a couple of icons, whenever we add or change a file. So far so good! Now we've got to be able to clear the file data from our form.

In our new HTML, we have a "clear file" icon that has an id of killfile. Now we want to add an event to it to give us a fresh start when it is clicked. Unfortunately, this has to take a slightly convulted path. Since there's no support to simply clear the attached file from the DOM Object, we're actually going to destroy the old one, create a new one, and reattach all of our styling and events. Here goes:

$('killfile').addEvent("click", function(e)
{
	$('filecabinet').remove();		/* Remove the file cabinet. */
	filename.setStyle('display', 'none');	/* Hide the filename */
	var container = $('filingsystem');	/* Get a handle on our file container */
	/* Create our new HTML (cut for brevity), and subsequently our new file cabinet. */
	var newCabinet = new Element('div').setProperty('id','filecabinet').setHTML(newfileuploadHTML); 
	newCabinet.injectInside(container);	/* Inject the new file cabinet into the container */
	addEventToFileUploadElement($('fancyfileupload')); /* Recursively attach this method to the new input. */
});

Et voila! This solution was tested working in Firefox, Safari, and IE7, and degrades gracefully for IE6. Please note that this means that it does not work in Opera at this time. I'm looking into a solution for this, and in the meantime, I serve Opera users a defacto file upload form server-side. You can leave questions and comments here.