スターレーティング

ホバーでプレビュー、クリックで確定し、矢印キー操作にも対応した5段階の星評価。レビュー投稿やアンケートの入力UIに使えます。

#js#css#rating#a11y

ライブデモ

使用例(お題: カフェ MOON BREW)

この技法を「カフェ MOON BREW」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- MOON BREW: レビュー投稿フォーム。スターレーティングを主役に -->
<div class="mb">
  <header class="mb__head">
    <span class="mb__logo">☕ MOON BREW</span>
    <span class="mb__sub">ご来店ありがとうございました</span>
  </header>

  <div class="mb__body">
    <div class="mb__item">
      <span class="mb__thumb" aria-hidden="true"></span>
      <div>
        <h1 class="mb__name">本日の一杯はいかがでしたか?</h1>
        <p class="mb__desc">琥珀カフェラテ ・ 渋谷桜丘店</p>
      </div>
    </div>

    <div class="rate">
      <p class="rate__label">評価を選んでください</p>
      <div class="rating" role="radiogroup" aria-label="5段階評価">
        <button class="star" type="button" role="radio" aria-checked="false" aria-label="1点">★</button>
        <button class="star" type="button" role="radio" aria-checked="false" aria-label="2点">★</button>
        <button class="star" type="button" role="radio" aria-checked="false" aria-label="3点">★</button>
        <button class="star" type="button" role="radio" aria-checked="false" aria-label="4点">★</button>
        <button class="star" type="button" role="radio" aria-checked="false" aria-label="5点">★</button>
      </div>
      <p class="rate__out">評価: <span class="rating__out">—</span></p>
    </div>

    <textarea class="mb__ta" rows="2" placeholder="ひとことコメント(任意)"></textarea>
    <button class="mb__send" type="button">レビューを送信</button>
  </div>
</div>
CSS
/* MOON BREW カフェ テーマ: クリーム/濃ブラウン/琥珀 */
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: "Hiragino Mincho ProN", "Segoe UI", system-ui, serif;
  background: #f5ede1;
  color: #2b1d12;
}

.mb {
  height: 400px;
  display: flex;
  flex-direction: column;
  padding: 0 30px;
}
.mb__head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 18px 0;
  border-bottom: 1px solid #e3d6c2;
}
.mb__logo { font-size: 17px; font-weight: 700; }
.mb__sub {
  font-size: 12px;
  color: #9b876f;
  font-family: "Segoe UI", system-ui, sans-serif;
}

