Bootstrap 5
Rating
Etoiles
Notation
Interactif
Template
Javascript
Système de notation par étoiles interactif en Bootstrap 5 : animation au survol, sélection persistante, demi-étoiles et affichage du score moyen.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="copyright" content="AngularForAll" />
<meta name="author" content="AngularForAll" />
<meta name="robots" content="noindex, nofollow" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="public, max-age=604800" />
<title>Snippets Rating Bootstrap5 2026 05022047 | AngularForAll</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
<style>
:root {
--star-inactive: #dee2e6;
--star-active: #ffc107;
--star-hover: #ffb300;
--star-shadow: rgba(255, 193, 7, 0.4);
}
body {
background: linear-gradient(150deg, #f8f9fa 0%, #e9ecef 30%, #fff3cd 70%, #fff8e1 100%);
background-attachment: fixed;
min-height: 100vh;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
/* ========== CONTENEUR PRINCIPAL ========== */
.rating-wrapper {
max-width: 580px;
margin: 0 auto;
padding: 2rem 1rem;
}
.rating-card {
border: none;
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.05);
overflow: hidden;
background: #ffffff;
}
/* ========== EN-TÊTE ========== */
.rating-header {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
padding: 1.8rem 2rem;
color: white;
position: relative;
overflow: hidden;
}
.rating-header::before {
content: '';
position: absolute;
top: -40%;
right: -15%;
width: 160px;
height: 160px;
background: radial-gradient(circle, rgba(255, 193, 7, 0.25) 0%, transparent 70%);
border-radius: 50%;
}
.header-icon {
width: 48px;
height: 48px;
background: rgba(255, 193, 7, 0.2);
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
border: 1px solid rgba(255, 193, 7, 0.3);
}
.header-badge {
background: rgba(255, 193, 7, 0.2);
border: 1px solid rgba(255, 193, 7, 0.35);
color: #ffc107;
padding: 0.4rem 1rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
}
/* ========== CORPS ========== */
.rating-body {
padding: 1.8rem 2rem 2rem;
}
/* ========== SECTION AFFICHAGE ========== */
.display-section {
background: #f8f9fa;
border-radius: 18px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid rgba(0, 0, 0, 0.04);
text-align: center;
}
.stars-display {
font-size: 2.2rem;
letter-spacing: 4px;
color: var(--star-inactive);
line-height: 1;
margin-bottom: 0.5rem;
}
.stars-display .filled {
color: var(--star-active);
text-shadow: 0 0 10px var(--star-shadow);
}
.stars-display .half {
position: relative;
display: inline-block;
color: var(--star-inactive);
}
.stars-display .half::before {
content: '★';
position: absolute;
left: 0;
top: 0;
width: 50%;
overflow: hidden;
color: var(--star-active);
text-shadow: 0 0 10px var(--star-shadow);
}
.rating-big-number {
font-size: 3.2rem;
font-weight: 800;
color: #1e293b;
letter-spacing: -0.03em;
line-height: 1;
}
.rating-big-number .decimal {
font-size: 2rem;
color: #64748b;
}
.rating-big-number .out-of {
font-size: 1rem;
color: #94a3b8;
font-weight: 500;
}
.rating-label {
font-size: 0.9rem;
font-weight: 600;
color: #64748b;
margin-top: 0.3rem;
}
.rating-total-votes {
font-size: 0.78rem;
color: #94a3b8;
}
/* ========== SECTION ÉDITION ========== */
.edit-section {
background: linear-gradient(135deg, #fffbeb, #fff7ed);
border-radius: 18px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 2px solid #fde68a;
text-align: center;
}
.stars-interactive {
font-size: 2.5rem;
letter-spacing: 6px;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
line-height: 1;
margin-bottom: 0.5rem;
}
.star-edit {
display: inline-block;
color: var(--star-inactive);
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
cursor: pointer;
}
.star-edit:hover {
transform: scale(1.2);
color: var(--star-hover);
filter: drop-shadow(0 0 8px rgba(255, 179, 0, 0.6));
}
.star-edit.active {
color: var(--star-active);
filter: drop-shadow(0 0 6px var(--star-shadow));
}
.star-edit.pop {
animation: starPop 0.35s ease forwards;
}
@keyframes starPop {
0% { transform: scale(1); }
40% { transform: scale(1.35); }
70% { transform: scale(0.9); }
100% { transform: scale(1); }
}
.hover-message {
font-size: 0.85rem;
font-weight: 600;
color: #92400e;
min-height: 24px;
margin-bottom: 0.5rem;
transition: all 0.2s ease;
}
/* Emoji rapides */
.emoji-btn {
width: 46px;
height: 46px;
border-radius: 50%;
border: 2px solid #e2e8f0;
background: white;
font-size: 1.3rem;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
}
.emoji-btn:hover {
transform: scale(1.15);
border-color: #f59e0b;
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.25);
}
.emoji-btn.selected {
background: #fef3c7;
border-color: #f59e0b;
box-shadow: 0 0 0 5px rgba(245, 158, 11, 0.12);
transform: scale(1.08);
}
/* ========== BOUTONS ========== */
.btn-save-rating {
background: linear-gradient(135deg, #f59e0b, #fbbf24);
border: none;
color: #1e293b;
font-weight: 700;
padding: 0.6rem 1.8rem;
border-radius: 25px;
box-shadow: 0 4px 15px rgba(245, 158, 11, 0.35);
transition: all 0.2s ease;
}
.btn-save-rating:hover {
background: linear-gradient(135deg, #fbbf24, #f59e0b);
box-shadow: 0 6px 20px rgba(245, 158, 11, 0.45);
transform: translateY(-1px);
color: #1e293b;
}
.btn-save-rating:active {
transform: scale(0.96);
}
.btn-reset-rating {
background: white;
border: 2px solid #e2e8f0;
color: #64748b;
font-weight: 600;
padding: 0.6rem 1.5rem;
border-radius: 25px;
transition: all 0.2s ease;
}
.btn-reset-rating:hover {
background: #f8fafc;
border-color: #cbd5e1;
color: #475569;
}
/* ========== DISTRIBUTION ========== */
.distribution-section {
background: #f8f9fa;
border-radius: 18px;
padding: 1.5rem;
border: 1px solid rgba(0, 0, 0, 0.04);
}
.dist-row {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.5rem;
}
.dist-label {
font-weight: 700;
font-size: 0.8rem;
color: #64748b;
width: 20px;
text-align: right;
}
.dist-bar {
flex: 1;
height: 8px;
background: #e2e8f0;
border-radius: 10px;
overflow: hidden;
}
.dist-fill {
height: 100%;
background: linear-gradient(90deg, #f59e0b, #fbbf24);
border-radius: 10px;
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.dist-count {
font-size: 0.75rem;
color: #94a3b8;
width: 32px;
text-align: left;
}
/* ========== TOAST ========== */
.rating-toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(120px);
background: #1e293b;
color: white;
padding: 0.7rem 1.5rem;
border-radius: 30px;
font-weight: 600;
font-size: 0.85rem;
z-index: 9999;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
}
.rating-toast.show {
transform: translateX(-50%) translateY(0);
}
/* ========== RESPONSIVE ========== */
@media (max-width: 576px) {
.rating-header {
padding: 1.3rem 1.2rem;
}
.rating-body {
padding: 1.2rem 1rem 1.5rem;
}
.stars-display {
font-size: 1.7rem;
letter-spacing: 2px;
}
.stars-interactive {
font-size: 2rem;
letter-spacing: 4px;
}
.rating-big-number {
font-size: 2.5rem;
}
.emoji-btn {
width: 38px;
height: 38px;
font-size: 1.1rem;
}
.display-section,
.edit-section,
.distribution-section {
padding: 1rem;
}
}
@media (max-width: 380px) {
.stars-display {
font-size: 1.4rem;
}
.stars-interactive {
font-size: 1.6rem;
letter-spacing: 2px;
}
}
</style>
</head>
<body>
<div class="rating-wrapper">
<div class="rating-card">
<!-- ========== EN-TÊTE ========== -->
<div class="rating-header">
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
<div class="d-flex align-items-center gap-3">
<div class="header-icon">⭐</div>
<div>
<h2 class="fw-bold mb-0 fs-5">RatingStars</h2>
<p class="mb-0 opacity-75 small">Évaluez ce produit</p>
</div>
</div>
<div class="header-badge">
<i class="bi bi-trophy-fill me-1"></i>Top Qualité
</div>
</div>
</div>
<!-- ========== CORPS ========== -->
<div class="rating-body">
<!-- Section Affichage -->
<div class="display-section">
<div class="text-uppercase small fw-bold text-muted mb-2">
<i class="bi bi-bar-chart-fill me-1"></i>Note moyenne
</div>
<div class="stars-display" id="starsDisplay">
<!-- Rempli par JS -->
</div>
<div class="rating-big-number" id="ratingBigNumber">
<!-- Rempli par JS -->
</div>
<div class="rating-label" id="ratingLabel"></div>
<div class="rating-total-votes" id="ratingTotalVotes"></div>
</div>
<!-- Section Édition -->
<div class="edit-section">
<div class="text-uppercase small fw-bold mb-2" style="color: #92400e;">
<i class="bi bi-pencil-fill me-1"></i>Votre évaluation
</div>
<div class="stars-interactive" id="starsInteractive">
<!-- Rempli par JS -->
</div>
<div class="hover-message" id="hoverMessage">Cliquez pour noter</div>
<!-- Emojis rapides -->
<div class="d-flex justify-content-center gap-2 mb-3" id="emojiContainer">
<button class="emoji-btn" data-rating="1" title="Très déçu">😡</button>
<button class="emoji-btn" data-rating="2" title="Déçu">😞</button>
<button class="emoji-btn" data-rating="3" title="Correct">😐</button>
<button class="emoji-btn" data-rating="4" title="Bien">😊</button>
<button class="emoji-btn" data-rating="5" title="Excellent">😍</button>
</div>
<div class="d-flex justify-content-center gap-3 flex-wrap">
<button class="btn btn-save-rating" id="btnSave">
<i class="bi bi-check-lg me-1"></i>Enregistrer
</button>
<button class="btn btn-reset-rating" id="btnReset">
<i class="bi bi-arrow-counterclockwise me-1"></i>Réinitialiser
</button>
</div>
</div>
<!-- Section Distribution -->
<div class="distribution-section">
<div class="text-uppercase small fw-bold text-muted mb-3 text-center">
<i class="bi bi-graph-up me-1"></i>Répartition des notes
</div>
<div id="distributionContainer">
<!-- Rempli par JS -->
</div>
</div>
</div>
</div>
</div>
<!-- ========== TOAST ========== -->
<div class="rating-toast" id="ratingToast">
<span id="toastContent">✓ Note enregistrée !</span>
</div>
<!-- Bootstrap 5 JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Script RatingStars -->
<script>
(function() {
// ============ DONNÉES ============
const TOTAL_STARS = 5;
// Distribution des votes (index 1 à 5, index 0 inutilisé)
let ratingsDistribution = [0, 4, 8, 22, 38, 94];
// Note de l'utilisateur courant
let currentUserRating = 0;
let hoveredRating = 0;
// ============ ÉLÉMENTS DOM ============
const starsDisplay = document.getElementById('starsDisplay');
const ratingBigNumber = document.getElementById('ratingBigNumber');
const ratingLabel = document.getElementById('ratingLabel');
const ratingTotalVotes = document.getElementById('ratingTotalVotes');
const starsInteractive = document.getElementById('starsInteractive');
const hoverMessage = document.getElementById('hoverMessage');
const distributionContainer = document.getElementById('distributionContainer');
const btnSave = document.getElementById('btnSave');
const btnReset = document.getElementById('btnReset');
const ratingToast = document.getElementById('ratingToast');
const toastContent = document.getElementById('toastContent');
// ============ CALCULS ============
function getAverageRating() {
let sum = 0;
let count = 0;
for (let i = 1; i <= TOTAL_STARS; i++) {
sum += i * ratingsDistribution[i];
count += ratingsDistribution[i];
}
return count > 0 ? sum / count : 0;
}
function getTotalVotes() {
return ratingsDistribution.slice(1).reduce((a, b) => a + b, 0);
}
function getRatingText(avg) {
if (avg === 0) return 'Aucune note';
if (avg <= 1.5) return 'Très décevant';
if (avg <= 2.5) return 'Décevant';
if (avg <= 3.5) return 'Correct';
if (avg <= 4.5) return 'Très bien';
return 'Excellent';
}
function getHoverText(rating) {
const messages = {
0: 'Cliquez pour noter',
1: '😡 Très déçu',
2: '😞 Déçu',
3: '😐 Correct',
4: '😊 Très bien',
5: '😍 Excellent !'
};
return messages[rating] || 'Cliquez pour noter';
}
// ============ AFFICHAGE (LECTURE) ============
function renderDisplayStars() {
const avg = getAverageRating();
const fullStars = Math.floor(avg);
const decimal = avg - fullStars;
const hasHalf = decimal >= 0.25 && decimal < 0.75;
const extraFull = decimal >= 0.75 ? 1 : 0;
starsDisplay.innerHTML = '';
for (let i = 1; i <= TOTAL_STARS; i++) {
const span = document.createElement('span');
if (i <= fullStars + extraFull) {
span.className = 'filled';
span.textContent = '★';
} else if (i === fullStars + 1 && hasHalf) {
span.className = 'half';
span.textContent = '★';
} else {
span.textContent = '★';
}
starsDisplay.appendChild(span);
}
// Nombre
const formatted = avg.toFixed(1);
const parts = formatted.split('.');
ratingBigNumber.innerHTML = `
${parts[0]}<span class="decimal">.${parts[1]}</span>
<span class="out-of">/ ${TOTAL_STARS}</span>
`;
// Texte
ratingLabel.textContent = getRatingText(avg);
ratingTotalVotes.textContent = `${getTotalVotes()} avis au total`;
}
// ============ ÉDITION (INTERACTIF) ============
function renderInteractiveStars() {
starsInteractive.innerHTML = '';
for (let i = 1; i <= TOTAL_STARS; i++) {
const star = document.createElement('span');
star.className = 'star-edit';
star.textContent = '★';
star.dataset.value = i;
// Survol souris
star.addEventListener('mouseenter', () => {
hoveredRating = i;
updateInteractiveStars();
hoverMessage.textContent = getHoverText(i);
});
star.addEventListener('mouseleave', () => {
hoveredRating = 0;
updateInteractiveStars();
hoverMessage.textContent = getHoverText(currentUserRating);
});
// Clic
star.addEventListener('click', () => {
setUserRating(i);
});
// Tactile
star.addEventListener('touchend', (e) => {
e.preventDefault();
setUserRating(i);
});
starsInteractive.appendChild(star);
}
updateInteractiveStars();
}
function updateInteractiveStars() {
const stars = starsInteractive.querySelectorAll('.star-edit');
const activeValue = hoveredRating > 0 ? hoveredRating : currentUserRating;
stars.forEach((star, index) => {
star.classList.remove('active');
if (index + 1 <= activeValue) {
star.classList.add('active');
}
});
}
function setUserRating(rating) {
currentUserRating = rating;
hoveredRating = 0;
updateInteractiveStars();
hoverMessage.textContent = getHoverText(currentUserRating);
updateEmojiButtons();
animateStars(rating);
}
function animateStars(rating) {
const stars = starsInteractive.querySelectorAll('.star-edit');
stars.forEach((star, index) => {
if (index + 1 <= rating) {
star.classList.remove('pop');
void star.offsetWidth;
star.classList.add('pop');
star.style.animationDelay = `${index * 0.06}s`;
}
});
}
// ============ EMOJIS ============
function setupEmojiButtons() {
const emojis = document.querySelectorAll('#emojiContainer .emoji-btn');
emojis.forEach(btn => {
btn.addEventListener('click', () => {
const rating = parseInt(btn.dataset.rating);
setUserRating(rating);
});
});
}
function updateEmojiButtons() {
const emojis = document.querySelectorAll('#emojiContainer .emoji-btn');
emojis.forEach(btn => {
btn.classList.remove('selected');
if (parseInt(btn.dataset.rating) === currentUserRating) {
btn.classList.add('selected');
}
});
}
// ============ DISTRIBUTION ============
function renderDistribution() {
const total = getTotalVotes();
distributionContainer.innerHTML = '';
for (let i = TOTAL_STARS; i >= 1; i--) {
const count = ratingsDistribution[i];
const pct = total > 0 ? (count / total) * 100 : 0;
const row = document.createElement('div');
row.className = 'dist-row';
row.innerHTML = `
<div class="dist-label">${i}</div>
<div class="dist-bar">
<div class="dist-fill" style="width:${pct}%;"></div>
</div>
<div class="dist-count">${count}</div>
`;
distributionContainer.appendChild(row);
}
}
// ============ SAUVEGARDE ============
function saveRating() {
if (currentUserRating === 0) {
showToast('⚠️ Veuillez sélectionner une note');
return;
}
ratingsDistribution[currentUserRating]++;
renderDisplayStars();
renderDistribution();
currentUserRating = 0;
hoveredRating = 0;
updateInteractiveStars();
hoverMessage.textContent = getHoverText(0);
updateEmojiButtons();
showToast('✅ Votre note a été enregistrée !');
}
function resetRating() {
if (currentUserRating === 0) {
showToast('ℹ️ Aucune note à réinitialiser');
return;
}
currentUserRating = 0;
hoveredRating = 0;
updateInteractiveStars();
hoverMessage.textContent = getHoverText(0);
updateEmojiButtons();
showToast('🔄 Note réinitialisée');
}
// ============ TOAST ============
let toastTimer;
function showToast(message) {
toastContent.textContent = message;
ratingToast.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
ratingToast.classList.remove('show');
}, 2500);
}
// ============ ÉVÉNEMENTS ============
btnSave.addEventListener('click', saveRating);
btnReset.addEventListener('click', resetRating);
// Raccourcis clavier
document.addEventListener('keydown', (e) => {
if (e.key >= '1' && e.key <= '5' && document.activeElement === document.body) {
setUserRating(parseInt(e.key));
}
if (e.key === 'Enter' && document.activeElement === document.body) {
saveRating();
}
});
// ============ INITIALISATION ============
function init() {
renderDisplayStars();
renderInteractiveStars();
renderDistribution();
setupEmojiButtons();
updateEmojiButtons();
console.log('⭐ Bootstrap 5 • RatingStars initialisé');
}
init();
})();
</script>
</body>
</html>
Télécharger le fichier source