September
2009

Styling HTML file upload elements

File upload elements are some of the most annoying elements on a website, since they don’t accept much CSS styling and every browser and every operating system displays them totally different. Safari displays the button on the left and the filename on the right, Firefox displays it the other way round. If you need to build a website from a fancy design, an ugly upload field can ruin everything and doesn’t look very professional.

Nevertheless with a few tricks it is still possible to have a custom file upload element that looks the same on all platforms in all browsers. There are already a few solutions out there to achieve it, but the ones that I found were either a bit dated or too complicated, so here’s a revisited and simplified version using MooTools.

file upload elements

The Markup

<div id="wrapper">
	<span id="browse"></span>
	<span id="filename">no file selected</span>
	<input type="file" id="fileinput" />
</div>

The markup is very simple. There is a wrapper container, a container for the browse button, one where the filename will be displayed and of course, the file input element. The input element will be placed on top of the browse button. The basic trick is to make the file input element transparent, so people will only see the browse button below and if they click it, they actually click the input element which opens the browse dialog.

The CSS

#wrapper {
	width: 300px;
	height: 100px;
	float: left;
	position: relative;
}
#fileinput {
	position: absolute;
	width: 75px;
	height: 26px;
	top: 0;
	left: 0;
	visibility: hidden;
}
#browse {
	position: absolute;
	width: 75px;
	height: 26px;
	top: 0;
	left: 0;
	background: url(button.png) left top no-repeat;
}
#filename {
	position: absolute;
	width: 175px;
	height: 26px;
	left: 75px;
	text-align: center;
	background: url(filename.png) left top no-repeat;
}

What’s important here, is, that the wrapper container has position: relative. This allows the three other elements to be positioned absolute within the wrapper container. Change the width, height and background graphic for both the button and the filename field to the values of your own image files. Make sure the file input element is positioned exactly on top of the button graphic. In the example I used a separate background image for both the browse button and the area where the filename will be displayed.

The Javascript

window.addEvent('domready', function() {
	$('fileinput').setStyles({
		'opacity': 0,
		'visibility': 'visible'
	});

	$('fileinput').addEvent('change', function() {
		$('filename').set('text', this.value);
	});
});

I chose to use MooTools here, but you can use plain Javascript or any other framework, it’s not difficult to rewrite the few lines. If you decide to copy the code, don’t forget to include the MooTools library.

First of all the file input element will be made transparent using opacity. The only reason why I’m setting this with MooTools and not directly in CSS is purely convenience, to make sure it works the same in all browsers. Then a change event is defined on the hidden file upload element. Whenever its value (the selected file) changes, a copy is written into the filename span so we can actually see it.

Note: First the fileupload element is set to visibility: hidden in the CSS and then changed to visibility: visible via Javascript. The reason is that the script code will be executed once the DOM is ready, so basically when the page finished loading. On slow connections this could mean visitors would see a plain unformatted file upload element before the script kicks in and hides it. By hiding it in CSS first this will be avoided.

Enhanced usability: hover and cursor

At this point the example is already complete, however with a little bit of tuning it can be made more userfriendly.

One of the most basic rules of interface design is, clickable elements need to indicate that they are actually clickable. For this there are two basic techniques: adding a hover effect and changing the mouse cursor. To change the browse button on mouse-over, change the Javascript so it looks like this:

window.addEvent('domready', function() {
	$('fileinput').setStyles({
		'opacity': 0,
		'visibility': 'visible'
	});

	$('fileinput').addEvent('change', function() {
		$('filename').set('text', this.value);
	});

	$('fileinput').addEvent('mouseenter', function() {
		$('browse').setStyle('background-image', 'url(button_hover.png)');
	});
	$('fileinput').addEvent('mouseleave', function() {
		$('browse').setStyle('background-image', 'url(button.png)');
	});
});

Two events are added to the hidden file upload element. On mouse-over the browse button background changes to the hover version, on mouse-out it changes back to normal. You cannot simply add a #browse:hover property to the CSS because the browse button is positioned behind the file upload element, so it won’t work.

Changing the cursor on the upload element to cursor: pointer is not possible, unfortunately. It may work in some browsers, but in my tests it didn’t work reliably in all browsers, no matter if it was set via CSS or dynamically via Javascript. The file upload element seems to be a bit resistant against cursor changes.

