PHP & MySQL

Star Rating System - PHP, MySQL & AJAX

Build an interactive star rating system with PHP, MySQL, and AJAX. Includes hover effects, half-star support, and anti-fraud measures.

Database Schema

CREATE TABLE ratings (
    id INT AUTO_INCREMENT PRIMARY KEY,
    item_id INT NOT NULL,
    user_ip VARCHAR(45) NOT NULL,
    score TINYINT NOT NULL CHECK (score BETWEEN 1 AND 5),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY unique_vote (item_id, user_ip),
    INDEX idx_item (item_id)
);

PHP Backend - Submit & Fetch Ratings

<?php
// rate.php
header('Content-Type: application/json');

$pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);

$itemId = (int) ($_POST['item_id'] ?? 0);
$score = (int) ($_POST['score'] ?? 0);
$ip = $_SERVER['REMOTE_ADDR'];

if ($itemId < 1 || $score < 1 || $score > 5) {
    echo json_encode(['error' => 'Invalid input']);
    exit;
}

// Upsert - one vote per IP per item
$stmt = $pdo->prepare("
    INSERT INTO ratings (item_id, user_ip, score)
    VALUES (?, ?, ?)
    ON DUPLICATE KEY UPDATE score = VALUES(score)
");
$stmt->execute([$itemId, $ip, $score]);

// Return updated average
$stmt = $pdo->prepare("
    SELECT ROUND(AVG(score), 1) AS avg_score,
           COUNT(*) AS total_votes
    FROM ratings WHERE item_id = ?
");
$stmt->execute([$itemId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);

echo json_encode([
    'avg' => (float) $result['avg_score'],
    'total' => (int) $result['total_votes'],
    'your_score' => $score,
]);

CSS Star Display

.stars {
  display: inline-flex;
  direction: rtl;  /* right-to-left for hover trick */
  gap: 2px;
}
.stars input { display: none; }
.stars label {
  cursor: pointer;
  font-size: 28px;
  color: #ddd;
  transition: color 0.15s;
}
.stars label::before { content: '\2605'; }  /* ★ */

/* Highlight on hover and when checked */
.stars input:checked ~ label,
.stars label:hover,
.stars label:hover ~ label {
  color: #f2bf59;
}

/* Average display (read-only, uses width percentage) */
.stars-avg {
  position: relative;
  display: inline-block;
  font-size: 24px;
  letter-spacing: 3px;
  color: #ddd;
}
.stars-avg::before { content: '\2605\2605\2605\2605\2605'; }
.stars-avg .fill {
  position: absolute; top: 0; left: 0;
  overflow: hidden; white-space: nowrap;
  color: #f2bf59;
}
.stars-avg .fill::before { content: '\2605\2605\2605\2605\2605'; }

JavaScript - Interactive Rating

document.querySelectorAll('.stars input').forEach(input => {
  input.addEventListener('change', async function() {
    const itemId = this.closest('.rating-widget').dataset.itemId;
    const score = this.value;

    const res = await fetch('rate.php', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: `item_id=${itemId}&score=${score}`
    });
    const data = await res.json();

    // Update display
    const widget = this.closest('.rating-widget');
    widget.querySelector('.avg').textContent = data.avg;
    widget.querySelector('.count').textContent =
      `(${data.total} vote${data.total !== 1 ? 's' : ''})`;
  });
});

Accessible HTML

<div class="rating-widget" data-item-id="42" role="radiogroup"
     aria-label="Rate this item">
  <div class="stars">
    <input type="radio" name="rating-42" value="5" id="r42-5">
    <label for="r42-5" aria-label="5 stars"></label>
    <input type="radio" name="rating-42" value="4" id="r42-4">
    <label for="r42-4" aria-label="4 stars"></label>
    <input type="radio" name="rating-42" value="3" id="r42-3">
    <label for="r42-3" aria-label="3 stars"></label>
    <input type="radio" name="rating-42" value="2" id="r42-2">
    <label for="r42-2" aria-label="2 stars"></label>
    <input type="radio" name="rating-42" value="1" id="r42-1">
    <label for="r42-1" aria-label="1 star"></label>
  </div>
  <span class="avg">4.2</span>
  <span class="count">(128 votes)</span>
</div>

Last updated: 2026 • Browse all courses