Styling File Inputs with CSS and the DOM

File inputs (<input type="file" />) are the bane of beautiful form design. No rendering engine provides the granular control over their presentation designers desire. This simple, three-part progressive enhancement provides the markup, CSS, and JavaScript to address the long-standing irritation.

Before we get down to the nitty gritty details, check out the demo or, if you’re a Mint user, check out your settings page in the Mint Account Center (requires login).

The Markup

<label class="cabinet"> 
    <input type="file" class="file" />
</label>

The file input is given a class of file and wrapped in an element with a class of cabinet. (The type of wrapper element and actual class names are not set in stone and can be changed via properties of the SI.Files object to avoid conflicts as necessary.)

The CSS

.SI-FILES-STYLIZED label.cabinet
{
    width: 79px;
    height: 22px;
    background: url(btn-choose-file.gif) 0 0 no-repeat;

    display: block;
    overflow: hidden;
    cursor: pointer;
}

.SI-FILES-STYLIZED label.cabinet input.file
{
    position: relative;
    height: 100%;
    width: auto;
    opacity: 0;
    -moz-opacity: 0;
    filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);
}

The first three properties of the wrapper element style should be changed to reflect the width, height and url of your custom button image. Everything else can be left alone. (Different buttons can be used for different file inputs by adding an additional class to the wrapper element and styling accordingly.)

The JavaScript

First include the SI.Files library. Simple enough:

<script type="text/javascript" src="/path/to/si.files.js"></script>

Then, either in your onload event handler or at some point after your file inputs appear in the source, call:

SI.Files.stylizeAll();

The library also offers two alternate methods of styling file inputs. Given an element id:

SI.Files.stylizeById('input-id');

Or given an element node:

SI.Files.stylize(HTMLInputNode);

Both alternates are useful when the form that contains your file input is loaded via Ajax.

How it works

We start with a simple replacement. The custom button image is set as the background-image of our wrapper element and dimensions are set to match.

Next we set the opacity of the file input itself to zero, effectively making it invisible but still clickable (something that can’t be achieved with display: none; or visibility: hidden;).

Finally, the JavaScript keeps the button portion of the the file input underneath the pointer whenever the mouse enters the wrapper element. A class of SI-FILES-STYLIZED (also configurable) is applied to the html element of the page for use as a styling context for compatible browsers.

Compatibility

This solution is known to work in:

  • IE 5.5+
  • Firefox 1.5+
  • Safari 2+

It is known to degrade gracefully in:

  • Opera
  • IE 5.01

The SI.Files library and example files have been downloaded 34015 times.

Previous
Ringles? Really?
Next
Rocking the iPhone on T-Mobile
Author
Shaun Inman
Posted
September 10th, 2007 at 3:04 pm
Categories
CSS
Design
JavaScript
Comments
029 (Now closed)

029 Comments

001

Genius. Pure and unadulterated evil genius.

Author
Dan Mall
Posted
Sep 10th, 2007 3:47 pm
002

Once Again, Shaun Inman is my hero for what would otherwise seem to be an impossible situation.

Author
Ryan Sonnenberg
Posted
Sep 10th, 2007 3:53 pm
003

Thanks guys. There’s definitely room for improvement.

Currently, there’s no indication when a file is already selected.

With an additional element sitting inside the wrapper you could use HTML text and your favorite bulletproof technique to make the button resize with the rest of the page.

I also wonder if screen readers would apply the visual opacity change like they erroneously do with display: none;.

It just occurred to me that this approach (sans JavaScript) would also work for styling <input type="submit"> if you’re adverse to <input type="image"> (I personally don’t care for the way they unnecessarily pass the x/y coordinates as part of the query string when used with GET).

Author
Shaun Inman
Posted
Sep 10th, 2007 4:03 pm
004

Well, I don’t pretend to know a lot about the JS running behind it, since I dabble mostly in HTML & CSS only, however from a visual perspective, I worked something up in Photoshop for a possible upload view that shows you the file that has been selected and then an upload option. Link

Author
Ryan Sonnenberg
Posted
Sep 10th, 2007 4:32 pm
005

I’ve used a similar approach for getting a custom select menu (set it as the background image, and set opactiy to 0). Works great! Good stuff here, man.

Author
Jeff Croft
Posted
Sep 10th, 2007 4:53 pm
006

Nice one. As with Jeff above, I’ve used this type of technique as part of a custom select menu. Now I can make file uploading just as pretty.

I’ve been meaning to dive into the way flickr allows you to upload files and see how they are doing things but I’ve not yet had the time.

Author
Sam Brown
Posted
Sep 10th, 2007 5:01 pm
007

One question about the CSS:

Why so explicit with: filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);

Why not just: filter: alpha(opacity=0);

Author
Stephen Stchur
Posted
Sep 10th, 2007 5:27 pm
008

Why not just: filter: alpha(opacity=0);

I just do it the way Microsoft documents it.

Author
Shaun Inman
Posted
Sep 10th, 2007 5:34 pm
009

Shaun,

One other thing. Unless I’m doing something wrong, the cursor: pointer doesn’t seem to be kicking in in Firefox 2 (at least not in the Mint settings page).

Any idea why?

Author
Stephen Stchur
Posted
Sep 10th, 2007 5:39 pm
010

