Styler un champ d'upload en CSS

En préambule, je tiens à rassurer ceux qui risquent l'énucléation à la vue du titre de cet article : non, je n'ai pas découvert de solution miracle capable d'outrepasser les limites intrinsèques des navigateurs quant à l'application de styles CSS sur les input[type=file] ! Il s'agit simplement d'une astuce que tout le monde ne connaît peut-être pas et peut se révéler un atout précieux dans la tâche fastidieuse qu'est le design de formulaires HTML.

L'idée est tout simplement de masquer l'input originel, et de transférer grâce à JavaScript son contrôle à d'autres éléments mis en place à cet effet. Commençons par le HTML :

<label>
<input type="file" name="my_file" id="upload" />
<input type="text" id="fake_upload" disabled="disabled" />
<button>Parcourir</button>
</label>

Ce markup est peut-être discutable, étant considéré comme invalide par le W3C : il me paraît personnellement sensé, s'agissant d'un cas très spécifique. À vous de voir !

Voyons ensuite le code CSS lié au fameux champ d'upload. Je passe volontairement sur le champ texte et le bouton, l'objectif étant de leur donner l'apparence que vous souhaitez !

label [type=file] {
position: absolute;
width: 0;
height: 0;
}

Pourquoi s'ennuyer à jouer sur les dimensions alors qu'il suffirait d'un classique mais efficace display: none, me demanderez-vous ? Tout simplement parce sur certains navigateurs (notamment Google Chrome sur Android à l'heure où j'écris ces lignes), les éléments masqués ne peuvent être la cible d'évènements JS. Le positionnement absolu est là pour sortir l'input du flux afin d'éviter un éventuel décalage de quelques pixels (pouvant se produire, et je choisis un exemple tout à fait au hasard, sur Internet Explorer).

Passons maintenant au code JS, pour lequel nous nous appuierons sur jQuery. La difficulté ici est que certains navigateurs (comme par exemple Google Chrome) transmettent automatiquement l'évènement click sur le label à l'input[type=file] qu'il contient, alors que d'autres (tel Mozilla Firefox) n'en font rien. Il nous faut donc ruser un brin :

$('label').on('click', function(e)
{
// Vérifions en premier lieu la cible "réelle" du clic/toucher
var target = e.target || e.srcElement;

// S'il s'agit du bouton, on transmet au champ d'upload
if ($(target).is('button')) {
$(this).find('[type=file]').click();
}

// Lors du clic simulé ci-dessus, ce même handler est appelé à nouveau
// On empêche donc l'éventuel comportement par défaut sauf dans ce cas précis
return $(target).is('[type=file]');
});

$('label [type=file]').on('change', function()
{
// Lorsqu'un fichier est choisi, on remplit notre "faux" input avec son nom
$(this).siblings('[type=text]').val($(this).val());
});

Cette astuce a été testée sans succès sur IE (dans ses versions 8 et 9) : si tout semble fonctionner en apparence, ce dernier refusera catégoriquement de procéder correctement à l'upload du fichier si la pop-up de sélection n'a pas été ouverte via un clic sur l'élément natif (sans JavaScript). Si vous avez une solution, je suis preneur ! Dans l'intervalle, je vous recommande de masquer les éléments décoratifs et de réafficher le "vrai" input[type=file] pour IE.