Prérequis
- Niveau : bases HTML/CSS/JS et événements (cf. événements JS).
- Outils : VS Code + Live Server, navigateur moderne.
- Temps estimé : 45 min.
Pourquoi un système de notation ?
Les avis et notes par étoiles multiplient le taux de conversion sur les pages produit, restaurant, formation. Un système maison (sans dépendance) tient en 50 lignes et reste personnalisable : demi-étoiles, animations, accessibilité au clavier, persistence en BDD.
Ce que vous allez construire
Un système de notation par étoiles interactif, comme ceux que vous voyez sur Amazon ou Google. L’utilisateur survole les étoiles pour prévisualiser sa note, clique pour la valider, et vous récupérez la valeur en JavaScript. Le tout en HTML, CSS et JS pur — zéro dépendance.
Le HTML : simple et sémantique
<div class="star-rating" id="starRating">
<span class="star" data-value="1">★</span>
<span class="star" data-value="2">★</span>
<span class="star" data-value="3">★</span>
<span class="star" data-value="4">★</span>
<span class="star" data-value="5">★</span>
</div>
<p>Votre note : <span id="ratingValue">0</span>/5</p>
★ est le caractère Unicode de l’étoile pleine (★). L’attribut data-value stocke la valeur de chaque étoile.
Le CSS : étoiles dorées et animations
.star-rating {
display: inline-flex;
gap: 4px;
direction: ltr;
}
.star {
font-size: 2.5rem;
color: #ddd;
cursor: pointer;
transition: color 0.15s ease, transform 0.15s ease;
user-select: none;
}
.star:hover { transform: scale(1.2); }
.star.actif {
color: #ffc107;
}
.star.hover {
color: #ffdb4d;
}
Le JavaScript : logique d’interaction
const starsContainer = document.getElementById('starRating');
const stars = starsContainer.querySelectorAll('.star');
const ratingValue = document.getElementById('ratingValue');
let currentRating = 0;
// Colorier les étoiles de 1 à N
function highlightStars(count, className) {
stars.forEach((star, index) => {
if (index < count) {
star.classList.add(className);
} else {
star.classList.remove(className);
}
});
}
// Au survol : prévisualisation
starsContainer.addEventListener('mouseover', (e) => {
if (e.target.classList.contains('star')) {
const value = parseInt(e.target.dataset.value);
highlightStars(value, 'hover');
}
});
// Quand la souris quitte : revenir à la note sélectionnée
starsContainer.addEventListener('mouseout', () => {
stars.forEach(star => star.classList.remove('hover'));
});
// Au clic : valider la note
starsContainer.addEventListener('click', (e) => {
if (e.target.classList.contains('star')) {
currentRating = parseInt(e.target.dataset.value);
highlightStars(currentRating, 'actif');
ratingValue.textContent = currentRating;
// Ici vous pouvez envoyer la note au serveur
console.log('Note sélectionnée :', currentRating);
}
});
Comment ça fonctionne
On utilise la délégation d’événements : au lieu d’attacher un écouteur à chaque étoile, on en attache un seul au conteneur parent. L’objet e.target identifie quelle étoile a été cliquée. C’est plus performant et plus maintenable que 5 écouteurs séparés.
Version avancée : demi-étoiles
Pour permettre les notes comme 3.5 ou 4.5, divisez chaque étoile en deux moitiés avec des pseudo-éléments :
.star {
position: relative;
display: inline-block;
font-size: 2.5rem;
color: #ddd;
cursor: pointer;
}
.star.half::before {
content: '★';
position: absolute;
left: 0;
width: 50%;
overflow: hidden;
color: #ffc107;
}
En JavaScript, détectez si le clic est dans la moitié gauche ou droite de l’étoile :
star.addEventListener('click', (e) => {
const rect = star.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const isHalf = clickX < rect.width / 2;
const value = parseInt(star.dataset.value);
currentRating = isHalf ? value - 0.5 : value;
});
Envoyer la note au serveur
async function envoyerNote(productId, rating) {
try {
const response = await fetch('/api/ratings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, rating })
});
if (response.ok) {
const data = await response.json();
console.log('Note enregistrée. Moyenne :', data.average);
}
} catch (error) {
console.error('Erreur lors de l\'envoi :', error);
}
}
Erreurs fréquentes
Inaccessible au clavier
Cause : on utilise des <span> non focalisables.
Solution : préférez des <button> ou ajoutez tabindex="0" + gestion keydown (Espace/Enter pour valider, flèches pour ajuster).
Pas d’aria-label
Cause : les lecteurs d’écran ne lisent que « étoile, étoile, étoile… ».
Solution : ajoutez aria-label="Note : X sur 5" sur chaque étoile et un rôle radiogroup sur le conteneur.
Demi-étoiles cliquées au mauvais moment
Cause : détection par clientX sans tenir compte du scroll/zoom.
Solution : utilisez getBoundingClientRect() + offsetX relatif à l’élément (déjà fait dans cet article).
Spam de votes
Cause : aucun contrôle côté serveur ni rate-limiting.
Solution : stockez l’IP + user_id par produit, refusez plus de 1 vote/utilisateur. Sinon les concurrents peuvent torpiller votre note.
Exercice
Améliorez le système de notation :
- Ajoutez un message contextuel qui change selon la note : 1★ = « Terrible », 2★ = « Pas terrible », 3★ = « Correct », 4★ = « Bien », 5★ = « Excellent ! »
- Ajoutez une animation de rebond quand l’utilisateur clique
- Empêchez de voter deux fois (désactivez les étoiles après le premier vote)
- Affichez la moyenne des votes avec une barre de progression pour chaque note (comme sur Amazon)
Pour creuser ce sujet
- Manipuler le DOM
- Requêtes AJAX (envoyer la note)
- Accessibilité : WAI-ARIA Radio Group pattern
- Caractères Unicode étoiles : ★ ☆ et autres
Pourquoi un systeme de notation maison plutot qu’un plugin
Pour une boutique en ligne au Senegal ou un blog professionnel a Lome, un systeme de notation par etoiles ajoute un signal social puissant : 67 % des acheteurs en ligne consultent les notes avant d’acheter. Les plugins WordPress dedies fonctionnent, mais ils ajoutent souvent 200 a 500 Ko de JS et CSS, ralentissent la page et collectent parfois des donnees sans transparence.
Ce tutoriel construit un systeme de notation 5 etoiles pas-a-pas en HTML, CSS et JavaScript pur, sans dependance externe. Vous obtenez un composant accessible (utilisable au clavier), reactif au survol, sauvegardant la note via fetch vers une API REST. Comptez 1h30 pour la mise en place complete.
Etape 1 : Le HTML semantique de base
L’approche la plus accessible utilise des inputs radio caches et des labels en forme d’etoile. Cela permet la navigation au clavier, la lecture par les technologies d’assistance et le fonctionnement sans JavaScript.
<form class="notation" data-article="123">
<fieldset>
<legend>Votre note</legend>
<input type="radio" id="note-5" name="note" value="5">
<label for="note-5" aria-label="5 etoiles">★</label>
<input type="radio" id="note-4" name="note" value="4">
<label for="note-4" aria-label="4 etoiles">★</label>
<input type="radio" id="note-3" name="note" value="3">
<label for="note-3" aria-label="3 etoiles">★</label>
<input type="radio" id="note-2" name="note" value="2">
<label for="note-2" aria-label="2 etoiles">★</label>
<input type="radio" id="note-1" name="note" value="1">
<label for="note-1" aria-label="1 etoile">★</label>
</fieldset>
</form>
Notez l’ordre inverse (5 a 1) : c’est volontaire pour exploiter le selecteur CSS frere ~ et colorer les etoiles a gauche d’une etoile survolee. Les attributs aria-label garantissent la lecture correcte par les lecteurs d’ecran.
Etape 2 : Le CSS pour cacher les radios et styler les etoiles
.notation fieldset { border: 0; padding: 0; display: inline-flex; flex-direction: row-reverse; }
.notation legend { font-size: 0.9rem; margin-bottom: 0.3rem; }
.notation input { position: absolute; opacity: 0; }
.notation label {
font-size: 2rem;
color: #d0d0d0;
cursor: pointer;
padding: 0 0.1rem;
transition: color 0.2s;
}
.notation input:checked ~ label,
.notation label:hover,
.notation label:hover ~ label { color: #f5b301; }
.notation input:focus + label { outline: 2px solid #2563eb; outline-offset: 2px; }
Le flex-direction: row-reverse inverse l’affichage visuel pour que l’utilisateur voit les etoiles de 1 a 5 de gauche a droite. Les selecteurs frere ~ permettent de colorer les etoiles cumulativement. Le focus visible (outline bleue) repond aux exigences WCAG 2.1.
Etape 3 : Le JavaScript de soumission
document.querySelectorAll('.notation').forEach(form => {
form.addEventListener('change', async (e) => {
if (e.target.name !== 'note') return;
const note = e.target.value;
const articleId = form.dataset.article;
try {
const r = await fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ articleId, note })
});
if (!r.ok) throw new Error('reponse non OK');
form.classList.add('saved');
} catch (err) {
form.classList.add('error');
console.error(err);
}
});
});
L’evenement change se declenche au clic ou a la selection clavier (espace ou fleches). La requete POST envoie la note immediatement. Les classes « saved » et « error » permettent d’afficher un retour visuel via CSS. Le signal de reussite : une coche verte ou un message « merci » qui apparait apres la soumission.
Etape 4 : L’API REST cote serveur (Node.js + Express)
const express = require('express');
const app = express();
app.use(express.json());
const notes = new Map(); // remplacer par une vraie base
app.post('/api/notes', (req, res) => {
const { articleId, note } = req.body;
const n = parseInt(note, 10);
if (!articleId || isNaN(n) || n < 1 || n > 5) {
return res.status(400).json({ error: 'donnees invalides' });
}
const existantes = notes.get(articleId) || [];
existantes.push({ note: n, date: Date.now() });
notes.set(articleId, existantes);
const moy = existantes.reduce((s, x) => s + x.note, 0) / existantes.length;
res.json({ moyenne: moy.toFixed(2), total: existantes.length });
});
app.listen(3000);
Validez systematiquement la note cote serveur (entre 1 et 5). Sans cela, un attaquant peut envoyer 999 ou des chaines arbitraires. Pour la production, remplacez la Map par PostgreSQL, MySQL ou MongoDB et ajoutez un middleware d’authentification ou un token CSRF.
Etape 5 : Empecher les votes multiples
Sans protection, un utilisateur peut voter 100 fois. Trois approches complementaires : (1) un cookie HttpOnly avec une cle « vote-articleId », (2) un enregistrement de l’IP cote serveur (avec respect du RGPD : informer dans la politique de confidentialite), (3) une authentification utilisateur si le site dispose deja d’un systeme de comptes.
Pour une PME, l’approche cookie + IP est suffisante. Stockez le cookie 30 jours. Si l’utilisateur efface ses cookies et change d’IP, il pourra revoter, mais cela reste rare et le cout d’une protection plus stricte n’en vaut pas la peine.
Etape 6 : Afficher la moyenne en lecture seule
Sur les pages de listing, affichez la moyenne sans formulaire : 5 etoiles statiques avec un pourcentage de remplissage. La technique la plus simple utilise deux div superposes, le premier en gris, le second en doré avec width: 80% (pour 4/5).
<div class="affichage-note" style="--pct: 80%">
<span aria-label="4,0 sur 5">★★★★★</span>
</div>
<style>
.affichage-note { position: relative; color: #d0d0d0; }
.affichage-note::before {
content: '\2605\2605\2605\2605\2605';
position: absolute; color: #f5b301;
width: var(--pct); overflow: hidden; white-space: nowrap;
}
</style>
La variable CSS –pct est passee inline depuis le serveur (ex : 80% pour 4 etoiles, 64% pour 3,2 etoiles). C’est leger et accessible.
Etape 7 : Marquer la note avec Schema.org pour le SEO
Pour beneficier des etoiles dans les resultats Google (rich results Product ou Recipe), ajoutez un bloc JSON-LD aggregateRating sur la page. Voir notre guide Schema.org pour la syntaxe complete.
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Casque Bluetooth",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.0",
"reviewCount": "37"
}
}
La regle Google : le ratingValue doit correspondre a une moyenne reelle de notes collectees, et reviewCount doit etre superieur a 1. Sinon, votre site risque une action manuelle « donnees structurees trompeuses ».
Pieges courants
Trois pieges frequents : (1) sauvegarder la note dans le localStorage uniquement, ce qui empeche d’avoir une moyenne globale, (2) oublier de valider cote serveur (un attaquant peut alors injecter des notes hors plage), (3) afficher des moyennes basees sur tres peu de votes (afficher « 4,8/5 » sur 2 votes est trompeur, attendez 10 votes minimum). Voir aussi notre guide formulaires accessibles.
Avec ces 7 etapes, votre composant de notation est leger (moins de 3 Ko HTML+CSS+JS), accessible, performant et compatible SEO.
Variante demi-etoiles pour plus de finesse
Si vous voulez permettre des notes en demi-points (3,5 sur 5 par exemple), dupliquez les radios en ajoutant des valeurs intermediaires : 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5. Le CSS doit aussi traiter chaque demi-etoile avec un linear-gradient(to right, #f5b301 50%, #d0d0d0 50%). Cette finesse n’est utile que pour les sites editoriaux ou critiques (cinema, restaurants). Pour la majorite des e-commerces, 5 paliers suffisent.
Une autre variante consiste a utiliser un <input type= »range » min= »0″ max= »5″ step= »0.5″> et a calculer dynamiquement l’affichage. C’est plus simple a implementer mais moins lisible visuellement par defaut.
Reduire le bruit : ponderation et anti-spam
Les notes brutes peuvent etre biaisees. Pour reduire le bruit, ponderez par l’anciennete (notes recentes pesent plus), exigez un minimum de mots dans un commentaire associe, et detectez les patterns de spam (10 votes 5 etoiles dans la meme minute depuis la meme IP). Une simple verification « 1 vote par IP par 24h » elimine deja 80 % des abus sans complexite excessive.
Pour les sites avec plusieurs centaines de produits, considerez l’algorithme du score Wilson (intervalle de confiance bayesien) qui penalise les produits avec peu de votes et favorise ceux qui ont une masse statistiquement significative. C’est l’approche utilisee par Reddit et Stack Overflow pour le tri par score.
Tests fonctionnels et d’accessibilite
Avant mise en production, testez : navigation au clavier (Tab pour atteindre le composant, fleches pour selectionner), lecture par lecteur d’ecran (NVDA sur Windows, VoiceOver sur Mac), comportement sans JavaScript (le formulaire doit pouvoir se soumettre via action= »… » en fallback), et performance reseau (la requete POST doit echouer gracieusement en mode offline avec un message clair).
Avec ces tests valides, le composant est pret pour la production. Le code total reste sous les 200 lignes (HTML + CSS + JS cote client).
Integration WordPress : shortcode et REST API
Pour integrer ce composant dans WordPress sans plugin, creez un endpoint REST custom avec register_rest_route(‘notation/v1’, ‘/note’, …) dans functions.php. L’endpoint stocke les notes dans la table wp_postmeta ou dans une table custom dediee. Cote front, ajoutez un shortcode [notation_etoiles] qui rend le HTML decrit plus haut, avec data-article egal a get_the_ID() automatiquement. Le JS cible /wp-json/notation/v1/note pour la soumission.
Pour la securite WordPress, generez un nonce avec wp_create_nonce(‘wp_rest’) et incluez-le en header X-WP-Nonce dans la requete fetch. WordPress valide automatiquement le nonce et bloque les requetes hors-site. Pensez aussi a limiter le taux de requetes (rate limiting) avec un plugin comme Limit Login Attempts ou un middleware Cloudflare pour eviter les attaques par flooding.