Notation Étoiles Interactive Bootstrap 5

Extraits & Composants HTML 10/04/2026 12:00:00 angularforall.com
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

Partager