One solution to fix this would be to change the above markup structure so the file upload element is in the background, and not on top:

<div id="wrapper">
	<input type="file" id="fileinput" />
	<span id="browse"></span>
	<span id="filename">no file selected</span>
</div>

We could then safely add cursor: pointer to the browse button’s CSS and add the following Javascript:

$('browse').addEvent('click', function() {
	$('fileinput').click();
});	

Since the file upload element is now in the background, it doesn’t recognise clicks anymore and therefore the browse dialog needs to be triggered manually. This can be done by calling the click() method on the upload element to simulate a click. Unfortunately this is pure theory, it does not work, since the nerds at Mozilla and Opera decided not to support the click() method on file upload elements for whatever reason:

The click method is intended to be used with INPUT elements of type button, checkbox, radio, reset or submit. Gecko does not implement the click method on other elements that might be expected to respond to mouse–clicks such as links (A elements), nor will it necessarily fire the click event of other elements.

Non–Gecko DOMs may behave differently.

And they are right, in Safari and Internet Explorer it works indeed – one more reason to dump Firefox and use a WebKit browser instead.

As far as I can see this means cursors cannot be changed to pointer, so make sure you use at least a hover image.

Enhanced usability: truncate file paths

I seems like browsers do not only display the form element differently, there are also some that display the filename including full path, while others only display the filename. To make it more userfriendly, change the Javascript to the following:

function strrpos (haystack, needle, offset) {
	var i = (haystack+'').lastIndexOf( needle, offset );
	return i >= 0 ? i : false;
}

function strlen (string) {
	var str = string+'';
	var i = 0, chr = '', lgth = 0;

	var getWholeChar = function (str, i) {
		var code = str.charCodeAt(i);
		var next = '', prev = '';
		if (0xD800 <= code && code <= 0xDBFF) {
			if (str.length <= (i+1))  {
				throw 'High surrogate without following low surrogate';
			}
			next = str.charCodeAt(i+1);
			if (0xDC00 > next || next > 0xDFFF) {
				throw 'High surrogate without following low surrogate';
			}
			return str.charAt(i)+str.charAt(i+1);
		} else if (0xDC00 <= code && code <= 0xDFFF) {
			if (i === 0) {
				throw 'Low surrogate without preceding high surrogate';
			}
			prev = str.charCodeAt(i-1);
			if (0xD800 > prev || prev > 0xDBFF) {
				throw 'Low surrogate without preceding high surrogate';
			}
			return false;
		}
		return str.charAt(i);
	};

	for (i=0, lgth=0; i < str.length; i++) {
		if ((chr = getWholeChar(str, i)) === false) {
			continue;
		}
		lgth++;
	}
	return lgth;
}

function substr (f_string, f_start, f_length) {
	f_string += '';

	if (f_start < 0) {
		f_start += f_string.length;
	}

	if (f_length == undefined) {
		f_length = f_string.length;
	} else if (f_length < 0){
		f_length += f_string.length;
	} else {
		f_length += f_start;
	}

	if (f_length < f_start) {
		f_length = f_start;
	}

	return f_string.substring(f_start, f_length);
}

window.addEvent('domready', function() {
	$('fileinput').setStyles({
		'opacity': 0,
		'visibility': 'visible'
	});

	$('fileinput').addEvent('change', function() {
		var file = this.value;
		var length = strlen(file);
		var slash = strrpos(file, '/');
		var backslash = strrpos(file, '\\');
		if (slash) {
			file = substr(file, slash + 1, length);
		} else if (backslash) {
			file = substr(file, backslash + 1, length);
		}

		$('filename').set('text', file);
	});
	$('fileinput').addEvent('mouseenter', function() {
		$('browse').setStyle('background-image', 'url(button_hover.png)');
	});
	$('fileinput').addEvent('mouseleave', function() {
		$('browse').setStyle('background-image', 'url(button.png)');
	});
});

Three new functions were added (taken from PHPJSLicense) and the change event was extended. Before the upload element’s value is copied into the filename span, it will be scanned for slashes (Unix paths) and backslashes (Windows paths). If one is found, the string gets truncated and only the file name at the end is copied.

Comments

  • rami gershuni

    Thanks. this is exactly what i needed and very nicely explained and designed by you.