> ## Oymyakon DS index
> Fetch the full index at: https://super-dollop-pzmo65r.pages.github.io/llms.txt
> HTML preview: https://super-dollop-pzmo65r.pages.github.io/rating.html
> Source: specs/components/rating/rating.md, specs/components/rating/rating-a11y.md, specs/components/rating/capabilities.json

---

# Rating · Oymyakon DS 3

> A SlotsContainer of five Slots used to collect or display a score from 0 to 5 (in half-star steps),
> with an optional Description line below.

**Version:** 3.2.3 · **Status:** Ready

---

## 1. Overview

Rating renders five Slots in a horizontal SlotsContainer. Each Slot holds a star icon in one of three
fill states:

- **filled** — `filled/actions/fav-filled` — the star is fully active (score reached)
- **half** — a filled star clipped to its left half over an empty base — score reaches the half step
- **empty** — `filled/actions/fav-filled` drawn in the empty colour — score not reached

The component has two modes: **display** (read-only, shows a value) and **input** (interactive, the
user sets a value by tapping a Slot). Ratings ship in **four sizes** — L / M / S / Mini — each a
separate Figma component (`[Rating-L/M/S/Mini] 3.1`); the structure is identical and only the icon
size and gap differ. RTL is a variant axis (Off / On).

Since **3.2** an **optional Description** line can sit below the SlotsContainer — a supporting caption
such as a score value or review count. It is available on the two larger sizes (L, M), hidden by
default, and truncates to two lines (see § 2, § 7a).

---

## 2. Slot structure

```
Rating                              vertical stack, gap s8
├── SlotsContainer (row)            horizontal, gap by size
│   ├── Slot-1   › star icon › filled / half / empty
│   ├── Slot-2   › star icon › filled / half / empty
│   ├── Slot-3   › star icon › filled / half / empty
│   ├── Slot-4   › star icon › filled / half / empty
│   └── Slot-5   › star icon › filled / half / empty
└── Description   text, centered  (optional · L, M only · since 3.2)
```

The root is a **vertical** stack: the SlotsContainer on top, and an optional Description below it
(`s8` gap). When there is no Description the root is just the SlotsContainer.

Every Slot uses the same `filled/actions/fav-filled` shape. **Empty** = the shape in the empty
colour; **filled** = the shape in the fill colour; **half** = a filled star clipped to its left
half, layered over an empty base (see § 6a).

**Description** (since 3.2) — a single centered text line under the SlotsContainer, spanning its full
width. Optional and hidden by default; available on the **L** and **M** sizes only. Truncates to a
maximum of two lines. Typography and colour are in § 7a.

