Rating
stimeo--rating
An ordinal star scale: aria-checked, roving tabindex, clamped arrows, hover preview, clearing.
The stimeo--rating controller implements the APG Radio Group pattern as an ordinal scale: exactly one symbol is aria-checked. Unlike a generic radio group it deliberately does not wrap — arrows clamp at the bounds. Clicking the selected symbol clears to 0 when clearable, hover/focus previews a fill range via data-rating-hover, and a readonly mode renders a non-interactive role="img". The value is mirrored to a hidden field and stimeo--rating:change is dispatched on every change. The star glyph is owned by this Playground.
Keyboard
| Key | Action |
|---|---|
| → / ↑ | Raise the rating by one (stops at the max). |
| ← / ↓ | Lower the rating by one (down to 0 when clearable). |
| Home / End | Jump to the minimum / maximum. |
| Space / Enter | Select the focused symbol. |
<%# Markup for the rating (APG Radio Group, ordinal scale) demo.
Each symbol is role="radio" with an aria-label ("3 stars", etc.). Selection is
aria-checked; the currently filled range is data-rating-hover (the selected value or
a hover/focus preview). The star look is the consumer's CSS. %>
<div class="rating-demo" role="radiogroup" aria-label="<%= t('components.rating.demo.label') %>"
data-controller="stimeo--rating"
data-stimeo--rating-value-value="0" data-stimeo--rating-max-value="5">
<% (1..5).each do |n| %>
<span class="rating-demo__star" role="radio" aria-checked="false"
aria-label="<%= t('components.rating.demo.star', count: n) %>"
tabindex="<%= n == 1 ? 0 : -1 %>"
data-rating-value="<%= n %>" data-stimeo--rating-target="symbol"
data-action="click->stimeo--rating#select
mouseenter->stimeo--rating#preview
mouseleave->stimeo--rating#endPreview
focus->stimeo--rating#preview
blur->stimeo--rating#endPreview
keydown->stimeo--rating#onKeydown">
<svg class="rating-demo__icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2l2.9 6.26 6.86.6-5.2 4.52 1.56 6.72L12 17.1 5.88 20.6l1.56-6.72-5.2-4.52
6.86-.6z"></path>
</svg>
</span>
<% end %>
<input type="hidden" name="rating" value="0" data-stimeo--rating-target="field" />
<%# Subscribe to the change event to show the current value (updated by demo.js). %>
<span class="rating-demo__status" aria-live="polite"
data-empty-text="<%= t('components.rating.demo.empty') %>"
data-rated-template="<%= t('components.rating.demo.rated') %>"><%= t(
'components.rating.demo.empty'
) %></span>
</div>
/*
* Presentation-only styles for the rating demo.
* The currently filled range is expressed via [data-rating-hover] (the selected value
* or a hover/focus preview).
*/
.rating-demo {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.rating-demo__star {
display: inline-flex;
padding: 0.125rem;
border-radius: 0.25rem;
cursor: pointer;
line-height: 0;
}
.rating-demo__star:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.rating-demo__icon {
width: 1.75rem;
height: 1.75rem;
fill: var(--slate-300);
transition: fill 0.12s ease;
}
/* Symbols within the filled range are colored with the accent (amber). */
.rating-demo__star[data-rating-hover] .rating-demo__icon {
fill: var(--amber-500);
}
.rating-demo__status {
margin-left: 0.5rem;
font-size: 0.85rem;
color: var(--color-text-muted);
}
// Demo script that subscribes to the rating change event and shows the current value.
// The copy uses the localized template in a data attribute (substituting "{n}" with
// the value) and an empty-state text.
document.addEventListener('stimeo--rating:change', function (event) {
const status = document.querySelector('.rating-demo__status');
if (!status) return;
const value = event.detail.value;
if (value === 0) {
status.textContent = status.dataset.emptyText || '';
} else {
status.textContent = (status.dataset.ratedTemplate || '').replace('{n}', String(value));
}
});
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--rating"
Targets
| Name | Description | Attribute |
|---|---|---|
symbol
required
|
A rating symbol (role=radio) on the ordinal scale; one is aria-checked. |
data-stimeo--rating-target="symbol" |
field
|
Optional hidden input mirroring the current numeric value. | data-stimeo--rating-target="field" |
Values
| Name | Description | Attribute |
|---|---|---|
value
|
Current rating (default 0). | data-stimeo--rating-value-value |
max
|
Highest rating / number of symbols (default 5). | data-stimeo--rating-max-value |
clearable
|
When true (default), re-clicking the selected symbol clears to 0 and min becomes 0. | data-stimeo--rating-clearable-value |
readonly
|
When true, renders a non-interactive role=img snapshot (default false). |
data-stimeo--rating-readonly-value |
Actions
| Name | Description | Action |
|---|---|---|
endPreview
|
Restores the fill range to the selected value (on mouseleave/blur). | stimeo--rating#endPreview |
onKeydown
|
Arrow/Home/End/Space-Enter keyboard control, clamped without wrapping. | stimeo--rating#onKeydown |
preview
|
Previews a fill range on hover/focus via data-rating-hover. |
stimeo--rating#preview |
select
|
Selects the clicked symbol, or clears to 0 when clearable and already selected. | stimeo--rating#select |
Events
| Name | Description | Event |
|---|---|---|
change
|
Dispatched on every value change, with the new value in detail. | stimeo--rating:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
aria-checked |
Symbol | "true" on the selected symbol only. |
data-rating-hover |
Symbol | Present on symbols within the shown fill range (selection or preview). |
tabindex |
Symbol | 0 on the selected (or first) symbol, -1 on the rest (roving). |