はじめに
Shopifyの商品情報で、商品メタフィールドを使って情報管理しているショップは多いかと思います。
定義できるメタフィールドの種類はたくさんありますが、その中に「評価(Rating)」というコンテンツタイプがあり、ワインを販売するショップなら「ボディ」や「テイスト」、アパレル関連なら「生地の厚み」や「着心地」、10caでも「難易度」として利用しています。
また、「Judge.me」などのレビューアプリを採用しているショップなら「商品評価」のメタフィールドが自動追加されているケースもあります。
この評価値を出力する際に使うLiquidオブジェクトは以下の3種類で、それぞれ「評価値」「最大値」「最小値」が数値として出力可能です。
- rating
- scale_max
- scale_min
例えばレビュー評価の場合、以下のように記述すれば数値が出力されます。
<!-- rating(評価値)→ 4.6 -->
{{ product.metafields.reviews.rating.value.rating }}
<!-- scale_max(最大値)→ 5.0 -->
{{ product.metafields.reviews.rating.value.scale_max }}
<!-- scale_min(最小値)→ 0.0 -->
{{ product.metafields.reviews.rating.value.scale_min }}
この評価値を星評価バッジとして出力させるには様々な方法が考えられます。
星評価バッジを出力する方法
テキストグラデーションで出力
Dawnの場合「商品評価」ブロックがデフォルト機能で実装されており、以下が出力用のコードとなります。
.rating {
display: inline-block;
margin: 0;
}
.product .rating-star {
--letter-spacing: 0.8;
--font-size: 1.7;
}
.card-wrapper .rating-star {
--letter-spacing: 0.7;
--font-size: 1.4;
}
.rating-star {
--color-rating-star: rgb(var(--color-foreground));
--percent: calc(
(
var(--rating) / var(--rating-max) + var(--rating-decimal) * var(--font-size) /
(var(--rating-max) * (var(--letter-spacing) + var(--font-size)))
) * 100%
);
letter-spacing: calc(var(--letter-spacing) * 1rem);
font-size: calc(var(--font-size) * 1rem);
line-height: 1;
display: inline-block;
font-family: Times;
margin: 0;
}
.rating-star::before {
content: '★★★★★';
background: linear-gradient(
90deg,
var(--color-rating-star) var(--percent),
rgba(var(--color-foreground), 0.15) var(--percent)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.rating-text {
display: none;
}
.rating-count {
display: inline-block;
margin: 0;
}
@media (forced-colors: active) {
.rating {
display: none;
}
.rating-text {
display: block;
}
}
{%- if product.metafields.reviews.rating.value != blank -%}
{% liquid
assign rating_decimal = 0
assign decimal = product.metafields.reviews.rating.value.rating | modulo: 1
if decimal >= 0.3 and decimal <= 0.7
assign rating_decimal = 0.5
elsif decimal > 0.7
assign rating_decimal = 1
endif
%}
<div
class="rating"
role="img"
aria-label="{{ 'accessibility.star_reviews_info' | t: rating_value: product.metafields.reviews.rating.value, rating_max: product.metafields.reviews.rating.value.scale_max }}"
>
<span
aria-hidden="true"
class="rating-star"
style="--rating: {{ product.metafields.reviews.rating.value.rating | floor }}; --rating-max: {{ product.metafields.reviews.rating.value.scale_max }}; --rating-decimal: {{ rating_decimal }};"
></span>
</div>
<p class="rating-text caption">
<span aria-hidden="true">
{{- product.metafields.reviews.rating.value }} /
{{ product.metafields.reviews.rating.value.scale_max -}}
</span>
</p>
<p class="rating-count caption">
<span aria-hidden="true">({{ product.metafields.reviews.rating_count }})</span>
<span class="visually-hidden">
{{- product.metafields.reviews.rating_count }}
{{ 'accessibility.total_reviews' | t -}}
</span>
</p>
{%- endif -%}
基本的にはCSSファイルをベースにインラインスタイルで評価値を制御する仕様で、星の出力には単純にテキストの「★★★★★」を使っており、テキストにグラデーションを適用させる技で星に色を付けています。
この場合、2-10行目のLiquidと19行目のインラインスタイルで定義した数値から、CSSファイルの18-23行目でパーセンテージに変換していますが、かなり複雑ですよね...。
さらにアクセシビリティを考慮した設計になっているので、CSSテクニックを熟知したかなり玄人向けの設計となっています。
また、星以外で評価値を出力させたい場合は機種依存文字などの問題もあるので、どうしようかって感じです。
For文でSVGを出力
単純にFor文を使ってSVGファイルを出力する方法もあります。
{% liquid
assign icon_star = '<svg xmlns="http://www.w3.org/2000/svg" class="star" fill="currentColor" viewBox="0 0 20 20"><polygon points="10 16.11 3.82 19.51 5.03 12.5 0 7.75 6.93 6.67 10 .49 13.07 6.67 20 7.75 14.97 12.5 16.18 19.51 10 16.11"/></svg>'
assign rating = product.metafields.reviews.rating.value.raring | round
assign scale_max = product.metafields.reviews.rating.value.scale_max | round
assign base_color = '#eee'
assign accent_color = '#fc0'
%}
{% style %}
.review-stars {
display: grid;
grid-template-columns: repeat({{scale_max}}, 1fr);
width: 10rem;
}
{% endstyle %}
<div class="review-stars">
{% liquid
for i in (1..scale_max)
if i <= rating
echo icon_star | replace: 'currentColor', accent_color
else
echo icon_star | replace: 'currentColor', base_color
endif
endfor
%}
</div>
まず、星型のSVGコードを用意し「icon_star」として定義します。
続いて評価値と最大値をそれぞれ「rating」「scale_max」として四捨五入したものを定義、ベース色と強調色をそれぞれ「base_color」「accent_color」として定義します。
スタイルシートには「review-stars」の要素内が、グリッドレイアウトで横並びになるよう指定しており、繰り返し数は「scale_max」が適用され、横幅を指定することで星のサイズを調整できます。
あとはFor文を使って、評価値以下は「accent_color」が適用され、それ以外は「base_color」が適用されるようになっています。
SVGを変更すれば、星以外で評価値を出力させられますね。
小数点への対応も可能ですがかなり複雑になりますし、今回のテーマとは関係ないのでアクセシビリティの実装を含め、省略させていただきます。
これで問題ないように思えますが、例えばコレクションページの商品カードに適用した場合、ただでさえ長い<svg>コードが商品数✗5も出力されるので、ページの表示速度に影響がでる可能性があります。
それもあってか、Dawnではテキストの「星」を採用しているのかもしれません。
画像スプライトで出力
古くて不細工仕様なので、あまりお勧めできない方法ですが、画像を使った出力も可能です。
まずは評価値のSVG画像を用意します。
Liquidで実装する場合は以下のようなコードになります。
{% liquid
assign review_stars = 'review-stars.svg' | file_url
assign rating = product.metafields.reviews.rating.value.raring | round
assign scale_max = product.metafields.reviews.rating.value.scale_max | round
%}
{% style %}
.review-stars {
width: 10rem;
height: 2rem;
background-image: url({{ review_stars }});
background-size: 100% auto;
}
.review-stars[data-rating='0'] {
background-position: 0 0;
}
.review-stars[data-rating='1'] {
background-position: 0 -2rem;
}
.review-stars[data-rating='2'] {
background-position: 0 -4rem;
}
.review-stars[data-rating='3'] {
background-position: 0 -6em;
}
.review-stars[data-rating='4'] {
background-position: 0 -8rem;
}
.review-stars[data-rating='5'] {
background-position: 0 -10rem;
}
{% endstyle %}
<div class="review-stars" data-rating="{{ rating }}">
<span class="visually-hidden">{{ rating }}</span>
</div>
「画像スプライト」というかなり昔に流行った実装方法で、1枚の画像を背景に埋め込み、評価値によって画像位置を調整する仕組みです。
PNGではなくSVGを背景画像として使っているので、どの環境でも美しいラインで出力されます。
For文のSVG出力に比べてかなり軽量でシンプルなのですが、やはり時代遅れ感が否めません...。
お勧めしたい出力方法
プログレスバーで出力
<progress>タグは、処理の進捗状況やダウンロードの進捗状況などを視覚的に表示するために使用するので、本来の用途とは異なるのですが、評価値の出力には最適です。
実装例は以下になります。
{% liquid
assign icon_star = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><polygon points="10 16.11 3.82 19.51 5.03 12.5 0 7.75 6.93 6.67 10 .49 13.07 6.67 20 7.75 14.97 12.5 16.18 19.51 10 16.11"/></svg>' | url_param_escape
assign rating = product.metafields.reviews.rating.value.raring | round
assign scale_max = product.metafields.reviews.rating.value.scale_max | round
%}
{% style %}
.review-stars {
appearance: none;
-webkit-appearance: none;
width: 10rem;
height: 2rem;
background: #eee;
mask: url('data:image/svg+xml;charset=utf8,{{ icon_star }}') 0 0 / 20% 100% repeat-x;
-webkit-mask: url('data:image/svg+xml;charset=utf8,{{ icon_star }}') 0 0 / 20% 100% repeat-x;
}
.review-stars::-webkit-progress-bar {
background: #eee;
}
.review-stars::-webkit-progress-value {
background: #fc0;
}
.review-stars::-moz-progress-bar {
background: #fc0;
}
{% endstyle %}
<progress class="review-stars" max="{{ scale_max }}" value="{{ rating }}" aria-label="評価: {{ rating }} / {{ scale_max }}"></progress>
「SVGマスク」というCSSテクニックを使い、星評価バッジとして出力します。
CSSの仕組み
2行目の「icon_star」では、SVGマスクを安全に適用させるために星型のSVGを「url_param_escape」を使ってURLエンコードし、15-16行目のurl()内にて適用しております。
maskで一括指定しておりますが、分解すると以下の様になります。
.review-stars {
mask-image: url('data:image/svg+xml;charset=utf8,{{ icon_star }}'); /* マスクレイヤーの画像 */
mask-position: 0 0; /* マスクの位置(左上を指定) */
mask-size: 20% 100%; /* マスクのサイズ */
mask-repeat: repeat-x; /* マスクの繰り返し(水平軸を指定) */
}
正方形のSVG画像を左から5回、水平軸に繰り返す場合の指定となります。
mask-sizeは要素の横幅100%に対し20%の指定ですので、全体の1/5幅という意味です。
mask-repeatで水平軸の繰り返し指定をしてあるので、mask-imageで指定したSVGが横並びで5つ並べることができます。
10段階評価なら10%になりますね。
ブラウザ互換の関係でベンダープレフィックスを使用しており、正しく記述することで安全に出力できます。
- -webkit-:Webkit系(Chrome、Safariなど)
- -moz-:Mozilla系(FireFoxなど)
とくに「::-webkit-progress-bar」「::-webkit-progress-value」「::-moz-progress-bar」は重要で、正しく色指定するために必要な疑似要素です。
プログレスバーの仕組み
前述通り本来の使い方は進捗状況を出力するインジゲーターの役割ですが、上記のCSSでSVGマスクを適用すれば、星評価バッジとして使えます。

Dawnの様に複雑なHTMLやCSS、計算式を使わなくても、たった1行のHTMLとシンプルなCSSだけで星評価バッジを実装することができます。
しかも、CSSにSVGコードを1つ記述するだけなので、For文で複数のSVGを読み込んだり、画像スプライトでSVGを読み込むよりも軽量で快適です。
星以外のアイコンを使いたい場合はSVGコードを変更するだけなので、様々な用途にご利用いただけますね。
アクセシビリティへの配慮
<progress>タグに許可されている「role」属性はないので、「aria-label」属性に評価内容を記述するだけのシンプル仕様です。
<label>タグも利用できるので、いずれかをお好みでご利用ください。
応用編
linear-gradient()でカラーリング
10caでは難易度の出力にプログレスバーを採用しており、ステップごとにスケールカラーが変わるようにlinear-gradient()のテクニックを使って設定しています。
.review-stars {
--icon-mask: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%205%2015%22%3E%3Crect%20width%3D%225%22%20height%3D%2215%22%2F%3E%3C%2Fsvg%3E");
appearance: none;
-webkit-appearance: none;
width: 25rem;
height: 3rem;
background: #f9f9f9;
mask: var(--icon-mask) 0 0 / 10% 6.5rem repeat-x;
-webkit-mask: var(--icon-mask) 0 0 / 10% 6.5rem repeat-x;
}
.review-stars::-webkit-progress-bar {
background: #f9f9f9;
}
.review-stars::-webkit-progress-value {
background: linear-gradient(
to right,
#0f0 10%,
#6f0 10% 20%,
#9f0 20% 30%,
#cf0 30% 40%,
#ff0 40% 50%,
#fc0 50% 60%,
#f90 60% 70%,
#f60 70% 80%,
#f30 80% 90%,
#f00 90%
)
0 0 / 25rem 100% no-repeat;
}
.review-stars::-moz-progress-bar {
background: linear-gradient(
to right,
#0f0 10%,
#6f0 10% 20%,
#9f0 20% 30%,
#cf0 30% 40%,
#ff0 40% 50%,
#fc0 50% 60%,
#f90 60% 70%,
#f60 70% 80%,
#f30 80% 90%,
#f00 90%
)
0 0 / 25rem 100% no-repeat;
}
クリップパスでオブジェクトを変形
SVGマスクではなく、クリップパスやグラデーションを使って実装することも可能です。
.review-stars {
appearance: none;
-webkit-appearance: none;
width: 25rem;
height: 5rem;
background: #f9f9f9;
clip-path: polygon(100% 0, 0% 100%, 100% 100%);
}
.review-stars::-webkit-progress-bar {
background: #f9f9f9;
}
.review-stars::-webkit-progress-value {
background: linear-gradient(to right, #ff0, #f00) 0 0 / 25rem 100% no-repeat;
}
.review-stars::-moz-progress-bar {
background: linear-gradient(to right, #ff0, #f00) 0 0 / 25rem 100% no-repeat;
}
スクロールイベントでアニメーション
Javascriptを使って、スクロールして星評価バッジが画面内に入ったらValue値をセットし、アニメーションで評価値まで伸ばすといったイベントも実装可能です。
まずは<progress>のValue値を「0」にし、data-value値に評価値をセットします。
<progress class="review-stars" max="{{ scale_max }}" value="0" data-value="{{ rating }}" aria-label="評価: {{ rating }} / {{ scale_max }}"></progress>
次にIntersection Observer APIを使ってスクロールイベントを登録します。
document.addEventListener("DOMContentLoaded", () => {
const progressBars = document.querySelectorAll('.review-stars');
const duration = 500;
const animate = (progress, targetValue, startTime, timestamp) => {
const elapsed = timestamp - startTime;
const progressRatio = Math.min(elapsed / duration, 1);
progress.value = (targetValue * progressRatio).toFixed(2);
if (progressRatio < 1) {
requestAnimationFrame((t) => animate(progress, targetValue, startTime, t));
}
};
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const progress = entry.target;
const targetValue = parseFloat(progress.dataset.value);
requestAnimationFrame((timestamp) => animate(progress, targetValue, timestamp, timestamp));
obs.unobserve(progress);
}
});
}, { threshold: 0.1 });
progressBars.forEach(progress => observer.observe(progress));
});
コレクションページなど、複数の星評価バッジが出力されているのを想定しており、durationの数値を変更すればアニメーションスピードも変えられます。
また、監視要素が10%見えたらイベントを開始し、1度だけ実行する仕様となっています。
※このコードはChatGPTで作成しました。
この他、評価値によってカラーを変更したり、ワインなら「フルボディ」などテキストを出力したり、アイデアによって様々なカスタムが楽しめます。
この記事を書いた人

モリタオウ
株式会社テンカ 代表取締役 / ウェブクリエイター / グラフィックデザイナー
1977年12月20日生まれ。広告代理店や企業広報を経て、2007年12月にデザイン事務所「モリタ・クリエイト」を創業。2022年12月に「株式会社テンカ」を設立。