Every Slot is an instance of [`slot.md`](https://super-dollop-pzmo65r.pages.github.io/slot.md) with:

| Property | Value |
|---|---|
| `width` | `hug` |
| `height` | `hug` |
| `align-horizontal` | `center` |
| `align-vertical` | `center` |
| `padding-top` | `s0` |
| `padding-bottom` | `s0` |

The Slot is invisible — no background, border, or shadow. It only controls the bounding box of the IconContainer inside it.

> Full Slot specification — [`specs/primitives/slot.md`](https://super-dollop-pzmo65r.pages.github.io/slot.md).
> Full IconContainer specification — [`specs/primitives/icon-container.md`](https://github.com/inDriver/oymyakon-ds/blob/main/specs/primitives/icon-container.md).

---

## 3. Anatomy

```
┌────────────────────────────────────────────────┐
│  [★1]  [★2]  [★3]  [★4]  [★5]   ← SlotsContainer │
│               Description                        │  ← optional (L, M · since 3.2), gap s8
│                                                 │
│  ★ = Slot › IconContainer › fav SVG            │
└────────────────────────────────────────────────┘
```

Example: `value = 3`, Description "4.8 · 1 204 reviews"

```
[★ filled]  [★ filled]  [★ filled]  [★ empty]  [★ empty]
             4.8 · 1 204 reviews
```

| Element | Required | Description |
|---|---|---|
| **SlotsContainer** | Required | The horizontal row wrapping the five Slots |
| **Slot-1 … Slot-5** | Required | Five identical Slot+IconContainer pairs. Position index determines fill logic |
| **Description** | Optional | Single centered caption below the SlotsContainer (L, M only; since 3.2). Truncates to 2 lines. See § 7a |

---

## 4. Layout & Spacing

The root is a **vertical** autolayout (hug height, hug width) holding the SlotsContainer and the
optional Description. The SlotsContainer itself is a horizontal autolayout.

```
root  (vertical, hug × hug)
  padding: var(--rating-padding-y) var(--rating-padding-x)   ← both default s0 (see § 4b)
  gap (SlotsContainer → Description):  var(--sp-s8)   ← only present when a Description is shown
  └── SlotsContainer (horizontal)
        gap (Slot-N → Slot-N+1): depends on size (see §5)
  └── Description (optional, full width, centered)
```

Slots are always evenly spaced; the gap token is the same for all five gaps. The
SlotsContainer↔Description gap is `var(--sp-s8)` by default (changeable to any DS spacing token). With
no Description the root collapses to just the SlotsContainer.

### 4b. Container padding (since 3.2.2)

The root `.rating` carries an optional, configurable padding — the inset between the container edge
and its content (the SlotsContainer + Description). Horizontal and vertical are set independently via
two custom properties, **both defaulting to `var(--sp-s0)`** (no padding — the base look is unchanged):

| Property | Default | Controls |
|---|---|---|
| `--rating-padding-x` | `var(--sp-s0)` | Left & right padding |
| `--rating-padding-y` | `var(--sp-s0)` | Top & bottom padding |

Set either token per instance to any DS spacing token to inset the content — e.g.
`style="--rating-padding-x: var(--sp-s16); --rating-padding-y: var(--sp-s16);"` gives an `s16` inset on
all sides. Padding is a container property; it does not change icon size, the inter-Slot gap, or the
SlotsContainer↔Description gap.

### 4a. SlotsContainer resize modes

The SlotsContainer supports two horizontal-resize modes, chosen by how the Rating is placed:

| Mode | SlotsContainer width | Gap between Slots | When |
|---|---|---|---|
| **Hug** (default) | Wraps the five Slots (content width) | Fixed — the per-size token (see § 5; L `var(--sp-s6)`) | Rating sizes itself; the row is exactly `5 × icon + 4 × gap` wide |
| **Fill** | Stretches to a fixed-width parent | **Auto** — the free space is distributed evenly between the Slots (space-between) | Rating is dropped into a container with a set width; the five Slots spread across the full width |

In **Fill** mode the gap is no longer the size token — it is computed from the parent width:
`gap = (parentWidth − 5 × icon) ÷ 4`. Example (Figma `[Rating-L] 3.2`, fill): a `611`-wide parent with
`56` icons yields a `82.75` auto gap; the same L in **Hug** is `304` wide with a fixed `var(--sp-s6)`
gap. Fill only affects the horizontal distribution of the Slots — icon size, the vertical stack, and
the Description are unchanged.

---

## 5. Size

Four sizes, each a separate Figma component (`[Rating-L/M/S/Mini] 3.2`). All values are SP tokens.
The gap is not a linear function of icon size — use the exact token per size.

| Size | Icon | Gap | Figma |
|---|---|---|---|
| **L** | `var(--sp-s56)` | `var(--sp-s6)` | `[Rating-L] 3.2` |
| **M** | `var(--sp-s40)` | `var(--sp-s8)` | `[Rating-M] 3.2` |
| **S** | `var(--sp-s24)` | `var(--sp-s4)` | `[Rating-S] 3.2` |
| **Mini** | `var(--sp-s16)` | `var(--sp-s2)` | `[Rating-Mini] 3.2` |

**Default:** `M`

Icon size maps directly to `width` and `height` of each star `<svg>` via CSS.

---

## 6. Value

A number from `0` to `5` in **half-star steps** (`0.5` increments). Each whole part fills a star
fully; a trailing `.5` renders the next star as a **half star** (filled left half over an empty
base). The Figma `Fill` variant exposes representative values (`empty`, `0.5`, `3`, `3.5`, `4.5`,
`5 stars`); any `n` or `n.5` in 0–5 follows the same rule.

| `value` | Slot-1 | Slot-2 | Slot-3 | Slot-4 | Slot-5 |
|---|---|---|---|---|---|
| `0` | empty | empty | empty | empty | empty |
| `0.5` | half | empty | empty | empty | empty |
| `3` | filled | filled | filled | empty | empty |
| `3.5` | filled | filled | filled | half | empty |
| `4.5` | filled | filled | filled | filled | half |
| `5` | filled | filled | filled | filled | filled |

**Default:** `0`

Half steps are for **display** (showing an aggregated score like 3.5). **Input** mode commits whole
stars only — tapping star-N sets `value = N` (see § 7).

### 6a. Half-star construction

A half star is two layers in one Slot (matching the Figma `fav-half` frame):

1. **Base** — the full `fav-filled` star in the **Slot-background** colour (`--surface-overlay`).
2. **Overlay** — the same star in the **fill** colour (`--text-and-icon-primary` by default),
   clipped to its **left half** (`clip-path: inset(0 50% 0 0)`), layered on top.

The result reads as a star whose left half is filled and right half is empty. In RTL the clip
mirrors to the right half (`inset(0 0 0 50%)`) so the fill still begins from the value's leading
edge.

---

## 7. Mode

| Mode | Description |
|---|---|
| `display` | Read-only. No hover, no press, no focus. Renders the current `value` only |
| `input` | Interactive. The user taps a Slot to set `value`. Supports hover preview and press states |

**Default:** `display`

---

## 8. States

### 8.1 Display mode

| State | Visual |
|---|---|
| `Standard` | Slots up to `value` — fill colour; Slots above — empty colour. The rest state. |
| `disabled` | All Slots — `var(--text-and-icon-disabled)`. `value` ignored visually |

### 8.2 Input mode

| State | Trigger | Visual |
|---|---|---|
| `Standard` | No interaction | The rest state — same as display `Standard` |
| `hover` (desktop) | Cursor over Slot-N | Slots 1…N preview as filled; Slots N+1…5 preview as empty. Not committed |
| `press` | Pointer down on Slot-N | Slot-N scales down (`pushItem`). Preview same as hover |
| `active` | Pointer up on Slot-N | `value` set to N. Scale returns to 100% |
| `disabled` | `disabled: true` | Same as display disabled. Pointer events suppressed |

Hover state does not persist on touch devices — preview is shown only during the press gesture.

---

## 9. Color tokens

| Token | Variable | Usage |
|---|---|---|
| **Slot foreground (filled)** | `var(--text-and-icon-primary)` | Active Slots (filled icon) — default |
| **Slot background (empty)** | `var(--surface-overlay)` | Inactive Slots — the Slot-background layer (`fav-filled` shape); can be changed to any DS colour |
| **Disabled** | `var(--text-and-icon-disabled)` | All Slots in disabled state |

`color` is a configurable property on the component. The designer can override the filled Slot colour with any `TextAndIcon/*` token; the Slot background is `var(--surface-overlay)` by default and can likewise be changed to any DS colour.

| `color` value | Variable | Description |
|---|---|---|
| `primary` (default) | `var(--text-and-icon-primary)` | Neutral primary |
| `accent` | `var(--text-and-icon-accent)` | Informational / link accent |
| `brand` | `var(--text-and-icon-brand)` | TextAndIcon/Brand — Drive Green/250 |

### 9a. Description (since 3.2)

The optional Description is a single centered caption below the SlotsContainer. Available on **L** and
**M** only.

| Size | Text style | Tokens |
|---|---|---|
| **L** | Main Body | `var(--text-body-main-body-*)` |
| **M** | Compact Body | `var(--text-body-compact-body-*)` |

| Property | Value |
|---|---|
| Colour | `var(--text-and-icon-secondary)` by default — changeable to any `TextAndIcon/*` DS colour |
| Alignment | Centered, full width of the SlotsContainer |
| Truncation | Clamp to a maximum of **2 lines**, ellipsis overflow |
| Gap (SlotsContainer → Description) | `var(--sp-s8)` (see § 4) |
| Visibility | Hidden by default; shown per instance |

---

## 10. Motion

Input mode only. Display mode has no animation.

Every duration and curve is carried by a motion prop defined in `shared.css` (see §11):
`--rating-state-fast` (= `Transforming/State/Fast`, hover), `--rating-push-item-press` /
`--rating-push-item-release` (= the catalogued `pushItem` recipe, press/release) and the DS-wide
`--component-bounce-wave-step` / `--component-bounce-wave-bounce` / `--component-bounce-wave-fill`
(= the catalogued **`bounceWave`** selection recipe, `motion-rules.md` § 6.3 / Motion 1.6.0 —
Rating is its reference implementation).

### Tap — bounceWave

Tapping Slot N replays the fill from the very first Slot: Slots 1…N animate as a sequential wave,
each Slot starting `--component-bounce-wave-step` (70ms) after the previous one, bouncing and
revealing its fill at its bounce peak. Slots above N unfill instantly. This is the catalogued
`bounceWave` selection recipe (`motion-rules.md` § 6.3).

| Phase | Property | Value |
|---|---|---|
| Wave stagger | per-Slot start delay | Slot *i* starts at (*i* − 1) × `--component-bounce-wave-step` (70ms) |
| Bounce up | `transform: scale(1.28)` | peaks at 110ms into the `--component-bounce-wave-bounce` (380ms) bounce |
| Fill reveal | `clip-path` left→right | fires at each Slot's peak (110ms), runs `--component-bounce-wave-fill` (160ms) |
| Bounce down | `transform: scale(1.0)` | returns over the remaining bounce duration |
| Slots above N | `clip-path` instant | unfill instantly — no transition, no bounce |

### Hover — scale only the hovered Slot

| Event | Behaviour |
|---|---|
| `mouseenter` Slot-N | Slot-N scales to ×1.18 (`--rating-state-fast`). Slots 1…N fill instantly (clip-path, no transition). Slots N+1…5 unfill instantly |
| `mouseleave` Slot-N | Slot-N scale resets. No clip change on individual Slot leave |
| `mouseleave` rating container | All scales reset. Clip restores to committed value |

### Press — pushItem

The pressed Slot follows the catalogued `pushItem` recipe (`motion-rules.md` §6.1): scale
100% → 95% on press (`--rating-push-item-press`), back on release (`--rating-push-item-release`),
both 200ms standard-ease-in-out. The release prop is also the Slot's base `transition`, so leaving
hover settles at the same recipe.

### Reduced motion

`@media (prefers-reduced-motion: reduce)` overrides every Rating motion prop (`--rating-*` and
`--component-bounce-wave-*`) to `0ms` at `:root`. Hover and press transitions become instant, and
the input JS reads `--component-bounce-wave-bounce` before animating — at `0ms` it skips the wave
entirely and applies all fills instantly. No component-level override is needed.

---

## 11. CSS Implementation

Each Slot holds a `.star-layers` grid that stacks two copies of the same `fav-filled` SVG: an
**empty base** (`.star-svg--empty`) and a **fill overlay** (`.star-svg--filled`) laid over it in the
same grid cell. The fill state is set by the overlay's `clip-path` — `inset(0 0% 0 0)` full,
`inset(0 50% 0 0)` half, `inset(0 100% 0 0)` empty. Per-size icon dimensions are set on `.star-svg`;
the gap is on the SlotsContainer.

```css
/* Root — vertical stack: SlotsContainer + optional Description (since 3.2) */
.rating {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-s8);              /* SlotsContainer → Description; collapses when no Description */
  /* Optional container padding (since 3.2.2); both default s0 — see § 4b. */
  padding: var(--rating-padding-y, var(--sp-s0)) var(--rating-padding-x, var(--sp-s0));
}

/* SlotsContainer — the horizontal row of Slots */
.rating__slots-container {
  display: flex;
  align-items: center;
}

/* Slot — invisible bounding box holding one star (its two layers) */
.rating__slot {
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  padding: 0;
  cursor: default;
  flex-shrink: 0;
}

/* Two-layer star: empty base + fill overlay stacked in one grid cell */
.star-layers {
  display: grid;
  align-items: center;
  justify-items: center;
}
.star-layers > .star-svg {
  grid-area: 1 / 1;               /* stack both SVGs in the same cell */
  display: block;
  fill: currentColor;
}
/* Fill state = clip on the overlay. inset(0 0% 0 0) full · inset(0 50% 0 0) half ·
   inset(0 100% 0 0) empty. Set inline per Slot (SSR) or by JS. Default empty here. */
.star-svg--filled { clip-path: inset(0 100% 0 0); }
/* RTL mirrors the half clip so fill begins at the leading edge (inline: inset(0 0 0 50%)). */

/* Sizes — icon size on .star-svg; gap is per-size (not linear) on the SlotsContainer.
   This is the Hug mode (default): the SlotsContainer wraps its Slots with the fixed per-size gap. */
.rating--mini .rating__slots-container { gap: var(--sp-s2); }
.rating--s    .rating__slots-container { gap: var(--sp-s4); }
.rating--m    .rating__slots-container { gap: var(--sp-s8); }
.rating--l    .rating__slots-container { gap: var(--sp-s6); }

/* Fill mode — the Rating stretches to a fixed-width parent and the SlotsContainer distributes the
   free space evenly between the Slots (auto gap). Overrides the fixed per-size gap. */
.rating--fill,
.rating--fill .rating__slots-container { width: 100%; }
.rating--fill .rating__slots-container {
  justify-content: space-between;
  gap: 0;                          /* space-between owns the distribution in fill mode */
}

.rating--mini .star-svg { width: var(--sp-s16); height: var(--sp-s16); }
.rating--s    .star-svg { width: var(--sp-s24); height: var(--sp-s24); }
.rating--m    .star-svg { width: var(--sp-s40); height: var(--sp-s40); }
.rating--l    .star-svg { width: var(--sp-s56); height: var(--sp-s56); }

/* Colours — empty base = Slot-background colour; filled overlay = fill colour */
.star-svg--empty                   { color: var(--surface-overlay); }
.star-svg--filled                  { color: var(--text-and-icon-primary); }
.rating--primary .star-svg--filled { color: var(--text-and-icon-primary); }
.rating--accent  .star-svg--filled { color: var(--text-and-icon-accent);  }
.rating--brand   .star-svg--filled { color: var(--text-and-icon-brand);   }

/* Disabled — both layers to the disabled colour */
.rating--disabled .rating__slot     { pointer-events: none; }
.rating--disabled .star-svg--filled { color: var(--text-and-icon-disabled); }
.rating--disabled .star-svg--empty  { color: var(--text-and-icon-disabled); }

/* Input mode — Slot is interactive; hover / press scales are token-driven (§ 10).
   Base transition = pushItem release; hover-in = Transforming/State/Fast; press = pushItem press. */
.rating--input .rating__slot {
  cursor: pointer;
  transition: transform var(--rating-push-item-release);
}
.rating--input .rating__slot:hover  { transform: scale(1.18); transition: transform var(--rating-state-fast); }
.rating--input .rating__slot:active { transform: scale(0.95); transition: transform var(--rating-push-item-press); }

/* Motion props (shared.css :root) — zeroed under prefers-reduced-motion: reduce */
:root {
  --rating-state-fast:        150ms cubic-bezier(0.4, 0, 0.2, 1);   /* Transforming/State/Fast */
  --rating-push-item-press:   200ms cubic-bezier(0.25, 1, 0.5, 1);  /* pushItem §6.1 */
  --rating-push-item-release: 200ms cubic-bezier(0.25, 1, 0.5, 1);  /* pushItem §6.1 */
  --component-bounce-wave-step:   70ms;   /* bounceWave §6.3 — per-Slot stagger */
  --component-bounce-wave-bounce: 380ms;  /* bounceWave §6.3 — scale peak ×1.28 at 110ms */
  --component-bounce-wave-fill:   160ms;  /* bounceWave §6.3 — clip-path reveal at the peak */
}

/* Description (since 3.2) — a TextRow instance below the SlotsContainer, L and M only.
   .rating__description (also .text-row) sets colour + centering; the text node clamps + sizes. */
.rating__description {
  color: var(--text-and-icon-secondary);
  justify-content: center;
}
.rating__description .text-row__text {
  text-align: center;
  flex: 0 1 auto;
  /* clamp to 2 lines with ellipsis */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
/* Per-size typography: L → Main Body, M → Compact Body */
.rating--l .rating__description .text-row__text {
  font-family: var(--text-body-main-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-main-body-size);
  font-weight: var(--text-body-main-body-weight);
  line-height: var(--text-body-main-body-line-height);
}
.rating--m .rating__description .text-row__text {
  font-family: var(--text-body-compact-body-family), 'Noto Sans', sans-serif;
  font-size: var(--text-body-compact-body-size);
  font-weight: var(--text-body-compact-body-weight);
  line-height: var(--text-body-compact-body-line-height);
}
/* S and Mini do not carry a Description */
.rating--s .rating__description,
.rating--mini .rating__description { display: none; }
```

> **Motion (input mode).** Hover and press scales are part of the component CSS above:
> `.rating--input .rating__slot` transitions `transform` via the `--rating-*` motion props —
> `:hover` scales ×1.18 (`--rating-state-fast` = `Transforming/State/Fast`), `:active` scales to
> 95% (`--rating-push-item-press` / `-release` = the catalogued `pushItem` recipe). The tap
> wave (bounce + fill reveal per Slot) is driven by JS reading the DS-wide
> `--component-bounce-wave-step` / `-bounce` / `-fill` props — the catalogued `bounceWave`
> selection recipe (`motion-rules.md` § 6.3); Rating is its reference implementation. Every
> motion prop resolves to `0ms` under `prefers-reduced-motion: reduce`, so no component-level
> override is needed.

### HTML structure

The root is a vertical stack; the Slots live in `.rating__slots-container`. Every Slot holds a
`.star-layers` grid with two stacked `fav-filled` SVGs — an empty base and a fill overlay; the
overlay's `clip-path` sets the fill (full / half / empty). Input mode — whole-star buttons.

```html
<!-- Input mode — a value of 3: Slots 1–3 fully clipped-in, 4–5 clipped out -->
<div class="rating rating--m rating--input" role="group" aria-label="Rating">
  <div class="rating__slots-container">
    <button class="rating__slot" data-value="1" aria-label="1 star">
      <span class="star-layers">
        <svg class="star-svg star-svg--empty"><!-- fav-filled --></svg>
        <svg class="star-svg star-svg--filled" style="clip-path: inset(0 0% 0 0);"><!-- fav-filled --></svg>
      </span>
    </button>
    <!-- …Slots 2–3 same (clip inset(0 0% 0 0)); Slots 4–5 clip inset(0 100% 0 0)… -->
  </div>
</div>
```

Display mode with a half star — the fourth Slot's fill overlay is clipped to its left half:

```html
<div class="rating rating--m" role="img" aria-label="Rating: 3.5 out of 5">
  <div class="rating__slots-container">
    <!-- Slots 1–3: fill overlay clip-path: inset(0 0% 0 0) -->
    <span class="rating__slot" aria-hidden="true">
      <span class="star-layers">
        <svg class="star-svg star-svg--empty"><!-- fav-filled --></svg>
        <svg class="star-svg star-svg--filled" style="clip-path: inset(0 0% 0 0);"><!-- fav-filled --></svg>
      </span>
    </span>
    <!-- Slot 4: half — fill overlay clipped to the left half (RTL: inset(0 0 0 50%)) -->
    <span class="rating__slot" aria-hidden="true">
      <span class="star-layers">
        <svg class="star-svg star-svg--empty"><!-- fav-filled (base) --></svg>
        <svg class="star-svg star-svg--filled" style="clip-path: inset(0 50% 0 0);"><!-- fav-filled (fill) --></svg>
      </span>
    </span>
    <!-- Slot 5: empty — fill overlay clip-path: inset(0 100% 0 0) -->
  </div>
</div>
```

Display mode with a Description (L / M only, since 3.2) — the caption sits below the SlotsContainer:

```html
<div class="rating rating--l" role="img" aria-label="Rating: 4 out of 5, 1 204 reviews">
  <div class="rating__slots-container">
    <!-- 5 Slots, each a .star-layers with empty base + clipped fill overlay -->
  </div>
  <span class="rating__description text-row text-row--multiline" aria-hidden="true">
    <span class="text-row__text">4.8 · 1 204 reviews</span>
  </span>
</div>
```

In display mode, Slots are `<span>` (not buttons); the wrapper uses `role="img"` with a full-value
`aria-label`. Input mode commits whole stars only, so half stars never appear in input markup.

---

## 12. Accessibility

### Display mode

```html
<div class="rating rating--m" role="img" aria-label="Rating: 3 out of 5">
  <div class="rating__slots-container"><!-- 5 aria-hidden spans with SVGs --></div>
</div>
```

All five Slot spans are `aria-hidden="true"`. The single `aria-label` on the wrapper conveys the full value.

When a **Description** is present, fold its text into the wrapper's `aria-label` (e.g.
`aria-label="Rating: 4 out of 5, 1 204 reviews"`) and keep the visible `.rating__description` element
`aria-hidden="true"`, so the score and caption are announced once as a single image label rather than
twice.

### Input mode

```html
<div class="rating rating--m rating--input" role="group" aria-label="Rating">
  <button aria-label="1 star"  aria-pressed="true">…</button>
  <button aria-label="2 stars" aria-pressed="true">…</button>
  <button aria-label="3 stars" aria-pressed="true">…</button>
  <button aria-label="4 stars" aria-pressed="false">…</button>
  <button aria-label="5 stars" aria-pressed="false">…</button>
</div>
```

`aria-pressed="true"` on all stars up to and including the current `value`. Keyboard: `Tab` moves focus between stars, `Space` / `Enter` sets the value.

### Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Rating wrapper (display) | "Rating" | "3 out of 5" | `IMAGE` | — |
| Slot button (input) | "1 star" / "2 stars" / … | — | `BUTTON` | "Double-tap to set" |

### iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Rating wrapper (display) | "Rating, 3 out of 5" | — | `.image` | — |
| Slot button (input) | "1 star" / "2 stars" / … | — | `.button` | "Double-tap to activate" |

---

## 13. Usage context

### When to use

- Collecting a score after a completed trip, service, or product.
- Displaying an aggregated rating alongside a count label (e.g. "4.8 · 1 204 reviews").
- Inline in a Cell (end-slot or description area) to show item quality at a glance.

### When not to use

- When a score has more than 5 levels — use a Slider or numeric Input.
- When the user needs to pick from labeled options — use a Radio group.
- When precision beyond half-star steps is needed — Rating supports whole and half stars only (0–5 in 0.5 steps).

---

## 14. Analytics and coverage contract

Reference: [`docs/prototype-analytics-and-coverage.md`](https://github.com/inDriver/oymyakon-ds/blob/main/docs/prototype-analytics-and-coverage.md).

| Field | Value |
|---|---|
| `data-ds-component` | `Rating` |
| Coverage unit | Yes. One Rating root counts as one DS component instance |
| Tap target model | Display mode: none. Input mode: five internal Slot targets |
| Actions | `select` |
| Internal targets | `star-1`, `star-2`, `star-3`, `star-4`, `star-5` (one per Slot; identifiers kept stable for the analytics contract) |
| Emits value | Yes. `data-ds-value` is the selected integer from `1` to `5` |

Required input-mode prototype markup:

```html
<div
  class="rating rating--m rating--input"
  data-ds-component="Rating"
  data-ds-component-id="checkout.driver-rating"
  data-ds-variant="input"
  data-ds-state="default">
  <button data-ds-action="select" data-ds-target="star-1" data-ds-value="1">...</button>
  <button data-ds-action="select" data-ds-target="star-2" data-ds-value="2">...</button>
  <button data-ds-action="select" data-ds-target="star-3" data-ds-value="3">...</button>
  <button data-ds-action="select" data-ds-target="star-4" data-ds-value="4">...</button>
  <button data-ds-action="select" data-ds-target="star-5" data-ds-value="5">...</button>
</div>
```

Display mode still uses `data-ds-component="Rating"` for coverage, but does not define `data-ds-action` and does not emit tap analytics.

---

## RTL

**Default: RTL = Off.**

RTL is a variant axis in Figma (`RTL: Off / On`); default **Off**.

| Element | RTL behaviour |
|---|---|
| Rating root | `dir="rtl"` reverses Slot order: Slot-5 appears on the left, Slot-1 on the right |
| Fill logic | Unchanged — Slots fill from Slot-1 up to `value`, regardless of visual direction |
| Star shape (`fav-filled`) | Symmetric — no flip needed (`rtl: false`) |
| Half-star clip | Mirrors: the fill overlay clips to the right half (`inset(0 0 0 50%)`) so the fill still begins at the value's leading edge |

In RTL layout a value of 3 renders as: `[empty] [empty] [★] [★] [★]` (reading right to left). The star shape itself does not mirror; only the half-star clip side flips.

---

## Changelog

Newest entry first.

Version scheme `3.X.Y`: `3` = Oymyakon system major (fixed) · `X` = component major, tracking the
**Figma component** (`[Rating] 3.2`) · `Y` = repo-side minor. Numbering realigned to the Figma
lineage twice: the old repo-only 3.3–3.8 chain was superseded in 2026-07, and on 2026-07-27 the
post-3.2.0 repo bumps were renumbered `3.3.0/3.3.1/3.3.2` → `3.2.1/3.2.2/3.2.3` (same entries).

| Version | Date | Change |
|---|---|---|
| 3.2.3 | 2026-07-24 | **Motion catalogued; § 10 corrected to the real tap behaviour** — tapping Slot N replays the fill from the very first Slot as a sequential **bounce wave** (Slots 1…N, 70ms stagger, each revealing its fill at its bounce peak; the spec previously claimed only the tapped Slot animates). The wave is now the catalogued **`bounceWave`** selection recipe (`motion-rules.md` § 6.3, Motion 1.6.0) with DS-wide props `--component-bounce-wave-step` / `-bounce` / `-fill` — Rating is the reference implementation, the former ⚠️ un-catalogued TODO is resolved. Hover gains the § 10 instant fill-preview (was spec'd, not implemented); **press implemented** as the catalogued `pushItem` recipe (`:active` scale 0.95). Hover/press scales moved from inline JS into the component CSS, driven by motion props; `--rating-state-fast` curve corrected to `Transforming/State/Fast` (slow-ease-out). All props resolve to `0ms` under `prefers-reduced-motion` — the JS reads them and skips the wave at 0ms. §§ 10–11 updated to the real prop names (was `--component-push-item-*`). |
| 3.2.2 | 2026-07-23 | **Configurable container padding** — the root `.rating` gains an optional inset via `--rating-padding-x` / `--rating-padding-y` (both default `var(--sp-s0)`, so the base look is unchanged). Set per instance to any DS spacing token to pad the SlotsContainer + Description (see § 4b). Padding does not affect icon size or the inter-Slot / SlotsContainer↔Description gaps. |
| 3.2.1 | 2026-07-23 | **Container rename** — the three internal containers are renamed for a consistent Slot vocabulary: the star row → **SlotsContainer** (`.rating__stars` → `.rating__slots-container`), each star position → **Slot** (`.rating__star*` → `.rating__slot*`, incl. `--empty` / `--half` / `--filled` / `--disabled` and `rating__slot-fill`), and the subtitle → **Description** (`.rating__subtitle` → `.rating__description`). CSS-class contract change (structural); prose, anatomy, and examples updated throughout. **SlotsContainer resize modes** documented (§ 4a): **Hug** (default, wraps Slots with the fixed per-size gap) and **Fill** (`.rating--fill` — stretches to a fixed-width parent, gap auto-distributes space-between). §11 CSS realigned to the real `star-layers` / `star-svg` two-layer implementation. The star SVG glyph terms (`fav-filled`, `star-svg`) and the analytics `data-ds-target="star-N"` identifiers are unchanged. |
| 3.2.0 | 2026-07-09 | Figma `[Rating] 3.2` — **"Subtitle has been added".** Optional centered caption below the SlotsContainer (`s8` gap), on the **L** and **M** sizes only. Typography L → Main Body, M → Compact Body; colour `--text-and-icon-secondary` by default (recolorable); truncates to 2 lines. Root is now a vertical stack (`.rating` wraps the SlotsContainer + Description). Slot background (empty/base star) is `--surface-overlay` (configurable). Hidden by default. |
| 3.1.0 | 2026-06-18 | **Four sizes**, each a separate Figma component (L `s56`/gap `s6` · M `s40`/gap `s8` · S `s24`/gap `s4` · Mini `s16`/gap `s2`) — dropped Micro/XS/XL. Added **half-star** values: `0–5` in `0.5` steps; a half star is a filled overlay clipped to the left half over an empty base (§6a). Star fill classes → `--empty` / `--half`; every star uses the `fav-filled` shape (empty = same shape in the disabled colour). RTL mirrors the half-clip. Press uses the `pushItem` recipe via `--component-push-item-*`. Numbering realigned to the Figma component (superseding the divergent repo-only 3.3–3.8 chain). |
| 3.0.0 | 2026-04-29 | Initial version in Oymyakon DS 3. Five star positions, `fav-filled` shape, modes display/input, values `0–5`, colour tokens primary/accent/brand, full a11y (TalkBack + VoiceOver). |

---

# Rating · Accessibility Spec · Oymyakon DS 3

**Version:** 3.2.3 · **Status:** Ready · **Linked component:** [`rating.md`](https://super-dollop-pzmo65r.pages.github.io/rating.md)

---

## 1. Roles and Semantics

### Display mode

The Rating wrapper uses `role="img"` with a descriptive `aria-label`. All five Slot spans are `aria-hidden="true"` — the label conveys the full value to screen readers, **including half steps** (e.g. `aria-label="Rating: 3.5 out of 5"`). Input mode commits whole stars only, so half values appear only in display mode.

```html
<div class="rating rating--m" role="img" aria-label="Rating: 3 out of 5">
  <div class="rating__slots-container">
    <span class="rating__slot" aria-hidden="true">…</span>
    <span class="rating__slot" aria-hidden="true">…</span>
    <span class="rating__slot" aria-hidden="true">…</span>
    <span class="rating__slot" aria-hidden="true">…</span>
    <span class="rating__slot" aria-hidden="true">…</span>
  </div>
</div>
```

**Description (since 3.2).** When the optional Description is present (L / M sizes), fold its text into the wrapper's `aria-label` and mark the visible `.rating__description` element `aria-hidden="true"`, so the score and caption are announced once as a single image label — never twice.

```html
<div class="rating rating--l" role="img" aria-label="Rating: 4 out of 5, 1 204 reviews">
  <div class="rating__slots-container"><!-- 5 aria-hidden Slot spans --></div>
  <span class="rating__description" aria-hidden="true">4.8 · 1 204 reviews</span>
</div>
```

### Input mode

The wrapper uses `role="group"` with `aria-label="Rating"`. Each Slot is a `<button>` with:
- `aria-label` — "1 star", "2 stars", …, "5 stars"
- `aria-pressed` — `"true"` for all stars ≤ committed `value`, `"false"` for stars above

```html
<div class="rating rating--m rating--input" role="group" aria-label="Rating">
  <button aria-label="1 star"  aria-pressed="true">…</button>
  <button aria-label="2 stars" aria-pressed="true">…</button>
  <button aria-label="3 stars" aria-pressed="true">…</button>
  <button aria-label="4 stars" aria-pressed="false">…</button>
  <button aria-label="5 stars" aria-pressed="false">…</button>
</div>
```

When `disabled`, buttons get the native `disabled` attribute — they are skipped by screen readers automatically.

---

## 2. Keyboard Navigation

| Key | Behavior |
|---|---|
| `Tab` | Moves focus from Slot to Slot (forward) |
| `Shift+Tab` | Moves focus from Slot to Slot (backward) |
| `Space` | Sets value to the focused Slot's index |
| `Enter` | Sets value to the focused Slot's index |

Display mode has no keyboard interaction — the wrapper has no focusable descendants.

---

## 3. Focus Indicator

Focus follows the browser's default outline. Do not suppress `outline` on `.rating__slot`. The Slot button has sufficient tap/click target (minimum `--sp-s40` for M size). No custom focus ring is needed — the browser's default outline is sufficient.

---

## 4. Contrast Requirements

| Element | Token | Requirement |
|---|---|---|
| Filled star (primary) | `--text-and-icon-primary` on `--background-primary` | ≥ 3:1 (UI component, non-text) |
| Filled star (accent) | `--text-and-icon-accent` on `--background-primary` | ≥ 3:1 |
| Filled star (brand) | `--text-and-icon-brand` on `--background-primary` | ≥ 3:1 |
| Empty star (Standard) | `--surface-overlay` on `--background-primary` | Decorative — no contrast requirement (fills only up to `value`) |
| Empty / filled star (disabled state) | `--text-and-icon-disabled` on `--background-primary` | Decorative — no contrast requirement |

---

## 5. Platform Screen Reader Behavior

### Android · TalkBack

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Rating wrapper (display) | "Rating" | "3 out of 5" / "3.5 out of 5" | `IMAGE` | — |
| Slot button (input) | "1 star" / "2 stars" / … | — | `BUTTON` | "Double-tap to set" |

### iOS · VoiceOver

| Element | Label | Value | Trait | Hint |
|---|---|---|---|---|
| Rating wrapper (display) | "Rating, 3 out of 5" | — | `.image` | — |
| Slot button (input) | "1 star" / "2 stars" / … | — | `.button` | "Double-tap to activate" |

---

## 6. Reduced Motion

`@media (prefers-reduced-motion: reduce)` overrides every Rating motion prop (`--rating-*` and the DS-wide `--component-bounce-wave-*` of the `bounceWave` recipe) to `0ms` at `:root`. No component-level override needed — hover/press transitions resolve to zero, and the input JS reads `--component-bounce-wave-bounce` before animating, skipping the tap wave entirely at `0ms` (see component spec § 10).

```css
@media (prefers-reduced-motion: reduce) {
  :root {
    --rating-state-fast:            0ms linear;
    --rating-push-item-press:       0ms linear;
    --rating-push-item-release:     0ms linear;
    --component-bounce-wave-step:   0ms;
    --component-bounce-wave-bounce: 0ms;
    --component-bounce-wave-fill:   0ms;
  }
}
```

When reduced motion is on: Slots update fill state instantly with no bounce animation and no scale transitions.

---

## Changelog

> Note: component spec **3.2.2** (configurable container padding, `--rating-padding-x/y`) introduces no
> accessibility-relevant change — roles, labels, focus order, and announcements are unaffected — so this
> a11y spec skipped it and goes straight from **3.2.1** to **3.2.3**. (Lineage renumbered 2026-07-27 to
> track the Figma component `[Rating] 3.2`: old `3.3.0/3.3.2` = `3.2.1/3.2.3`.)

| Version | Date | Change |
|---|---|---|
| 3.2.3 | 2026-07-24 | § 6 Reduced Motion aligned to the motion reconcile: the override now lists the real props (`--rating-*` + the `bounceWave` recipe's `--component-bounce-wave-*`; was non-existent `--transforming-state-fast` / `--pattern-color` / `--component-push-item-*` names), and `prefers-reduced-motion` is now actually honored by the input JS — it reads `--component-bounce-wave-bounce` and skips the tap wave at `0ms`, applying all fills instantly. Announcement semantics unchanged. |
| 3.2.1 | 2026-07-23 | Container rename to the Slot vocabulary — examples and prose updated: `.rating__stars` → `.rating__slots-container` (SlotsContainer), `.rating__star*` → `.rating__slot*` (Slot), `.rating__subtitle` → `.rating__description` (Description). Announcement semantics unchanged. Contrast table corrected: the Standard-state empty star is `--surface-overlay` (was mislabeled `--text-and-icon-disabled`, which applies only in the disabled state). |
| 3.2.0 | 2026-07-09 | Description (L / M) folded into the wrapper `aria-label`; the visible `.rating__description` is `aria-hidden` so score + caption announce once. `.rating__slots-container` wrapper added to the display example. |
| 3.1.0 | 2026-06-18 | Aligned to the 4-size / half-star sync and the `3.X.Y` scheme. Display `aria-label` now carries half steps (e.g. "Rating: 3.5 out of 5"); input mode still commits whole stars only. |
| 1.0.0 | 2026-05-19 | Initial a11y spec — roles, keyboard, contrast, TalkBack/VoiceOver, reduced motion |

---

## Machine contract — `specs/components/rating/capabilities.json`

Axes, allowed values, defaults, constraints and CSS/Figma bindings. Read this instead of guessing what the component can do.

```json
{
  "$schema": "../component-capabilities.schema.json",
  "id": "rating",
  "name": "Rating",
  "version": "3.2.3",
  "description": "A SlotsContainer of five Slots collecting or displaying a 0–5 score (half-star steps), with an optional Description line below.",
  "files": {
    "spec": "specs/components/rating/rating.md",
    "a11y": "specs/components/rating/rating-a11y.md",
    "preview": "src/rating.njk",
    "css": "src/shared/shared.css"
  },
  "figma": {
    "library": "🕹️ Oymyakon 3.26.0 (components)",
    "fileKey": "7vdl5YkZFDWvh9QvSmydsH",
    "componentSets": {
      "[Rating-L]": { "key": "73520f298f50f4713a1723a395201576f1643552" },
      "[Rating-M]": { "key": "5b38f4db74fe644c5faf515db9c864dd0b81441e" },
      "[Rating-S]": { "key": "c315718d6087249db924baa034e8ecb339bb8027" },
      "[Rating-Mini]": { "key": "202034f21302e7dd32c72645bb6c544b71532cf9" }
    },
    "capturedAt": "2026-07-09"
  },
  "root": { "class": "rating", "dataDsComponent": "Rating" },
  "anatomy": {
    "slotsContainer": {
      "class": "rating__slots-container",
      "notes": "Horizontal row wrapping the five Slots. Two resize modes — see axis `resize`."
    },
    "slot": {
      "class": "rating__slot",
      "count": 5,
      "notes": "One star position: a .star-layers grid stacking .star-svg--empty (base) + .star-svg--filled (overlay, clip-path sets the fill)."
    },
    "description": {
      "class": "rating__description",
      "optional": true,
      "onlyWhen": { "size": ["l", "m"] },
      "notes": "A TextRow instance below the SlotsContainer; centered, clamps to 2 lines. Hidden by default."
    }
  },
  "axes": {
    "size": {
      "title": "Size",
      "type": "enum",
      "values": ["l", "m", "s", "mini"],
      "default": "m",
      "css": { "modifierTemplate": ".rating--{value}" },
      "figma": {
        "kind": "component-set",
        "values": { "l": "[Rating-L]", "m": "[Rating-M]", "s": "[Rating-S]", "mini": "[Rating-Mini]" },
        "notes": "In Figma each size is a separate component set, not a variant property."
      },
      "notes": "Icon/gap per size: l = s56/s6 · m = s40/s8 · s = s24/s4 · mini = s16/s2. Gap is per-size, not linear."
    },
    "mode": {
      "title": "Mode",
      "type": "enum",
      "values": ["display", "input"],
      "default": "display",
      "css": { "mechanism": "display = static SSR (ratingStars() macro); input = .rating--input root class + JS buildInputRating() (buttons, aria-pressed)" },
      "figma": { "kind": "none", "notes": "Interactivity is a web/runtime concern; Figma shows fill states only." }
    },
    "value": {
      "title": "Value",
      "type": "number",
      "min": 0,
      "max": 5,
      "step": 0.5,
      "default": 0,
      "constraints": ["Input mode commits whole stars only — half steps are display-only."],
      "css": { "mechanism": "clip-path on .star-svg--filled per Slot: inset(0 0% 0 0) full · inset(0 50% 0 0) half · inset(0 100% 0 0) empty" },
      "figma": { "kind": "variant-property", "property": "Fill", "notes": "Figma exposes representative values (empty / 0.5 stars / 3 stars / 3.5 stars / 4.5 stars / 5 stars); any n or n.5 in 0–5 follows the same rule." }
    },
    "resize": {
      "title": "SlotsContainer resize",
      "type": "enum",
      "values": ["hug", "fill"],
      "default": "hug",
      "css": { "modifier": ".rating--fill" },
      "figma": { "kind": "layout", "notes": "hug = instance hugs content, fixed per-size gap; fill = instance width fills a fixed-width parent, SlotsContainer alignment fill + auto gap (space-between)." },
      "constraints": ["In fill mode the inter-Slot gap is auto ((parentWidth − 5×icon) ÷ 4) — the per-size gap token does not apply."]
    },
    "color": {
      "title": "Fill colour",
      "type": "token",
      "tokens": ["--text-and-icon-primary", "--text-and-icon-accent", "--text-and-icon-brand"],
      "customizable": "any TextAndIcon/* semantic token",
      "default": "--text-and-icon-primary",
      "css": { "mechanism": ".rating--primary / .rating--accent / .rating--brand modifiers; any other TextAndIcon token via instance override on .star-svg--filled" },
      "figma": { "kind": "none", "notes": "Recolour via instance override in Figma." },
      "notes": "Applies to filled stars (the .star-svg--filled overlay). The empty base is --surface-overlay (configurable to any DS colour)."
    },
    "paddingX": {
      "title": "Container padding — horizontal",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-s0",
      "css": { "customProperty": "--rating-padding-x" },
      "figma": { "kind": "layout", "notes": "Autolayout horizontal padding on the rating container." },
      "notes": "Since 3.2.2. Insets the SlotsContainer + Description from the container edge; does not change icon size or gaps."
    },
    "paddingY": {
      "title": "Container padding — vertical",
      "type": "tokenScale",
      "scale": "sp",
      "default": "--sp-s0",
      "css": { "customProperty": "--rating-padding-y" },
      "figma": { "kind": "layout", "notes": "Autolayout vertical padding on the rating container." },
      "notes": "Since 3.2.2."
    },
    "description": {
      "title": "Description",
      "type": "text",
      "default": "",
      "constraints": [
        "Only on sizes l and m — never on s/mini.",
        "Truncates to a maximum of 2 lines.",
        "Announced once: folded into the wrapper aria-label; the visible element is aria-hidden."
      ],
      "css": { "mechanism": "render .rating__description (a TextRow instance); typography l → Main Body, m → Compact Body; colour --text-and-icon-secondary (recolorable to any TextAndIcon/*)" },
      "figma": { "kind": "variant-property", "property": "Subtitle", "notes": "The 3.2 'Subtitle' slot in Figma = web Description." }
    },
    "rtl": {
      "title": "RTL",
      "type": "boolean",
      "default": false,
      "css": { "mechanism": "dir=\"rtl\" on the root (or .rating--rtl); Slot order reverses; half-star clip mirrors to inset(0 0 0 50%)" },
      "figma": { "kind": "variant-property", "property": "RTL", "values": { "false": "Off", "true": "On" } }
    }
  },
  "states": {
    "display": ["standard", "disabled"],
    "input": ["standard", "hover", "press", "active", "disabled"]
  },
  "constraints": [
    "Exactly 5 Slots — the star count is fixed.",
    "Input mode commits whole stars only; half steps render in display mode only.",
    "Description only on sizes l and m.",
    "All spacing/sizing via SP tokens (var(--sp-sN)); raw px/em/rem/hex prohibited.",
    "Empty star = the same fav-filled shape in --surface-overlay — never an outline icon.",
    "Fill colour overrides only with TextAndIcon/* tokens; empty-base overrides with any DS colour token.",
    "Roundness is s0 — the container has no corner radius.",
    "Disabled state = .rating--disabled (both star layers → --text-and-icon-disabled); never opacity.",
    "Input-mode motion only via motion props: --rating-state-fast / --rating-push-item-* plus the catalogued bounceWave recipe's --component-bounce-wave-step/-bounce/-fill (motion-rules.md §6.3) — all zeroed under prefers-reduced-motion; tap replays the fill from Slot 1 up to the tapped Slot as the bounceWave (70ms stagger)."
  ],
  "analytics": {
    "dataDsComponent": "Rating",
    "action": "select",
    "targets": ["star-1", "star-2", "star-3", "star-4", "star-5"],
    "valueAttr": "data-ds-value",
    "notes": "Input mode only; display mode counts for coverage but emits no tap analytics. Target ids are a stable contract — kept as star-N despite the Slot rename."
  },
  "rtl": {
    "supported": true,
    "notes": "Default Off. Slot order reverses visually; fill logic unchanged; the star glyph is symmetric (no flip)."
  }
}
```
