Tailwind Css
Rating
Etoiles
Notation
Interactif
Template
Javascript
Système de notation par étoiles interactif en Tailwind CSS : thème mauve, animations fluides, score en temps réel et design utility-first moderne.
<!DOCTYPE html>
<html lang="fr" class="scroll-smooth">
<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 Tailwindcss 2026 05022046 | AngularForAll</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Configuration Tailwind personnalisée -->
<script>
tailwind.config = {
theme: {
extend: {
colors: {
mauve: {
50: '#faf5ff',
100: '#f3e8ff',
200: '#e9d5ff',
300: '#d8b4fe',
400: '#c084fc',
500: '#a855f7',
600: '#9333ea',
700: '#7e22ce',
800: '#6b21a8',
900: '#581c87',
950: '#3b0764',
},
surface: {
light: '#fafafa',
card: '#ffffff',
dark: '#0f0f1a',
}
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
display: ['Plus Jakarta Sans', 'Inter', 'sans-serif'],
},
borderRadius: {
'2.5xl': '1.25rem',
'3xl': '1.5rem',
},
boxShadow: {
'mauve-sm': '0 2px 8px rgba(147, 51, 234, 0.08)',
'mauve-md': '0 8px 30px rgba(147, 51, 234, 0.12)',
'mauve-lg': '0 20px 50px rgba(147, 51, 234, 0.15), 0 4px 12px rgba(147, 51, 234, 0.08)',
'mauve-glow': '0 0 20px rgba(168, 85, 247, 0.3), 0 0 40px rgba(168, 85, 247, 0.1)',
'mauve-inner': 'inset 0 2px 8px rgba(147, 51, 234, 0.06)',
},
animation: {
'star-pop': 'starPop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards',
'float-up': 'floatUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards',
'pulse-mauve': 'pulseMauve 2s ease-in-out infinite',
'shimmer': 'shimmer 2s linear infinite',
'bounce-in': 'bounceIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards',
},
keyframes: {
starPop: {
'0%': { transform: 'scale(1)' },
'40%': { transform: 'scale(1.35)' },
'70%': { transform: 'scale(0.9)' },
'100%': { transform: 'scale(1)' },
},
floatUp: {
'0%': { opacity: '1', transform: 'translateY(0) scale(1)' },
'100%': { opacity: '0', transform: 'translateY(-40px) scale(0.8)' },
},
pulseMauve: {
'0%, 100%': { boxShadow: '0 0 0 0 rgba(168, 85, 247, 0.3)' },
'50%': { boxShadow: '0 0 0 15px rgba(168, 85, 247, 0)' },
},
shimmer: {
'0%': { backgroundPosition: '-200% 0' },
'100%': { backgroundPosition: '200% 0' },
},
bounceIn: {
'0%': { opacity: '0', transform: 'scale(0.7)' },
'50%': { transform: 'scale(1.05)' },
'100%': { opacity: '1', transform: 'scale(1)' },
},
},
backgroundImage: {
'mauve-gradient': 'linear-gradient(135deg, #7e22ce 0%, #9333ea 30%, #a855f7 60%, #c084fc 100%)',
'mauve-gradient-light': 'linear-gradient(135deg, #faf5ff 0%, #f3e8ff 50%, #e9d5ff 100%)',
'mauve-gradient-subtle': 'linear-gradient(160deg, #faf5ff 0%, #f5f3ff 50%, #ede9fe 100%)',
},
},
},
};
</script>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- Font Awesome 6 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.2/css/all.min.css">
<style>
/* Styles personnalisés additionnels */
body {
background: linear-gradient(160deg, #faf5ff 0%, #f5f3ff 30%, #ede9fe 60%, #faf5ff 100%);
background-attachment: fixed;
min-height: 100vh;
}
/* Particules d'arrière-plan */
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
radial-gradient(circle at 15% 25%, rgba(168, 85, 247, 0.06) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(147, 51, 234, 0.05) 0%, transparent 50%),
radial-gradient(circle at 50% 50%, rgba(192, 132, 252, 0.04) 0%, transparent 60%);
pointer-events: none;
z-index: 0;
}
/* Étoiles en lecture */
.star-display {
text-shadow: none;
transition: all 0.3s ease;
}
.star-display.filled {
color: #a855f7;
text-shadow: 0 0 12px rgba(168, 85, 247, 0.4);
}
.star-display.half {
position: relative;
display: inline-block;
color: #d1d5db;
}
.star-display.half::before {
content: '★';
position: absolute;
left: 0;
top: 0;
width: 50%;
overflow: hidden;
color: #a855f7;
text-shadow: 0 0 12px rgba(168, 85, 247, 0.4);
}
/* Étoiles interactives */
.star-interactive {
cursor: pointer;
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
color: #d1d5db;
display: inline-block;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.star-interactive:hover {
transform: scale(1.18);
color: #c084fc;
filter: drop-shadow(0 0 10px rgba(192, 132, 252, 0.6));
}
.star-interactive.active {
color: #a855f7;
filter: drop-shadow(0 0 8px rgba(168, 85, 247, 0.5));
}
.star-interactive.pop-animation {
animation: starPop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes starPop {
0% { transform: scale(1); }
40% { transform: scale(1.35); }
70% { transform: scale(0.9); }
100% { transform: scale(1); }
}
/* Particules d'étoiles */
.star-particle {
position: fixed;
pointer-events: none;
z-index: 999;
font-size: 1.5rem;
animation: floatUp 0.6s ease-out forwards;
color: #a855f7;
}
@keyframes floatUp {
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(-50px) scale(0.5); }
}
/* Toast */
.toast-notification {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(120px);
z-index: 9999;
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
.toast-notification.show {
transform: translateX(-50%) translateY(0);
}
/* Barre de progression distribution */
.dist-bar-fill {
background: linear-gradient(90deg, #a855f7, #c084fc);
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Scrollbar */
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #d8b4fe;
border-radius: 10px;
}
</style>
</head>
<body class="font-sans antialiased text-gray-800 flex items-center justify-center min-h-screen p-4">
<!-- =============================================
CONTENEUR PRINCIPAL
============================================= -->
<div class="relative z-10 w-full max-w-lg mx-auto">
<!-- Carte principale -->
<div class="bg-white rounded-3xl shadow-mauve-lg overflow-hidden border border-mauve-100">
<!-- ========== EN-TÊTE ========== -->
<div class="relative bg-gradient-to-br from-mauve-900 via-mauve-800 to-mauve-950 px-6 sm:px-8 py-6 sm:py-7 overflow-hidden">
<!-- Cercles décoratifs -->
<div class="absolute top-0 right-0 w-40 h-40 bg-mauve-500/10 rounded-full -translate-y-1/2 translate-x-1/4"></div>
<div class="absolute bottom-0 left-1/3 w-32 h-32 bg-mauve-400/10 rounded-full translate-y-1/2"></div>
<!-- Contenu -->
<div class="relative z-10 flex items-center justify-between flex-wrap gap-4">
<div class="flex items-center gap-3">
<div class="w-11 h-11 bg-mauve-500/20 backdrop-blur-sm rounded-xl flex items-center justify-center text-2xl border border-mauve-400/30 flex-shrink-0">
⭐
</div>
<div>
<h1 class="font-display font-bold text-white text-lg sm:text-xl tracking-tight">RatingStars</h1>
<p class="text-mauve-300 text-xs sm:text-sm">Évaluez ce produit</p>
</div>
</div>
<div class="inline-flex items-center gap-2 bg-mauve-500/20 backdrop-blur-sm border border-mauve-400/30 text-mauve-200 px-3 py-1.5 rounded-full text-xs font-semibold">
<i class="fa-solid fa-crown"></i>
Top Qualité
</div>
</div>
</div>
<!-- ========== CORPS ========== -->
<div class="px-5 sm:px-7 py-6 sm:py-7 space-y-5">
<!-- ===== SECTION AFFICHAGE ===== -->
<div class="bg-mauve-50/80 rounded-2.5xl p-5 sm:p-6 text-center border border-mauve-100 shadow-mauve-inner">
<p class="text-[10px] sm:text-xs font-bold uppercase tracking-widest text-mauve-400 mb-3">
<i class="fa-solid fa-chart-simple mr-1"></i>Note moyenne
</p>
<!-- Étoiles affichage -->
<div class="text-3xl sm:text-4xl tracking-wide mb-2" id="starsDisplay">
<!-- Rempli dynamiquement -->
</div>
<!-- Note chiffrée -->
<div class="font-display font-black text-4xl sm:text-5xl text-gray-900 tracking-tight" id="ratingNumber">
<!-- Rempli dynamiquement -->
</div>
<p class="text-sm font-semibold text-mauve-600 mt-1" id="ratingLabel"></p>
<p class="text-xs text-mauve-400 mt-0.5" id="ratingCount"></p>
</div>
<!-- ===== SECTION ÉDITION ===== -->
<div class="bg-gradient-to-br from-mauve-50 to-purple-50 rounded-2.5xl p-5 sm:p-6 text-center border-2 border-mauve-200 shadow-mauve-sm">
<p class="text-[10px] sm:text-xs font-bold uppercase tracking-widest text-mauve-700 mb-3">
<i class="fa-solid fa-pen-to-square mr-1"></i>Votre évaluation
</p>
<!-- Étoiles interactives -->
<div class="text-4xl sm:text-5xl tracking-wider mb-2" id="starsInteractive">
<!-- Rempli dynamiquement -->
</div>
<p class="text-sm font-semibold text-mauve-700 min-h-[24px] transition-all duration-200" id="hoverMessage">
Cliquez pour noter
</p>
<!-- Émojis rapides -->
<div class="flex justify-center gap-2 sm:gap-3 my-3" id="emojiContainer">
<button class="w-10 h-10 sm:w-11 sm:h-11 rounded-full bg-white border-2 border-mauve-200 text-lg sm:text-xl flex items-center justify-center transition-all duration-200 hover:scale-115 hover:border-mauve-400 hover:shadow-mauve-sm cursor-pointer" data-rating="1" title="Très déçu">😡</button>
<button class="w-10 h-10 sm:w-11 sm:h-11 rounded-full bg-white border-2 border-mauve-200 text-lg sm:text-xl flex items-center justify-center transition-all duration-200 hover:scale-115 hover:border-mauve-400 hover:shadow-mauve-sm cursor-pointer" data-rating="2" title="Déçu">😞</button>
<button class="w-10 h-10 sm:w-11 sm:h-11 rounded-full bg-white border-2 border-mauve-200 text-lg sm:text-xl flex items-center justify-center transition-all duration-200 hover:scale-115 hover:border-mauve-400 hover:shadow-mauve-sm cursor-pointer" data-rating="3" title="Correct">😐</button>
<button class="w-10 h-10 sm:w-11 sm:h-11 rounded-full bg-white border-2 border-mauve-200 text-lg sm:text-xl flex items-center justify-center transition-all duration-200 hover:scale-115 hover:border-mauve-400 hover:shadow-mauve-sm cursor-pointer" data-rating="4" title="Bien">😊</button>
<button class="w-10 h-10 sm:w-11 sm:h-11 rounded-full bg-white border-2 border-mauve-200 text-lg sm:text-xl flex items-center justify-center transition-all duration-200 hover:scale-115 hover:border-mauve-400 hover:shadow-mauve-sm cursor-pointer" data-rating="5" title="Excellent">😍</button>
</div>
<!-- Boutons d'action -->
<div class="flex justify-center gap-3 flex-wrap">
<button id="btnSave" class="inline-flex items-center gap-2 px-5 py-2.5 bg-mauve-600 text-white font-semibold rounded-full shadow-mauve-md hover:bg-mauve-700 hover:shadow-mauve-glow active:scale-95 transition-all duration-200 text-sm">
<i class="fa-solid fa-check text-xs"></i>
Enregistrer
</button>
<button id="btnReset" class="inline-flex items-center gap-2 px-5 py-2.5 bg-white text-mauve-600 font-semibold rounded-full border-2 border-mauve-200 hover:bg-mauve-50 hover:border-mauve-300 active:scale-95 transition-all duration-200 text-sm">
<i class="fa-solid fa-arrow-rotate-left text-xs"></i>
Réinitialiser
</button>
</div>
</div>
<!-- ===== SECTION DISTRIBUTION ===== -->
<div class="bg-mauve-50/80 rounded-2.5xl p-5 sm:p-6 border border-mauve-100 shadow-mauve-inner">
<p class="text-[10px] sm:text-xs font-bold uppercase tracking-widest text-mauve-400 mb-4 text-center">
<i class="fa-solid fa-chart-bar mr-1"></i>Répartition des notes
</p>
<div class="space-y-2.5" id="distributionContainer">
<!-- Rempli dynamiquement -->
</div>
</div>
</div>
</div>
<!-- Indicateur de confiance -->
<p class="text-center text-xs text-mauve-400 mt-4 flex items-center justify-center gap-1.5">
<i class="fa-solid fa-shield-halved"></i>
Avis vérifiés • Authentiques
</p>
</div>
<!-- ========== TOAST NOTIFICATION ========== -->
<div class="toast-notification bg-gray-900 text-white px-5 py-3 rounded-full shadow-xl flex items-center gap-2 text-sm font-semibold" id="toastNotification">
<span id="toastIcon">✅</span>
<span id="toastMessage">Note enregistrée !</span>
</div>
<!-- ========== SCRIPT ========== -->
<script>
(function() {
// ============ CONFIGURATION ============
const TOTAL_STARS = 5;
// Distribution des votes (index 1 à 5)
let distribution = [0, 4, 8, 22, 38, 94];
// État utilisateur
let userRating = 0;
let hoverRating = 0;
// ============ ÉLÉMENTS DOM ============
const starsDisplay = document.getElementById('starsDisplay');
const ratingNumber = document.getElementById('ratingNumber');
const ratingLabel = document.getElementById('ratingLabel');
const ratingCount = document.getElementById('ratingCount');
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 toastNotification = document.getElementById('toastNotification');
const toastIcon = document.getElementById('toastIcon');
const toastMessage = document.getElementById('toastMessage');
// ============ CALCULS ============
function getAverage() {
let sum = 0, count = 0;
for (let i = 1; i <= TOTAL_STARS; i++) {
sum += i * distribution[i];
count += distribution[i];
}
return count > 0 ? sum / count : 0;
}
function getTotalVotes() {
return distribution.slice(1).reduce((a, b) => a + b, 0);
}
function getLabel(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(r) {
const texts = { 0: 'Cliquez pour noter', 1: '😡 Très déçu', 2: '😞 Déçu', 3: '😐 Correct', 4: '😊 Très bien', 5: '😍 Excellent !' };
return texts[r] || 'Cliquez pour noter';
}
// ============ AFFICHAGE DES ÉTOILES (LECTURE) ============
function renderDisplay() {
const avg = getAverage();
const full = Math.floor(avg);
const dec = avg - full;
const hasHalf = dec >= 0.25 && dec < 0.75;
const extra = dec >= 0.75 ? 1 : 0;
starsDisplay.innerHTML = '';
for (let i = 1; i <= TOTAL_STARS; i++) {
const span = document.createElement('span');
span.className = 'star-display';
span.textContent = '★';
if (i <= full + extra) {
span.classList.add('filled');
} else if (i === full + 1 && hasHalf) {
span.classList.add('half');
}
starsDisplay.appendChild(span);
}
// Nombre
const formatted = avg.toFixed(1);
const [whole, decimal] = formatted.split('.');
ratingNumber.innerHTML = `${whole}<span class="text-xl sm:text-2xl text-mauve-400 font-bold">.${decimal}</span><span class="text-xs sm:text-sm text-mauve-300 font-medium ml-1">/ ${TOTAL_STARS}</span>`;
ratingLabel.textContent = getLabel(avg);
ratingCount.textContent = `${getTotalVotes()} avis au total`;
}
// ============ ÉTOILES INTERACTIVES ============
function renderInteractive() {
starsInteractive.innerHTML = '';
for (let i = 1; i <= TOTAL_STARS; i++) {
const star = document.createElement('span');
star.className = 'star-interactive';
star.textContent = '★';
star.dataset.value = i;
star.addEventListener('mouseenter', () => {
hoverRating = i;
updateInteractiveStars();
hoverMessage.textContent = getHoverText(i);
});
star.addEventListener('mouseleave', () => {
hoverRating = 0;
updateInteractiveStars();
hoverMessage.textContent = getHoverText(userRating);
});
star.addEventListener('click', (e) => {
setUserRating(i, e);
});
star.addEventListener('touchend', (e) => {
e.preventDefault();
setUserRating(i, e);
});
starsInteractive.appendChild(star);
}
updateInteractiveStars();
}
function updateInteractiveStars() {
const stars = starsInteractive.querySelectorAll('.star-interactive');
const active = hoverRating > 0 ? hoverRating : userRating;
stars.forEach((star, idx) => {
star.classList.remove('active');
if (idx + 1 <= active) star.classList.add('active');
});
}
function setUserRating(rating, event) {
userRating = rating;
hoverRating = 0;
updateInteractiveStars();
hoverMessage.textContent = getHoverText(userRating);
updateEmojiButtons();
animateStars(rating);
// Particules si clic sur étoile
if (event && event.target) {
spawnParticles(event.target);
}
}
function animateStars(rating) {
const stars = starsInteractive.querySelectorAll('.star-interactive');
stars.forEach((star, idx) => {
if (idx + 1 <= rating) {
star.classList.remove('pop-animation');
void star.offsetWidth;
star.classList.add('pop-animation');
star.style.animationDelay = `${idx * 0.06}s`;
}
});
}
// ============ PARTICULES ============
function spawnParticles(element) {
const rect = element.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
const emojis = ['✨', '💜', '⭐', '💫', '🌟'];
for (let i = 0; i < 8; i++) {
const particle = document.createElement('span');
particle.className = 'star-particle';
particle.textContent = emojis[Math.floor(Math.random() * emojis.length)];
particle.style.left = cx + (Math.random() - 0.5) * 60 + 'px';
particle.style.top = cy + (Math.random() - 0.5) * 40 + 'px';
particle.style.fontSize = (1 + Math.random() * 1.5) + 'rem';
particle.style.animationDuration = (0.5 + Math.random() * 0.5) + 's';
document.body.appendChild(particle);
setTimeout(() => {
particle.remove();
}, 700);
}
}
// ============ EMOJIS ============
function setupEmojis() {
const btns = document.querySelectorAll('#emojiContainer button');
btns.forEach(btn => {
btn.addEventListener('click', () => {
const r = parseInt(btn.dataset.rating);
setUserRating(r, { target: btn });
});
});
}
function updateEmojiButtons() {
const btns = document.querySelectorAll('#emojiContainer button');
btns.forEach(btn => {
const r = parseInt(btn.dataset.rating);
if (r === userRating) {
btn.classList.add('border-mauve-500', 'bg-mauve-100', 'shadow-mauve-sm');
btn.classList.remove('border-mauve-200', 'bg-white');
} else {
btn.classList.remove('border-mauve-500', 'bg-mauve-100', 'shadow-mauve-sm');
btn.classList.add('border-mauve-200', 'bg-white');
}
});
}
// ============ DISTRIBUTION ============
function renderDistribution() {
const total = getTotalVotes();
distributionContainer.innerHTML = '';
for (let i = TOTAL_STARS; i >= 1; i--) {
const count = distribution[i];
const pct = total > 0 ? (count / total) * 100 : 0;
const row = document.createElement('div');
row.className = 'flex items-center gap-2 sm:gap-3';
row.innerHTML = `
<span class="text-xs font-bold text-mauve-500 w-5 text-right flex-shrink-0">${i}</span>
<div class="flex-1 h-2 bg-mauve-200 rounded-full overflow-hidden">
<div class="dist-bar-fill h-full rounded-full" style="width:${pct}%;"></div>
</div>
<span class="text-xs text-mauve-400 w-8 text-left flex-shrink-0">${count}</span>
`;
distributionContainer.appendChild(row);
}
}
// ============ SAUVEGARDE ============
function saveRating() {
if (userRating === 0) {
showToast('⚠️', 'Veuillez sélectionner une note');
return;
}
distribution[userRating]++;
renderDisplay();
renderDistribution();
userRating = 0;
hoverRating = 0;
updateInteractiveStars();
hoverMessage.textContent = getHoverText(0);
updateEmojiButtons();
showToast('✅', 'Votre note a été enregistrée !');
}
function resetRating() {
if (userRating === 0) {
showToast('ℹ️', 'Aucune note à réinitialiser');
return;
}
userRating = 0;
hoverRating = 0;
updateInteractiveStars();
hoverMessage.textContent = getHoverText(0);
updateEmojiButtons();
showToast('🔄', 'Note réinitialisée');
}
// ============ TOAST ============
let toastTimer;
function showToast(icon, msg) {
toastIcon.textContent = icon;
toastMessage.textContent = msg;
toastNotification.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
toastNotification.classList.remove('show');
}, 2500);
}
// ============ ÉVÉNEMENTS ============
btnSave.addEventListener('click', saveRating);
btnReset.addEventListener('click', resetRating);
document.addEventListener('keydown', (e) => {
if (e.key >= '1' && e.key <= '5' && document.activeElement === document.body) {
setUserRating(parseInt(e.key), { target: document.body });
}
if (e.key === 'Enter' && document.activeElement === document.body) saveRating();
});
// ============ INITIALISATION ============
function init() {
renderDisplay();
renderInteractive();
renderDistribution();
setupEmojis();
updateEmojiButtons();
// console.info('💜 RatingStars • Mauve Edition • Prêt');
}
init();
})();
</script>
</body>
</html>
Télécharger le fichier source