.mb__body {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
}
.mb__item {
  display: flex;
  align-items: center;
  gap: 14px;
}
.mb__thumb {
  width: 50px;
  height: 50px;
  border-radius: 12px;
  flex: none;
  background: radial-gradient(circle at 42% 38%, #d9b388, #c98a3b 55%, #7a5224);
  box-shadow: 0 6px 14px rgba(122,82,36,.3);
}
.mb__name {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
}
.mb__desc {
  margin: 4px 0 0;
  font-size: 12px;
  color: #9b876f;
  font-family: "Segoe UI", system-ui, sans-serif;
}

/* 評価ブロック */
.rate {
  background: #fbf6ee;
  border: 1px solid #e7d9c4;
  border-radius: 14px;
  padding: 14px 18px;
}
.rate__label {
  margin: 0 0 6px;
  font-size: 12px;
  color: #6b513a;
  font-family: "Segoe UI", system-ui, sans-serif;
}

/* 主役: スターレーティング */
.rating {
  display: inline-flex;
  gap: 6px;
}
.star {
  font-size: 32px;
  line-height: 1;
  color: #e0cdb1;
  background: none;
  border: none;
  padding: 0 2px;
  cursor: pointer;
  transition: transform .12s ease, color .15s ease;
}
.star:hover { transform: scale(1.12); }
.star.is-on { color: #c98a3b; text-shadow: 0 2px 8px rgba(201,138,59,.4); }
.star.is-pop { animation: star-pop .32s ease; }
.star:focus-visible { outline: 2px solid #c98a3b; outline-offset: 3px; border-radius: 4px; }
@keyframes star-pop {
  0% { transform: scale(1); }
  45% { transform: scale(1.4); }
  100% { transform: scale(1); }
}

.rate__out {
  margin: 8px 0 0;
  font-size: 13px;
  font-weight: 700;
  color: #2b1d12;
  font-family: "Segoe UI", system-ui, sans-serif;
}
.rating__out { color: #c98a3b; }

.mb__ta {
  font-family: "Segoe UI", system-ui, sans-serif;
  font-size: 13px;
  color: #2b1d12;
  background: #fbf6ee;
  border: 1px solid #e7d9c4;
  border-radius: 10px;
  padding: 10px 12px;
  resize: none;
}
.mb__ta::placeholder { color: #b9a78d; }
.mb__ta:focus { outline: none; border-color: #c98a3b; }

.mb__send {
  align-self: flex-start;
  font-family: "Segoe UI", system-ui, sans-serif;
  font-size: 13px;
  font-weight: 700;
  color: #fff;
  background: linear-gradient(135deg, #c98a3b, #a86b28);
  border: none;
  padding: 11px 26px;
  border-radius: 10px;
  cursor: pointer;
  box-shadow: 0 8px 20px rgba(201,138,59,.35);
}

@media (prefers-reduced-motion: reduce) {
  .star, .star.is-pop { transition: none; animation: none; }
}
JavaScript
// MOON BREWレビュー: ホバーでプレビュー、クリックで確定、矢印キー対応
(() => {
  const group = document.querySelector('.rating');
  const out = document.querySelector('.rating__out');
  if (!group) return; // null安全

  const stars = Array.from(group.querySelectorAll('.star'));
  if (!stars.length) return;

  let current = 0; // 確定値

  // n個目までを点灯(プレビュー兼確定描画)
  const paint = (n) => stars.forEach((s, i) => s.classList.toggle('is-on', i < n));

  // 確定値を反映しARIAを同期
  const commit = (n) => {
    current = n;
    paint(n);
    stars.forEach((s, i) => s.setAttribute('aria-checked', String(i === n - 1)));
    if (out) out.textContent = n ? `${n} / 5` : '—';
  };

  stars.forEach((star, i) => {
    const val = i + 1;
    star.addEventListener('pointerenter', () => paint(val)); // ホバープレビュー
    star.addEventListener('click', () => {
      commit(val);
      star.classList.remove('is-pop');
      void star.offsetWidth; // リフローでアニメ再生をリセット
      star.classList.add('is-pop');
    });
    // 左右キーで増減(アクセシビリティ)
    star.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
        e.preventDefault();
        const next = Math.min(5, current + 1);
        commit(next);
        stars[next - 1].focus();
      } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
        e.preventDefault();
        const next = Math.max(1, current - 1);
        commit(next);
        stars[next - 1].focus();
      }
    });
  });

  // グループから離れたら確定値表示へ戻す
  group.addEventListener('pointerleave', () => paint(current));
})();

コード

HTML
<!-- スターレーティング: ホバープレビュー+クリック確定の星評価 -->
<div class="stage">
  <div class="rating" role="radiogroup" aria-label="5段階評価">
    <button class="star" type="button" role="radio" aria-checked="false" data-value="1" aria-label="星1">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 6 21.2l1.4-6.8L2.3 9.7l6.9-.7z"/></svg>
    </button>
    <button class="star" type="button" role="radio" aria-checked="false" data-value="2" aria-label="星2">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 6 21.2l1.4-6.8L2.3 9.7l6.9-.7z"/></svg>
    </button>
    <button class="star" type="button" role="radio" aria-checked="false" data-value="3" aria-label="星3">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 6 21.2l1.4-6.8L2.3 9.7l6.9-.7z"/></svg>
    </button>
    <button class="star" type="button" role="radio" aria-checked="false" data-value="4" aria-label="星4">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 6 21.2l1.4-6.8L2.3 9.7l6.9-.7z"/></svg>
    </button>
    <button class="star" type="button" role="radio" aria-checked="false" data-value="5" aria-label="星5">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 6 21.2l1.4-6.8L2.3 9.7l6.9-.7z"/></svg>
    </button>
  </div>
  <p class="rating__label">タップして評価 <span class="rating__out">—</span></p>
</div>
CSS
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: radial-gradient(circle at 50% 30%, #2a2440 0%, #15101f 75%);
  color: #f3eefc;
}

.stage {
  display: grid;
  place-items: center;
  gap: 18px;
}

.rating {
  display: flex;
  gap: 8px;
}

/* 星ボタン: 既定は枠だけのアウトライン表示 */
.star {
  padding: 4px;
  border: none;
  background: none;
  cursor: pointer;
  line-height: 0;
  -webkit-tap-highlight-color: transparent;
}
.star svg {
  width: 42px;
  height: 42px;
  fill: #3b3354;
  stroke: #5b507e;
  stroke-width: 1;
  transition: fill .15s ease, transform .15s cubic-bezier(.34,1.56,.64,1), filter .15s ease;
}

/* 点灯状態(JSが is-on を付与): 累積で左から点く */
.star.is-on svg {
  fill: #ffc83d;
  stroke: #ffb300;
  filter: drop-shadow(0 0 6px rgba(255, 200, 61, .6));
}

/* ホバー/フォーカス中の星を少し持ち上げる */
.star:hover svg,
.star:focus-visible svg {
  transform: scale(1.18) rotate(-4deg);
}
.star:focus-visible { outline: none; }

/* 確定アニメ(クリック時のひと押し) */
.star.is-pop svg { animation: star-pop .4s ease; }
@keyframes star-pop {
  0% { transform: scale(.6); }
  60% { transform: scale(1.3); }
  100% { transform: scale(1); }
}

.rating__label {
  margin: 0;
  font-size: 14px;
  letter-spacing: .03em;
  color: rgba(243, 238, 252, .7);
}
.rating__out {
  font-weight: 700;
  color: #ffc83d;
  margin-left: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .star svg { transition: fill .15s ease; }
  .star:hover svg, .star:focus-visible svg { transform: none; }
  .star.is-pop svg { animation: none; }
}
JavaScript
// スターレーティング: ホバーでプレビュー、クリックで確定。キーボード操作対応
(() => {
  const group = document.querySelector('.rating');
  const out = document.querySelector('.rating__out');
  if (!group) return; // null安全

  const stars = Array.from(group.querySelectorAll('.star'));
  if (!stars.length) return;

  let current = 0; // 確定値

  // n個目までを点灯表示する(プレビュー兼確定描画)
  const paint = (n) => {
    stars.forEach((s, i) => s.classList.toggle('is-on', i < n));
  };

  // 確定値を反映し、ARIAを同期
  const commit = (n) => {
    current = n;
    paint(n);
    stars.forEach((s, i) => s.setAttribute('aria-checked', String(i === n - 1)));
    if (out) out.textContent = n ? `${n} / 5` : '—';
  };

  stars.forEach((star, i) => {
    const val = i + 1;
    // ホバーで一時プレビュー
    star.addEventListener('pointerenter', () => paint(val));
    // クリックで確定+ポップ
    star.addEventListener('click', () => {
      commit(val);
      star.classList.remove('is-pop');
      void star.offsetWidth; // リフローでアニメ再生をリセット
      star.classList.add('is-pop');
    });
    // 左右キーで値を増減(アクセシビリティ)
    star.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
        e.preventDefault();
        const next = Math.min(5, current + 1);
        commit(next);
        stars[next - 1].focus();
      } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
        e.preventDefault();
        const next = Math.max(1, current - 1);
        commit(next);
        stars[next - 1].focus();
      }
    });
  });

  // グループから離れたら確定値の表示へ戻す
  group.addEventListener('pointerleave', () => paint(current));
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「スターレーティング」の効果を追加してください。

# 追加してほしい効果
スターレーティング(マイクロインタラクション)
ホバーでプレビュー、クリックで確定し、矢印キー操作にも対応した5段階の星評価。レビュー投稿やアンケートの入力UIに使えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- スターレーティング: ホバープレビュー+クリック確定の星評価 -->
<div class="stage">
  <div class="rating" role="radiogroup" aria-label="5段階評価">
    <button class="star" type="button" role="radio" aria-checked="false" data-value="1" aria-label="星1">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 6 21.2l1.4-6.8L2.3 9.7l6.9-.7z"/></svg>
    </button>
    <button class="star" type="button" role="radio" aria-checked="false" data-value="2" aria-label="星2">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 6 21.2l1.4-6.8L2.3 9.7l6.9-.7z"/></svg>
    </button>
    <button class="star" type="button" role="radio" aria-checked="false" data-value="3" aria-label="星3">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 6 21.2l1.4-6.8L2.3 9.7l6.9-.7z"/></svg>
    </button>
    <button class="star" type="button" role="radio" aria-checked="false" data-value="4" aria-label="星4">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 6 21.2l1.4-6.8L2.3 9.7l6.9-.7z"/></svg>
    </button>
    <button class="star" type="button" role="radio" aria-checked="false" data-value="5" aria-label="星5">
      <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2l2.9 6.3 6.9.7-5.1 4.7 1.4 6.8L12 17.8 6 21.2l1.4-6.8L2.3 9.7l6.9-.7z"/></svg>
    </button>
  </div>
  <p class="rating__label">タップして評価 <span class="rating__out">—</span></p>
</div>

【CSS】
* { box-sizing: border-box; }
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  font-family: "Segoe UI", system-ui, sans-serif;
  background: radial-gradient(circle at 50% 30%, #2a2440 0%, #15101f 75%);
  color: #f3eefc;
}

.stage {
  display: grid;
  place-items: center;
  gap: 18px;
}

.rating {
  display: flex;
  gap: 8px;
}

/* 星ボタン: 既定は枠だけのアウトライン表示 */
.star {
  padding: 4px;
  border: none;
  background: none;
  cursor: pointer;
  line-height: 0;
  -webkit-tap-highlight-color: transparent;
}
.star svg {
  width: 42px;
  height: 42px;
  fill: #3b3354;
  stroke: #5b507e;
  stroke-width: 1;
  transition: fill .15s ease, transform .15s cubic-bezier(.34,1.56,.64,1), filter .15s ease;
}

/* 点灯状態(JSが is-on を付与): 累積で左から点く */
.star.is-on svg {
  fill: #ffc83d;
  stroke: #ffb300;
  filter: drop-shadow(0 0 6px rgba(255, 200, 61, .6));
}

/* ホバー/フォーカス中の星を少し持ち上げる */
.star:hover svg,
.star:focus-visible svg {
  transform: scale(1.18) rotate(-4deg);
}
.star:focus-visible { outline: none; }

/* 確定アニメ(クリック時のひと押し) */
.star.is-pop svg { animation: star-pop .4s ease; }
@keyframes star-pop {
  0% { transform: scale(.6); }
  60% { transform: scale(1.3); }
  100% { transform: scale(1); }
}

.rating__label {
  margin: 0;
  font-size: 14px;
  letter-spacing: .03em;
  color: rgba(243, 238, 252, .7);
}
.rating__out {
  font-weight: 700;
  color: #ffc83d;
  margin-left: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .star svg { transition: fill .15s ease; }
  .star:hover svg, .star:focus-visible svg { transform: none; }
  .star.is-pop svg { animation: none; }
}

【JavaScript】
// スターレーティング: ホバーでプレビュー、クリックで確定。キーボード操作対応
(() => {
  const group = document.querySelector('.rating');
  const out = document.querySelector('.rating__out');
  if (!group) return; // null安全

  const stars = Array.from(group.querySelectorAll('.star'));
  if (!stars.length) return;

  let current = 0; // 確定値

  // n個目までを点灯表示する(プレビュー兼確定描画)
  const paint = (n) => {
    stars.forEach((s, i) => s.classList.toggle('is-on', i < n));
  };

  // 確定値を反映し、ARIAを同期
  const commit = (n) => {
    current = n;
    paint(n);
    stars.forEach((s, i) => s.setAttribute('aria-checked', String(i === n - 1)));
    if (out) out.textContent = n ? `${n} / 5` : '—';
  };

  stars.forEach((star, i) => {
    const val = i + 1;
    // ホバーで一時プレビュー
    star.addEventListener('pointerenter', () => paint(val));
    // クリックで確定+ポップ
    star.addEventListener('click', () => {
      commit(val);
      star.classList.remove('is-pop');
      void star.offsetWidth; // リフローでアニメ再生をリセット
      star.classList.add('is-pop');
    });
    // 左右キーで値を増減(アクセシビリティ)
    star.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
        e.preventDefault();
        const next = Math.min(5, current + 1);
        commit(next);
        stars[next - 1].focus();
      } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
        e.preventDefault();
        const next = Math.max(1, current - 1);
        commit(next);
        stars[next - 1].focus();
      }
    });
  });

  // グループから離れたら確定値の表示へ戻す
  group.addEventListener('pointerleave', () => paint(current));
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。