Nope. Just goes to show how limited the styling options are for file inputs—you can’t even change the cursor. IIRC, Safari is the only browser that honors the cursor property on a file input.

Author
Shaun Inman
Posted
Sep 10th, 2007 5:43 pm
011

This doesn’t seem to work in Safari 3.0.3. Can someone else verify this, because I might just be missing something. At any rate, the mini screenshot looks great.

Author
Karan Lyons
Posted
Sep 10th, 2007 7:20 pm
012

Good fucking idea.

Author
M.e.
Posted
Sep 10th, 2007 7:25 pm
013

Very handy. I was doing something similar (minus the JS) for a submit button today and will definitely try this out for file inputs.

Author
Adam
Posted
Sep 10th, 2007 7:43 pm
014

As said in comment 3, this has one major usability problem: the user doesn’t have any feedback about which file has been selected.
And another usability issue: no feedback about the focus state.

Author
Philippe
Posted
Sep 10th, 2007 8:23 pm
015

Damn UR sexy

Author
Josiah
Posted
Sep 10th, 2007 9:58 pm
016

Couldn’t you alter the graphic to represent a value inside it (and even list the file path) by attaching an onchange listener to the input field?

Author
louis w
Posted
Sep 10th, 2007 10:27 pm
017

Just wondering… does this work without changes with any of the drag-and-drop file upload plugins for Firefox?

Author
Bryant Cutelr
Posted
Sep 10th, 2007 11:47 pm
018

This technique was invented by Michael McGrady years ago. michaelmcgrady.com/file_browse_images.jsp

Author
Robbert Broersam
Posted
Sep 11th, 2007 1:40 am
019

yep, in safari 3.03 its doesnt work

Author
brainopia
Posted
Sep 11th, 2007 2:12 am
020

What, exactly, is the JavaScript doing? I don’t see why it’s necessary to make this work.

Author
Darryl McAdams
Posted
Sep 11th, 2007 3:00 am
021

“This simple, three-part progressive enhancement provides the markup, CSS, and JavaScript to address the long-standing irritation.”

Chill, jotos. He’s not claiming he invented it.

Author
Colin
Posted
Sep 11th, 2007 3:03 am
022

I like what you’re doing here, but as long as you’re taking away usability it’s not worth the hassle.

Author
Wolf
Posted
Sep 11th, 2007 3:48 am
023

Is there a way for the user to know which file he has selected?

Is there a way for the user to change his mind and revoke the file without resetting/reloading the form?

Here is how you could have done it: disney.co.uk/toontown/contactus.html (Select “technical question”, then look for the “Screenshot” section.)

Author
Wabbitseason
Posted
Sep 11th, 2007 4:01 am
024

Darryl McAdams:

Finally, the JavaScript keeps the button portion of the the file input underneath the pointer whenever the mouse enters the wrapper element

Author
Paul Decowski
Posted
Sep 11th, 2007 4:02 am
025

Yes, Wabbitseason. I like that implementation.

Author
hcabbos
Posted
Sep 11th, 2007 6:46 am
026

Opera 9 does have support for opacity, in the DOM too!

Instead of having narrow, hardcoded and not future-proof check for a certain browsers, you’d better use object detection that does specifically check for opacity support:

if (some_element.style.opacity !== undefined) {works}

Author
kL
Posted
Sep 11th, 2007 8:02 am
027

hi shaun, great tips, but like someone sayd, there’s no pointer when rollover, do you think it’s possible to use an additional image on rollover ?

Author
graffiti
Posted
Sep 11th, 2007 8:25 am
028

AFAIK Safari 3 is still in beta. I’ll worry about it once it’s officially released. That said it works fine for me in Safari 3.03b on XP.

Testing for elem.style.opacity doesn’t work (did you even bother testing this superior alternative before commenting kL?). Even if it did work, IE PC 5.5 & 6 would fail the test.

Before I added the code to omit Opera, opacity was observed not to work in Opera 9.22 Mac. Opera users make up less than 2% of the visitors on the site this technique was developed for. It degrades gracefully. My job is done.

Seems I forgot my usual disclaimer: this script is offered as is. It works for me. If it doesn’t work for you—technically, morally, whatever—don’t use it. Like the Michael McGrady/Disney method of styling inputs better? Use it. Think you can improve on my approach? Do it. Once you’ve done the work, share it. And then get on with your life.

Author
Shaun Inman
Posted
Sep 11th, 2007 10:20 am
029

Unfortunately, in IE7 the file control can track outside of the container-tag. Adding the following code to your javascript solves the problem:

var x = e.pageX - ox;
var y = e.pageY - oy;
var w = this.file.offsetWidth;
var h = this.file.offsetHeight;

/// ADDED CODE START ------------------
    // Fix for IE7+, otherwise the control can get dragged outside despite overflow: hidden;
    if (x < 0 || y < 0 || x > this.offsetWidth || y > this.offsetHeight) { 
        x = 0; y = 0; h = 0; w = 30;
    }
/// ADDED CODE END ---------------

this.file.style.top     = y - (h / 2)  + 'px';
this.file.style.left    = x - (w - 30) + 'px';
Author
jxtps
Posted
Feb 9th, 2009 11:46 am