Introduction
Dans ce tutoriel, vous allez construire un créateur d'avatar interactif entièrement en HTML, CSS et JavaScript — sans aucune bibliothèque externe.
Le principe repose sur la superposition de couches d'images PNG : peau, cheveux, yeux, vêtements… chaque couche est dessinée dans l'ordre sur un canvas HTML5. L'utilisateur navigue entre les catégories, sélectionne une vignette ou une couleur, et l'aperçu se met à jour instantanément.
Prérequis
Avant de commencer, assurez-vous de disposer des éléments suivants :
Éditeur de code
Nous recommandons Visual Studio Code, mais tout éditeur de texte convient.
Notions HTML / CSS / JS
Vous devez maîtriser les bases du DOM (sélecteurs, événements), les propriétés CSS courantes et les fonctions JavaScript. Les notions d'async/await seront introduites progressivement.
Éléments de l'avatar
Préparez au moins 4 à 5 images par catégorie (coiffures, vêtements, yeux…). Format PNG obligatoire pour la transparence, taille identique pour toutes les couches — idéalement 1000×1000 px.
Un kit de démarrage est mis à disposition : il inclut la structure de dossiers prête à l'emploi, les fichiers JavaScript de base et un jeu d'images d'exemple pour démarrer sans attendre.
Architecture des dossiers
Voici l'arborescence complète du projet. Chaque catégorie d'avatar est un dossier dans sections/ — les catégories avec couleur ou genre imbriquent des sous-dossiers supplémentaires.
📦avatar-creator ┣ 📂sections ┃ ┣ 📂Eyes ┃ ┃ ┣ 📜1.png ┃ ┃ ┗ 📜(...).png ┃ ┣ 📂Hair ┃ ┃ ┣ 📂1 ┃ ┃ ┃ ┣ 📜black.png ┃ ┃ ┃ ┗ 📜(...).png ┃ ┃ ┗ 📂(...) ┃ ┣ 📂Mouth ┃ ┣ 📂Nose ┃ ┣ 📂Skin ┃ ┃ ┣ 📂men ┃ ┃ ┗ 📂women ┃ ┗ 📂Top ┃ ┣ 📂men ┃ ┃ ┣ 📂1 ┃ ┃ ┃ ┣ 📜black.png ┃ ┃ ┃ ┗ 📜(...).png ┃ ┃ ┗ 📂(...) ┃ ┗ 📂women ┣ 📂UI ┣ 📜global.js ┣ 📜index.html ┣ 📜reset.css ┗ 📜style.css
Bonnes pratiques
Utilisez des noms anglais pour les dossiers — ils seront repris directement comme valeurs data-element dans le HTML et comme clés dans avatarState.
Fichier reset.css
Un fichier reset.css neutralise les marges et styles par défaut du navigateur pour garantir un rendu identique sur tous les environnements.
Schéma d'une section avec couleur : [Nom]/[numéro]/[couleur.png] — par exemple Hair/3/blond.png. Pour les sections sans couleur, c'est simplement [Nom]/[numéro].png. Le dossier UI/ accueille les éléments graphiques d'interface.
HTML 01
La page est divisée en deux colonnes : à gauche le canvas d'aperçu en temps réel, à droite la navigation par catégorie et les vignettes de sélection.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Creation d'avatar</title>
</head>
<body>
</body>
</html>
La colonne de gauche (#leftSection) n'affiche que le canvas. La colonne de droite (#rightSection) contient un <header> pour les boutons de navigation et un <main> pour les sections de vignettes.
<body>
<div class="grid-2">
<div id="leftSection">
<div class="result">
<canvas id="resultFinal" width="1000" height="1000"></canvas>
</div>
</div>
<div id="rightSection">
<header>
<!-- Navigation des différentes sections -->
</header>
<main>
<!-- Sections -->
</main>
</div>
</div>
</body>
<canvas> permet de dessiner les couches dans un ordre précis et d'exporter le résultat final en PNG en un clic. C'est bien plus propre que d'empiler des <img> en position absolue.
Navigation
Chaque bouton porte un attribut data-target dont la valeur correspond à l'id de la section à afficher. Le JavaScript lira cette valeur pour basculer entre les sections.
<header>
<nav id="categories">
<ul>
<li><button type="button" data-target="Skin" class="active">Peau</button></li>
<li><button type="button" data-target="Mouth">Bouche</button></li>
<li><button type="button" data-target="Nose">Nez</button></li>
<li><button type="button" data-target="Eyes">Regard</button></li>
<li><button type="button" data-target="Hair">Cheveux</button></li>
<li><button type="button" data-target="Top">Vetement</button></li>
</ul>
</nav>
</header>
Ajoutez un bouton de téléchargement à côté de la navigation. Il sera connecté au canvas en JavaScript pour déclencher l'export de l'avatar en PNG.
<button id="download">Mon Avatar est prêt !</button>
Sections d'avatar
Chaque section correspond à une catégorie d'avatar. Elle contient une grille de vignettes — chaque <li class="vignette"> porte un attribut data-element dont la valeur sera utilisée par JavaScript pour construire le chemin de l'image à charger sur le canvas.
Options partagées
Le genre sélectionné dans la section Peau (men ou women) est mémorisé dans la variable currentGender et répercuté automatiquement sur toutes les sections dépendantes, comme les vêtements.
Système de couleurs
Un clic sur une pastille met à jour la valeur color dans avatarState. JavaScript reconstruit alors le chemin de l'image — par exemple Hair/3/blond.png — et redessine le canvas.
data-* s'applique à toutes les catégories, il suffit de l'adapter.
Section sans options
Pour les catégories sans variante de couleur ni de genre (bouche, nez, yeux…), la structure est simple : une liste de vignettes numérotées, chacune pointant vers son image correspondante.
<section id="Mouth">
<ul class="vignettes">
<li class="vignette active" data-element="1"><img src="sections/Mouth/1.png" alt="Mouth - 1"></li>
<li class="vignette" data-element="2"><img src="sections/Mouth/2.png" alt="Mouth - 2"></li>
<li class="vignette" data-element="3"><img src="sections/Mouth/3.png" alt="Mouth - 3"></li>
<li class="vignette" data-element="4"><img src="sections/Mouth/4.png" alt="Mouth - 4"></li>
<li class="vignette" data-element="5"><img src="sections/Mouth/5.png" alt="Mouth - 5"></li>
<li class="vignette" data-element="6"><img src="sections/Mouth/6.png" alt="Mouth - 6"></li>
<li class="vignette" data-element="7"><img src="sections/Mouth/7.png" alt="Mouth - 7"></li>
<li class="vignette" data-element="8"><img src="sections/Mouth/8.png" alt="Mouth - 8"></li>
</ul>
</section>
Section avec genre (Peau)
La section Peau est particulière : le choix de vignette définit le genre (men ou women), et la palette de couleurs représente les carnations disponibles. La classe .active sur la <section> la rend visible au démarrage.
<section id="Skin" class="active">
<ul class="vignettes">
<li class="vignette active" data-element="men"><img src="sections/Skin/men/1.png" alt="Skin - men"></li>
<li class="vignette" data-element="women"><img src="sections/Skin/women/1.png" alt="Skin - women"></li>
</ul>
<ul class="colors">
<li class="color active" data-color="1" style="background-color: #eed8c5;"></li>
<li class="color" data-color="2" style="background-color: #d1b39a;"></li>
<li class="color" data-color="3" style="background-color: #bd9574;"></li>
<li class="color" data-color="4" style="background-color: #ba875e;"></li>
<li class="color" data-color="5" style="background-color: #9b6e4a;"></li>
<li class="color" data-color="6" style="background-color: #775235;"></li>
<li class="color" data-color="7" style="background-color: #68513f;"></li>
<li class="color" data-color="8" style="background-color: #64554b;"></li>
</ul>
</section>
Section avec palette de couleurs (Cheveux)
Chaque vignette affiche par défaut la variante black.png. La palette de couleurs permet de changer cette teinte dynamiquement — JavaScript reconstruit le chemin à chaque sélection. Ce même schéma s'applique à tout élément avec variantes de couleur.
<section id="Hair">
<ul class="vignettes">
<li class="vignette active" data-element="1"><img src="sections/Hair/1/black.png" alt="Hair - 1"></li>
<li class="vignette" data-element="2"><img src="sections/Hair/2/black.png" alt="Hair - 2"></li>
<li class="vignette" data-element="3"><img src="sections/Hair/3/black.png" alt="Hair - 3"></li>
<li class="vignette" data-element="4"><img src="sections/Hair/4/black.png" alt="Hair - 4"></li>
<li class="vignette" data-element="5"><img src="sections/Hair/5/black.png" alt="Hair - 5"></li>
<li class="vignette" data-element="6"><img src="sections/Hair/6/black.png" alt="Hair - 6"></li>
<li class="vignette" data-element="7"><img src="sections/Hair/7/black.png" alt="Hair - 7"></li>
<li class="vignette" data-element="8"><img src="sections/Hair/8/black.png" alt="Hair - 8"></li>
<li class="vignette" data-element="9"><img src="sections/Hair/9/black.png" alt="Hair - 9"></li>
<li class="vignette" data-element="10"><img src="sections/Hair/10/black.png" alt="Hair - 10"></li>
</ul>
<ul class="colors">
<li class="color active" data-color="black" style="background-color: #000000;"></li>
<li class="color" data-color="darkbrown" style="background-color: #373018;"></li>
<li class="color" data-color="brown" style="background-color: #7e623e;"></li>
<li class="color" data-color="darkred" style="background-color: #a9612e;"></li>
<li class="color" data-color="ginger" style="background-color: #db792e;"></li>
<li class="color" data-color="blond" style="background-color: #dede7e;"></li>
<li class="color" data-color="gray" style="background-color: #888888;"></li>
<li class="color" data-color="white" style="background-color: #ededed;"></li>
</ul>
</section>
Section avec genre + couleur (Vêtements)
Les vêtements combinent genre et couleur. JavaScript lit currentGender (défini dans la section Peau) et la couleur sélectionnée pour construire le chemin correct — par exemple sections/Top/men/1/red.png.
<section id="Top">
<ul class="vignettes">
<li class="vignette active" data-element="1"><img src="sections/Top/men/1/red.png" alt="Top - 1"></li>
<li class="vignette" data-element="2"><img src="sections/Top/men/2/red.png" alt="Top - 2"></li>
<li class="vignette" data-element="3"><img src="sections/Top/men/3/red.png" alt="Top - 3"></li>
<li class="vignette" data-element="4"><img src="sections/Top/men/4/red.png" alt="Top - 4"></li>
<li class="vignette" data-element="5"><img src="sections/Top/men/5/red.png" alt="Top - 5"></li>
<li class="vignette" data-element="6"><img src="sections/Top/men/6/red.png" alt="Top - 6"></li>
</ul>
<ul class="colors">
<li class="color" data-color="black" style="background-color: #000000;"></li>
<li class="color" data-color="grey" style="background-color: #888888;"></li>
<li class="color" data-color="white" style="background-color: #ffffff;"></li>
<li class="color active" data-color="red" style="background-color: #ff0000;"></li>
<li class="color" data-color="orange" style="background-color: #ff8800;"></li>
<li class="color" data-color="yellow" style="background-color: #ffff00;"></li>
<li class="color" data-color="green" style="background-color: #44ff44;"></li>
<li class="color" data-color="blue" style="background-color: #0000ff;"></li>
<li class="color" data-color="purple" style="background-color: #aa44aa;"></li>
</ul>
</section>
La structure HTML est prête. Passons maintenant au CSS pour mettre en page l'interface et lui donner son apparence.
CSS 02
Créez style.css et liez-le dans le <head>. Ajoutez également un reset CSS en premier pour neutraliser les marges et styles par défaut des navigateurs.
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Creation d'avatar</title>
<link rel="stylesheet" href="reset.css">
<link rel="stylesheet" href="style.css">
</head>
Mise en page
/*---GLOBAL---*/
.grid-2 {
display: grid;
grid-template-columns: 40% 60%;
grid-gap: 0px;
}
#leftSection {
background-color: #eaeaea;
padding: 16px;
font-size: 16px;
}
#rightSection {
background-color: #1f1f1f;
color: white;
padding: 16px;
font-size: 16px;
}
Navigation
La navigation utilise flexbox pour aligner les boutons horizontalement. La classe .active indique la catégorie ouverte — elle sera ajoutée et retirée dynamiquement par JavaScript.
/*---NAVIGATION---*/
header {
display: flex;
justify-content: space-between;
align-items: stretch;
gap: 16px;
}
header nav ul {
display: flex;
gap: 16px;
padding: 8px;
background-color: white;
border-radius: 8px;
box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;
}
button {
padding: 8px;
border-radius: 8px;
border: none;
background-color: #eaeaea;
color: black;
cursor: pointer;
border: solid 2px #eaeaea;
transition: all ease-in-out 0.5s;
}
button.active, button:hover {
background-color: #1f1f1f;
color: white;
}
Résultats
Le canvas occupe toute la largeur de son conteneur. En déclarant width="1000" height="1000" en HTML (qui fixe le ratio interne) et width: 100%; height: auto en CSS, le navigateur redimensionne l'élément en respectant son ratio carré — sans JavaScript.
/*---RESULT---*/
.result {
overflow: hidden;
max-width: 100%;
background-color: white;
border-radius: 1rem;
margin: 1rem 2rem;
box-shadow: rgba(0, 0, 0, 0.15) 0px 2px 8px;
}
#resultFinal {
width: 100%;
height: auto;
display: block;
border-radius: 1rem;
}
Sections & vignettes
Les vignettes sont disposées en grille responsive avec auto-fill — le nombre de colonnes s'adapte à la largeur disponible. Le pseudo-élément ::after avec padding-bottom: 100% est une astuce CSS classique pour forcer un ratio carré, quelle que soit la taille de la vignette.
/*---SECTIONS---*/
main section {
display: none;
}
main section.active {
padding: 16px 0px;
display: block;
}
/*---VIGNETTES---*/
.vignettes {
overflow-y: auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
grid-auto-rows: max-content;
grid-gap: 16px;
}
.vignette {
transition: all ease-in-out 0.5s;
cursor: pointer;
transition-duration: 300ms;
box-shadow: rgba(0,0,0,0.18) 0px 2px 4px;
border-radius: 16px;
position: relative;
background-color: white;
overflow: hidden;
}
.vignette:after {
content: "";
display: block;
padding-bottom: 100%;
}
.vignette img {
position: absolute;
top: 0;
left: 0;
display: block;
width: 100%;
max-width: 100%;
height: auto;
image-rendering: optimizequality;
}
Couleurs
La palette est une grille de petites pastilles colorées. La classe .active, ajoutée par JavaScript au clic, signale visuellement la couleur en cours.
/*---COULEURS---*/
.colors {
background-color: white;
padding: 1rem;
border-radius: 10px;
box-shadow: rgba(0,0,0,0.18) 0px 2px 4px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(50px, 1fr));
grid-gap: 8px;
height: max-content;
margin-top: 1rem;
}
.color {
display: block;
width: 50px;
height: 50px;
border-radius: 10px;
box-shadow: 0px 0px 0px 2px rgba(0, 0, 0, .3) inset;
transition: all ease-in-out 0.5s;
}
.color:active, .color:hover {
box-shadow: 0px 0px 0px 2px rgb(255, 255, 255) inset;
cursor: pointer;
}
JavaScript 03
Changer de catégorie
Au clic sur un bouton de navigation, on retire la classe .active de toutes les sections, puis on l'ajoute uniquement à la section dont l'id correspond au data-target du bouton cliqué.
const navigationButtons = document.querySelectorAll('#categories button');
const sections = document.querySelectorAll('main section');
function switchSection() {
navigationButtons.forEach(button => {
button.addEventListener('click', function () {
navigationButtons.forEach(b => b.classList.remove("active"));
this.classList.add("active");
const dataTarget = this.getAttribute('data-target');
const sectionTarget = document.getElementById(dataTarget);
sections.forEach(section => section.classList.remove("active"));
sectionTarget.classList.add("active");
});
});
}
switchSection();
État de l'avatar
avatarState est la source de vérité unique de l'avatar : il mémorise l'élément et la couleur actifs pour chaque section. La fonction getLayerSrc traduit cet état en chemin de fichier, en tenant compte du genre et des variantes de couleur.
let currentGender = 'men';
const sectionsWithGender = ['Top'];
const sectionsWithColor = ['Skin', 'Hair', 'Top'];
const layerOrder = ['Skin', 'Hair', 'Eyes', 'Mouth', 'Nose', 'Top'];
const avatarState = {
Skin: { element: 'men', color: '1' },
Hair: { element: '1', color: 'black' },
Eyes: { element: '1' },
Mouth: { element: '1' },
Nose: { element: '1' },
Top: { element: '1', color: 'red' }
};
function getLayerSrc(name) {
const s = avatarState[name];
if (sectionsWithColor.includes(name)) {
if (sectionsWithGender.includes(name))
return `sections/${name}/${currentGender}/${s.element}/${s.color}.png`;
return `sections/${name}/${s.element}/${s.color}.png`;
}
if (sectionsWithGender.includes(name))
return `sections/${name}/${currentGender}/${s.element}.png`;
return `sections/${name}/${s.element}.png`;
}
Rendu sur Canvas
renderCanvas charge toutes les couches en parallèle grâce à Promise.all — bien plus rapide que de les charger une par une. Une fois toutes les images disponibles, elles sont dessinées dans l'ordre défini par layerOrder sur un fond blanc. Cette fonction est appelée à chaque changement de sélection.
async function renderCanvas() {
const canvas = document.getElementById('resultFinal');
const ctx = canvas.getContext('2d');
const images = await Promise.all(
layerOrder.map(name => new Promise(resolve => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => resolve(null);
img.src = getLayerSrc(name);
}))
);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const img of images) {
if (img) ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
}
renderCanvas();
Gestion des vignettes
Au clic sur une vignette, on met à jour avatarState[sectionName].element avec la valeur de data-element, puis on rappelle renderCanvas(). La section Skin est un cas particulier : son choix de vignette définit le genre, donc currentGender est également mis à jour.
function changeShape() {
for (const section of sections) {
const sectionName = section.getAttribute('id');
for (const vignette of section.querySelectorAll('.vignette')) {
vignette.addEventListener('click', function () {
this.parentElement.querySelector('.active')?.classList.remove('active');
this.classList.add("active");
avatarState[sectionName].element = this.getAttribute('data-element');
if (sectionName === 'Skin') currentGender = avatarState.Skin.element;
renderCanvas();
});
}
}
}
changeShape();
Gestion des couleurs
La gestion des couleurs suit le même schéma : on met à jour avatarState[sectionName].color avec la valeur data-color de la pastille cliquée, puis on redessine le canvas.
function changeColor() {
for (const section of sections) {
const sectionName = section.getAttribute('id');
for (const color of section.querySelectorAll('.color')) {
color.addEventListener('click', function () {
this.parentElement.querySelector('.active')?.classList.remove('active');
this.classList.add("active");
avatarState[sectionName].color = this.getAttribute('data-color');
renderCanvas();
});
}
}
}
changeColor();
Téléchargement
L'un des avantages du canvas : l'export natif. canvas.toDataURL('image/png') retourne l'image encodée en base64, qu'il suffit d'affecter au href d'un lien temporaire pour déclencher le téléchargement. Assurez-vous que la variable canvas est accessible dans la portée de ce code.
document.getElementById('download').addEventListener('click', () => {
const link = document.createElement('a');
link.download = 'mon-avatar.png';
link.href = canvas.toDataURL('image/png');
link.click();
});
Randomisation Optionnel
La fonction randomize parcourt toutes les sections, tire au sort une vignette et (si applicable) une couleur, met à jour avatarState en conséquence, puis redessine le canvas. Il suffit d'un bouton avec id="randomize" pour la déclencher.
<button id="randomize">Aléatoire</button>
function randomize() {
const pick = arr => arr[Math.floor(Math.random() * arr.length)];
for (const section of sections) {
const sectionName = section.getAttribute('id');
const vignettes = [...section.querySelectorAll('.vignette')];
if (!vignettes.length) continue;
const v = pick(vignettes);
section.querySelector('.vignette.active')?.classList.remove('active');
v.classList.add('active');
avatarState[sectionName].element = v.getAttribute('data-element');
if (sectionName === 'Skin') currentGender = avatarState.Skin.element;
const colors = [...section.querySelectorAll('.color')];
if (colors.length) {
const c = pick(colors);
section.querySelector('.color.active')?.classList.remove('active');
c.classList.add('active');
avatarState[sectionName].color = c.getAttribute('data-color');
}
}
renderCanvas();
}
document.getElementById('randomize').addEventListener('click', randomize);
Automatisation PHP Optionnel
Quand le projet grossit, maintenir manuellement la liste des <li> pour chaque image devient fastidieux et source d'erreurs. En renommant index.html en index.php, on peut déléguer cette tâche au serveur : PHP comptera les images dans chaque dossier via glob() et générera les vignettes automatiquement.
index.html → index.php.
Compter les images avec glob()
glob() retourne un tableau de tous les fichiers ou dossiers correspondant à un motif. Placez ces comptages en tout début de fichier PHP, avant toute sortie HTML — les variables seront disponibles dans tout le reste du template.
<?php
$mouthCount = count(glob('sections/Mouth/*.png'));
$noseCount = count(glob('sections/Nose/*.png'));
$eyesCount = count(glob('sections/Eyes/*.png'));
$hairCount = count(glob('sections/Hair/*/', GLOB_ONLYDIR));
?>
Générer les vignettes en boucle
On remplace la liste statique de <li> par une boucle PHP. Désormais, ajouter une image dans le dossier sections/Mouth/ suffit — le HTML se régénère automatiquement sans aucune modification du code.
<section id="Mouth">
<ul class="vignettes">
<?php for ($i = 1; $i <= $mouthCount; $i++): ?>
<li class="vignette <?= $i === 1 ? 'active' : '' ?>" data-element="<?= $i ?>">
<img src="sections/Mouth/<?= $i ?>.png" alt="Mouth - <?= $i ?>">
</li>
<?php endfor; ?>
</ul>
</section>
GLOB_ONLYDIR — glob('sections/Hair/*/', GLOB_ONLYDIR) retourne uniquement les dossiers numériques (1/, 2/…). Le nom de chaque dossier devient la valeur de data-element.
<section id="Hair">
<ul class="vignettes">
<?php for ($i = 1; $i <= $hairCount; $i++): ?>
<li class="vignette <?= $i === 1 ? 'active' : '' ?>" data-element="<?= $i ?>">
<img src="sections/Hair/<?= $i ?>/black.png" alt="Hair - <?= $i ?>">
</li>
<?php endfor; ?>
</ul>
<!-- Palette de couleurs -->
</section>
Passer les comptes à JavaScript
Si votre JavaScript a besoin de connaître le nombre d'éléments par section (pour la randomisation, par exemple), PHP peut écrire ces valeurs directement dans un bloc <script> au moment de la génération de la page.
<script>
const sectionCounts = {
Mouth: <?= $mouthCount ?>,
Nose: <?= $noseCount ?>,
Eyes: <?= $eyesCount ?>,
Hair: <?= $hairCount ?>
};
</script>