# Rating Scale

> A score on real radio buttons, from 0 to 10 or 1 to 5. The picked score fills with ink from the bottom, its end lights up, and an optional follow-up question unfolds and rolls to match the score. Pure CSS.

Forms · rating scale, nps, survey, radio, feedback, form · HTML and CSS only · no dependencies · updated 2026-11-04
Demo: https://kodza.dev/components/rating-scale/
Source on GitHub: https://github.com/kodza-dev/kodza/tree/main/components/rating-scale

## Files

The code below, served as files: byte for byte what the Kodza site copies (the CSS includes its theme guard).

- [`kodza-rating-scale.css`](https://kodza.dev/registry/rating-scale/kodza-rating-scale.css) · 18.9 KB
- [`kodza-rating-scale.html`](https://kodza.dev/registry/rating-scale/kodza-rating-scale.html) · 6.3 KB · the markup, to adapt and paste into your page

**Can you run shell commands? Download the CSS, don't retype it.** Retyping costs thousands of tokens and can drop or change lines of the CSS. Put the files where your stack expects them (Use it says where: change the `-o` paths):

```bash
curl -fsSL --create-dirs -o kodza/kodza-rating-scale.css https://kodza.dev/registry/rating-scale/kodza-rating-scale.css
```

Then load the file from your page as Use it shows, instead of pasting the content. The HTML is the part you adapt (text, links, modifiers): paste it from the Code section. Copy the CSS from the Code section only if you can't run commands, exactly as it is. (Windows PowerShell: `curl.exe`.)

## API

- Root: `.kodza-rating-scale`
- Variables (set them on the root element itself: it declares its own defaults):
  - `--kz-rating-scale-bg: #ffffff`
  - `--kz-rating-scale-fg: #0a0a0a`
  - `--kz-rating-scale-muted: rgba(10, 10, 10, 0.58)`
  - `--kz-rating-scale-border: rgba(10, 10, 10, 0.1)`
  - `--kz-rating-scale-border-hover: rgba(10, 10, 10, 0.28)`
  - `--kz-rating-scale-ink: #0a0a0a`
  - `--kz-rating-scale-on-ink: #ffffff`
  - `--kz-rating-scale-soft: rgba(10, 10, 10, 0.06)` — the halo of the follow-up box
  - `--kz-rating-scale-shadow: rgba(10, 10, 10, 0.18)` — under a lifted score
  - `--kz-rating-scale-cell: 44px` — a score's height, and its width when there is room
  - `--kz-rating-scale-gap: 4px`
  - `--kz-rating-scale-lift: 3px` — how far the score under the pointer rises
- Modifiers: `.kodza-rating-scale--fill`
- Attributes:
  - `data-end` (e.g. `low`, `high`)

## Keyboard

What each key does, by where the focus is. Keep it working when you adapt the HTML: same elements, same classes.

- On a score (focus in `.kodza-rating-scale__input`):
  - `ArrowRight` / `ArrowDown`: Picks the next score, round and round
  - `ArrowLeft` / `ArrowUp`: Picks the previous score, round and round
  - `Space`: Picks the focused score

## Dark mode

The values Kodza uses on a dark background (`#0a0a0a`). For a page that follows the system theme, add them after the component's CSS:

```css
@media (prefers-color-scheme: dark) {
  .kodza-rating-scale {
    --kz-rating-scale-bg: #161616;
    --kz-rating-scale-fg: #f2f2f0;
    --kz-rating-scale-muted: rgba(242, 242, 240, 0.62);
    --kz-rating-scale-border: rgba(255, 255, 255, 0.12);
    --kz-rating-scale-border-hover: rgba(255, 255, 255, 0.3);
    --kz-rating-scale-ink: #f2f2f0;
    --kz-rating-scale-on-ink: #0a0a0a;
    --kz-rating-scale-soft: rgba(255, 255, 255, 0.08);
    --kz-rating-scale-shadow: rgba(0, 0, 0, 0.6);
  }
}
```

A site with its own theme switch puts the same rules under its dark selector instead (`[data-theme="dark"] .kodza-rating-scale { … }`); a single dark section, under that section's class.

## Use it

### Plain HTML page

- CSS: `kodza/kodza-rating-scale.css` next to the page (the Files commands as they are), linked in `<head>`: `<link rel="stylesheet" href="kodza/kodza-rating-scale.css">`. Without a shell, paste it in a `<style>` in `<head>` instead. It only touches `kodza-` classes.
- HTML: where the component goes. Paste it as many times as you need: instances never interfere.
- No JS.
- Restyle with the `--kz-…` variables, set on the component's root element itself (the root declares its own defaults, so a value on an ancestor is ignored), and with the modifiers / attributes listed under API.
- Font: it takes the page's own. A site with its own type sets it once for every Kodza component, on `:root`, never by editing the CSS (unset, nothing changes): `--kz-font-label`, the face of the small labels (meta, captions, counters, hints, footer links, button labels); `--kz-font-label-size`, their size, one for all of them (`11px`…); `--kz-font-label-weight`, their weight; `--kz-font-label-tracking`, their letter-spacing; `--kz-font-label-case`, their case (`uppercase`…; code and units keep theirs); `--kz-font-label-leading`, their line-height (`1.45`…; buttons, pills and keys keep their box); `--kz-font-mono`, the monospace face (code, labels, counters). The Kodza look is Geist: `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap">`.
- Corners: a site with its own shapes sets them once for every Kodza component, on `:root`, never by editing the CSS (unset, nothing changes): `--kz-radius-scale` multiplies every corner (`0` square, `0.5` softer, `1` Kodza).
- Motion: a site sets its pace once for every Kodza component, on `:root`, never by editing the CSS or the JS: `--kz-motion-scale` multiplies every duration (`0.5` twice as fast, `2` twice as slow, `0` no motion at all, loops stopped, like reduced motion). Unset, nothing changes.

### Claude artifact (claude.ai)

- Paste the CSS in a `<style>`, in the artifact itself: it loads stylesheets only from Google Fonts and scripts only from a few CDNs, so no `<link>` or `<script src>` to Kodza.
- Writing the artifact file with a shell? Insert the downloaded files (Files) with a command instead of retyping them: a line `KODZA-CSS` alone inside the `<style>`, then `sed -e '/^KODZA-CSS$/{r kodza/kodza-rating-scale.css' -e 'd' -e '}' page.html > artifact.html`.
- Font: load Geist from Google Fonts (allowed): `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap">`. A site with its own type sets it once for every Kodza component, on `:root`, never by editing the CSS (unset, nothing changes): `--kz-font-label`, the face of the small labels (meta, captions, counters, hints, footer links, button labels); `--kz-font-label-size`, their size, one for all of them (`11px`…); `--kz-font-label-weight`, their weight; `--kz-font-label-tracking`, their letter-spacing; `--kz-font-label-case`, their case (`uppercase`…; code and units keep theirs); `--kz-font-label-leading`, their line-height (`1.45`…; buttons, pills and keys keep their box); `--kz-font-mono`, the monospace face (code, labels, counters).
- Corners: a site with its own shapes sets them once for every Kodza component, on `:root`, never by editing the CSS (unset, nothing changes): `--kz-radius-scale` multiplies every corner (`0` square, `0.5` softer, `1` Kodza).
- Motion: a site sets its pace once for every Kodza component, on `:root`, never by editing the CSS or the JS: `--kz-motion-scale` multiplies every duration (`0.5` twice as fast, `2` twice as slow, `0` no motion at all, loops stopped, like reduced motion). Unset, nothing changes.
- Dark mode: the viewer sets `data-theme="dark"` or `"light"` on `<html>`, or nothing when it follows the system. Write the Dark mode rules twice: inside `@media (prefers-color-scheme: dark)` with `:root:not([data-theme="light"])` in front of each selector, and with `:root[data-theme="dark"]` in front of each selector.
- Forms: there is nowhere to send them. Handle `submit` in your script with `event.preventDefault()` and show the result in the page.

### WordPress

- HTML: a **Custom HTML** block (or a pattern / template part). WordPress keeps this markup as it is only for users allowed to post unfiltered HTML (Administrators; Super Admins on multisite): for anyone else it strips tags such as `<svg>`, `<input>` or `<dialog>`, so add it from the theme (a pattern or template part) instead.
- CSS: the downloaded file goes in the theme, under `wp-content/themes/<your-theme>/kodza/` (the child theme if there is one), enqueued from its functions.php (or a code-snippets plugin):
  ```php
  add_action('wp_enqueue_scripts', function () {
    $dir = get_stylesheet_directory() . '/kodza/';
    $uri = get_stylesheet_directory_uri() . '/kodza/';
    wp_enqueue_style('kodza-rating-scale', $uri . 'kodza-rating-scale.css', [], filemtime($dir . 'kodza-rating-scale.css'));
  });
  ```
  Scoped to `kodza-` classes, the CSS never touches the theme.
- No access to the theme files? The CSS can also go, as it is, in **Appearance → Customize → Additional CSS** (classic theme) or **Styles → Additional CSS** in the Site Editor (block theme).
- No JS.
- Colours: it takes the colour of the text around it (or keeps its own monochrome): nothing to point at the theme.
- WordPress turns straight quotes and `--` into typographic ones in the text of a Custom HTML block (not inside `<code>` or `<pre>`).

#### With Elementor

- HTML: an **HTML** widget (Elementor shows it to administrators only). It keeps the code exactly as pasted, straight quotes included.
- CSS: as above. Elementor's Site Settings → Theme Style styles every button, field, link and image of the page, components included; the theme guard at the end of the CSS keeps the component's buttons, fields and link colours as they are.
- No child theme: the CSS can go in the HTML widget instead, in a `<style>` before the HTML.

### Shopify

- Files: `assets/kodza-rating-scale.css` (the Files commands with `-o assets/…`).
- In the section: `{{ 'kodza-rating-scale.css' | asset_url | stylesheet_tag }}`, then the HTML.

### React / Next.js

- Files, side by side (e.g. in `components/kodza/`): `RatingScale.tsx` (below), the CSS block under Code saved as `rating-scale.css`, both as they are. The HTML block is already inside the component: skip it.
- Use it: `import { RatingScale } from '@/components/kodza/RatingScale';` then `<RatingScale />`. Props: `variant` ("fill", or several in an array), `vars` (the `--kz-…` variables listed under API, e.g. `vars={{ '--kz-rating-scale-bg': … }}`), `className`, `style` and the other props of its `<fieldset>` (`onClick`, `aria-…`).
- Next.js App Router: it is a Client Component already (`'use client'`). Pages Router: Next.js takes global CSS only in `pages/_app`, so move the `import './rating-scale.css'` line there.

```tsx
'use client';
// Kodza — Rating Scale, for React. From https://kodza.dev/components/rating-scale/
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Next to this file: rating-scale.css, the component's CSS from Kodza, as they are.
// Yours to edit: the markup is plain JSX below.
import { useRef, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './rating-scale.css';

type Variant = "fill";

export type RatingScaleProps = Omit<ComponentPropsWithoutRef<'fieldset'>, "children" | "variant" | "vars"> & {
  /** Modifier class(es): .kodza-rating-scale--fill */
  variant?: Variant | Variant[];
  /** Settings, as CSS variables on the root */
  vars?: {
    /** default: #ffffff */
    "--kz-rating-scale-bg"?: string | number;
    /** default: #0a0a0a */
    "--kz-rating-scale-fg"?: string | number;
    /** default: rgba(10, 10, 10, 0.58) */
    "--kz-rating-scale-muted"?: string | number;
    /** default: rgba(10, 10, 10, 0.1) */
    "--kz-rating-scale-border"?: string | number;
    /** default: rgba(10, 10, 10, 0.28) */
    "--kz-rating-scale-border-hover"?: string | number;
    /** default: #0a0a0a */
    "--kz-rating-scale-ink"?: string | number;
    /** default: #ffffff */
    "--kz-rating-scale-on-ink"?: string | number;
    /** the halo of the follow-up box — default: rgba(10, 10, 10, 0.06) */
    "--kz-rating-scale-soft"?: string | number;
    /** under a lifted score — default: rgba(10, 10, 10, 0.18) */
    "--kz-rating-scale-shadow"?: string | number;
    /** a score's height, and its width when there is room — default: 44px */
    "--kz-rating-scale-cell"?: string | number;
    /** default: 4px */
    "--kz-rating-scale-gap"?: string | number;
    /** how far the score under the pointer rises — default: 3px */
    "--kz-rating-scale-lift"?: string | number;
  };
  className?: string;
};

export function RatingScale({ variant, vars, className, style, ...rest }: RatingScaleProps) {
  const ref = useRef<ComponentRef<'fieldset'>>(null);
  const classes = ["kodza-rating-scale", ...([] as string[]).concat(variant ?? []).map((v) => `kodza-rating-scale--${v}`), className].filter(Boolean).join(' ');

  return (
    <fieldset {...rest} className={classes} style={{ ...vars, ...style } as CSSProperties} ref={ref}>
      <legend className="kodza-rating-scale__legend">How likely are you to recommend Kodza to another dev?</legend>
      <div className="kodza-rating-scale__scale">
        <label className="kodza-rating-scale__option" data-end="low">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="0" />
          {' '}
          <span className="kodza-rating-scale__value">0<span className="kodza-rating-scale__sr"> (not likely)</span></span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">0</span>
        </label>
        {' '}
        <label className="kodza-rating-scale__option" data-end="low">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="1" />
          {' '}
          <span className="kodza-rating-scale__value">1</span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">1</span>
        </label>
        {' '}
        <label className="kodza-rating-scale__option" data-end="low">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="2" />
          {' '}
          <span className="kodza-rating-scale__value">2</span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">2</span>
        </label>
        {' '}
        <label className="kodza-rating-scale__option" data-end="low">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="3" />
          {' '}
          <span className="kodza-rating-scale__value">3</span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">3</span>
        </label>
        {' '}
        <label className="kodza-rating-scale__option">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="4" />
          {' '}
          <span className="kodza-rating-scale__value">4</span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">4</span>
        </label>
        {' '}
        <label className="kodza-rating-scale__option">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="5" />
          {' '}
          <span className="kodza-rating-scale__value">5</span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">5</span>
        </label>
        {' '}
        <label className="kodza-rating-scale__option">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="6" />
          {' '}
          <span className="kodza-rating-scale__value">6</span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">6</span>
        </label>
        {' '}
        <label className="kodza-rating-scale__option">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="7" />
          {' '}
          <span className="kodza-rating-scale__value">7</span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">7</span>
        </label>
        {' '}
        <label className="kodza-rating-scale__option" data-end="high">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="8" />
          {' '}
          <span className="kodza-rating-scale__value">8</span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">8</span>
        </label>
        {' '}
        <label className="kodza-rating-scale__option" data-end="high">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="9" />
          {' '}
          <span className="kodza-rating-scale__value">9</span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">9</span>
        </label>
        {' '}
        <label className="kodza-rating-scale__option" data-end="high">
          <input className="kodza-rating-scale__input" type="radio" name="recommend" defaultValue="10" />
          {' '}
          <span className="kodza-rating-scale__value">10<span className="kodza-rating-scale__sr"> (ship it to everyone)</span></span>
          {' '}
          <span className="kodza-rating-scale__ink" aria-hidden="true">10</span>
        </label>
      </div>
      <div className="kodza-rating-scale__ends" aria-hidden="true">
        <span className="kodza-rating-scale__end kodza-rating-scale__end--low">Not likely</span>
        {' '}
        <span className="kodza-rating-scale__end kodza-rating-scale__end--high">Ship it to everyone</span>
      </div>
      <div className="kodza-rating-scale__follow">
        <div className="kodza-rating-scale__follow-inner">
          <label className="kodza-rating-scale__box">
            <span className="kodza-rating-scale__ask">
              <span className="kodza-rating-scale__question kodza-rating-scale__question--low">What broke? Be blunt.</span>
              {' '}
              <span className="kodza-rating-scale__question kodza-rating-scale__question--mid">What would make it a 10?</span>
              {' '}
              <span className="kodza-rating-scale__question kodza-rating-scale__question--high">Nice. What sold you?</span>
            </span>
            {' '}
            <textarea className="kodza-rating-scale__text" name="recommend-why" rows={2} />
          </label>
        </div>
      </div>
    </fieldset>
  );
}
```

## Code

```html
<fieldset class="kodza-rating-scale">
  <legend class="kodza-rating-scale__legend">How likely are you to recommend Kodza to another dev?</legend>
  <div class="kodza-rating-scale__scale">
    <label class="kodza-rating-scale__option" data-end="low">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="0">
      <span class="kodza-rating-scale__value">0<span class="kodza-rating-scale__sr"> (not likely)</span></span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">0</span>
    </label>
    <label class="kodza-rating-scale__option" data-end="low">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="1">
      <span class="kodza-rating-scale__value">1</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">1</span>
    </label>
    <label class="kodza-rating-scale__option" data-end="low">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="2">
      <span class="kodza-rating-scale__value">2</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">2</span>
    </label>
    <label class="kodza-rating-scale__option" data-end="low">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="3">
      <span class="kodza-rating-scale__value">3</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">3</span>
    </label>
    <label class="kodza-rating-scale__option">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="4">
      <span class="kodza-rating-scale__value">4</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">4</span>
    </label>
    <label class="kodza-rating-scale__option">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="5">
      <span class="kodza-rating-scale__value">5</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">5</span>
    </label>
    <label class="kodza-rating-scale__option">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="6">
      <span class="kodza-rating-scale__value">6</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">6</span>
    </label>
    <label class="kodza-rating-scale__option">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="7">
      <span class="kodza-rating-scale__value">7</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">7</span>
    </label>
    <label class="kodza-rating-scale__option" data-end="high">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="8">
      <span class="kodza-rating-scale__value">8</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">8</span>
    </label>
    <label class="kodza-rating-scale__option" data-end="high">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="9">
      <span class="kodza-rating-scale__value">9</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">9</span>
    </label>
    <label class="kodza-rating-scale__option" data-end="high">
      <input class="kodza-rating-scale__input" type="radio" name="recommend" value="10">
      <span class="kodza-rating-scale__value">10<span class="kodza-rating-scale__sr"> (ship it to everyone)</span></span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">10</span>
    </label>
  </div>
  <div class="kodza-rating-scale__ends" aria-hidden="true">
    <span class="kodza-rating-scale__end kodza-rating-scale__end--low">Not likely</span>
    <span class="kodza-rating-scale__end kodza-rating-scale__end--high">Ship it to everyone</span>
  </div>
  <div class="kodza-rating-scale__follow">
    <div class="kodza-rating-scale__follow-inner">
      <label class="kodza-rating-scale__box">
        <span class="kodza-rating-scale__ask">
          <span class="kodza-rating-scale__question kodza-rating-scale__question--low">What broke? Be blunt.</span>
          <span class="kodza-rating-scale__question kodza-rating-scale__question--mid">What would make it a 10?</span>
          <span class="kodza-rating-scale__question kodza-rating-scale__question--high">Nice. What sold you?</span>
        </span>
        <textarea class="kodza-rating-scale__text" name="recommend-why" rows="2"></textarea>
      </label>
    </div>
  </div>
</fieldset>

<fieldset class="kodza-rating-scale kodza-rating-scale--fill">
  <legend class="kodza-rating-scale__legend">How painful was the setup?</legend>
  <div class="kodza-rating-scale__scale">
    <label class="kodza-rating-scale__option" data-end="low">
      <input class="kodza-rating-scale__input" type="radio" name="setup-pain" value="1">
      <span class="kodza-rating-scale__value">1<span class="kodza-rating-scale__sr"> (barely noticed)</span></span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">1</span>
    </label>
    <label class="kodza-rating-scale__option" data-end="low">
      <input class="kodza-rating-scale__input" type="radio" name="setup-pain" value="2" checked>
      <span class="kodza-rating-scale__value">2</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">2</span>
    </label>
    <label class="kodza-rating-scale__option">
      <input class="kodza-rating-scale__input" type="radio" name="setup-pain" value="3">
      <span class="kodza-rating-scale__value">3</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">3</span>
    </label>
    <label class="kodza-rating-scale__option" data-end="high">
      <input class="kodza-rating-scale__input" type="radio" name="setup-pain" value="4">
      <span class="kodza-rating-scale__value">4</span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">4</span>
    </label>
    <label class="kodza-rating-scale__option" data-end="high">
      <input class="kodza-rating-scale__input" type="radio" name="setup-pain" value="5">
      <span class="kodza-rating-scale__value">5<span class="kodza-rating-scale__sr"> (send help)</span></span>
      <span class="kodza-rating-scale__ink" aria-hidden="true">5</span>
    </label>
  </div>
  <div class="kodza-rating-scale__ends" aria-hidden="true">
    <span class="kodza-rating-scale__end kodza-rating-scale__end--low">Barely noticed</span>
    <span class="kodza-rating-scale__end kodza-rating-scale__end--high">Send help</span>
  </div>
</fieldset>
```

```css
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Rating Scale
   A score on real radio buttons: NPS from 0 to 10, or 1 to 5 after a purchase or a ticket. The
   score under the pointer lifts, the picked one fills with ink from the bottom (its number turns
   white exactly at the ink line), and the word of its end lights up. An optional follow-up
   question unfolds after the score, and rolls to match it: a low score, a middle one, a high one.
   Which scores count as an end is in the HTML: data-end="low" or data-end="high" on an option.
   --fill reads as a level: every score up to the picked one fills.
   Pure CSS: the form gets the value as usual, and the keyboard is the radio group's own.
   Everything is tuned with the --kz-rating-scale-* variables. */
.kodza-rating-scale {
  --kz-rating-scale-bg: #ffffff;
  --kz-rating-scale-fg: #0a0a0a;
  --kz-rating-scale-muted: rgba(10, 10, 10, 0.58);
  --kz-rating-scale-border: rgba(10, 10, 10, 0.1);
  --kz-rating-scale-border-hover: rgba(10, 10, 10, 0.28);
  --kz-rating-scale-ink: #0a0a0a;
  --kz-rating-scale-on-ink: #ffffff;
  --kz-rating-scale-soft: rgba(10, 10, 10, 0.06); /* the halo of the follow-up box */
  --kz-rating-scale-shadow: rgba(10, 10, 10, 0.18); /* under a lifted score */
  --kz-rating-scale-cell: 44px; /* a score's height, and its width when there is room */
  --kz-rating-scale-gap: 4px;
  --kz-rating-scale-lift: 3px; /* how far the score under the pointer rises */

  display: block;
  width: fit-content;
  min-width: 0;
  max-width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  color: var(--kz-rating-scale-fg);
}

.kodza-rating-scale__legend {
  margin-bottom: 14px;
  padding: 0;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.4;
  text-wrap: pretty;
}

/* For screen readers only: what the two ends mean */
.kodza-rating-scale__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Grid tracks that may shrink to zero: the scale's smallest width is only its gaps,
   so it fits a narrow column even when a theme sets its own max-width on the root */
.kodza-rating-scale__scale {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, var(--kz-rating-scale-cell));
  gap: var(--kz-rating-scale-gap);
}

.kodza-rating-scale__option {
  position: relative;
  display: grid;
  place-items: center;
  box-sizing: border-box;
  min-width: 0;
  height: var(--kz-rating-scale-cell);
  margin: 0;
  border-radius: calc(10px * var(--kz-radius-scale, 1));
  background: var(--kz-rating-scale-bg);
  box-shadow: inset 0 0 0 1px var(--kz-rating-scale-border);
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  transition:
    transform calc(0.25s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow calc(0.25s * var(--kz-motion-scale, 1)) ease;
}

/* The real radio covers its score: a click anywhere picks it */
.kodza-rating-scale__input {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: inherit;
}

.kodza-rating-scale__value,
.kodza-rating-scale__ink {
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 15px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, inherit);
  font-variant-numeric: tabular-nums;
}

/* The ink rises from the bottom and carries a white copy of the number, cut at the same line */
.kodza-rating-scale__ink {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border-radius: inherit;
  background: var(--kz-rating-scale-ink);
  color: var(--kz-rating-scale-on-ink);
  clip-path: inset(100% 0 0 0);
  pointer-events: none;
  transition: clip-path calc(0.45s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}

.kodza-rating-scale__option:has(.kodza-rating-scale__input:checked) .kodza-rating-scale__ink,
.kodza-rating-scale--fill .kodza-rating-scale__option:has(~ .kodza-rating-scale__option .kodza-rating-scale__input:checked) .kodza-rating-scale__ink {
  clip-path: inset(0 0 0 0);
}

@media (hover: hover) {
  .kodza-rating-scale__option:hover {
    box-shadow:
      inset 0 0 0 1px var(--kz-rating-scale-border-hover),
      0 8px 14px -10px var(--kz-rating-scale-shadow);
    transform: translateY(calc(-1 * var(--kz-rating-scale-lift)));
  }
}

.kodza-rating-scale__option:active {
  transform: none;
}

.kodza-rating-scale__option:has(.kodza-rating-scale__input:focus-visible) {
  outline: 2px solid var(--kz-rating-scale-fg);
  outline-offset: 2px;
}

.kodza-rating-scale__option:has(.kodza-rating-scale__input:disabled) {
  cursor: not-allowed;
  opacity: 0.4;
}

/* The two ends: the one on the picked side lights up, with a dot */
.kodza-rating-scale__ends {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  margin-top: 10px;
  font-family: var(--kz-font-label, var(--kz-font-mono, ui-monospace, "SF Mono", Menlo, monospace));
  font-size: var(--kz-font-label-size, 12px);
  font-weight: var(--kz-font-label-weight, inherit);
  line-height: var(--kz-font-label-leading, 1.4);
  letter-spacing: var(--kz-font-label-tracking, inherit);
  text-transform: var(--kz-font-label-case, inherit);
  color: var(--kz-rating-scale-muted);
}

.kodza-rating-scale__end {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
  transition: color calc(0.25s * var(--kz-motion-scale, 1)) ease;
}

.kodza-rating-scale__end--high {
  flex-direction: row-reverse;
}

.kodza-rating-scale__end::before {
  content: "";
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  transform: scale(0);
  transition: transform calc(0.35s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}

.kodza-rating-scale:has([data-end="low"] .kodza-rating-scale__input:checked) .kodza-rating-scale__end--low,
.kodza-rating-scale:has([data-end="high"] .kodza-rating-scale__input:checked) .kodza-rating-scale__end--high {
  color: var(--kz-rating-scale-fg);
}

.kodza-rating-scale:has([data-end="low"] .kodza-rating-scale__input:checked) .kodza-rating-scale__end--low::before,
.kodza-rating-scale:has([data-end="high"] .kodza-rating-scale__input:checked) .kodza-rating-scale__end--high::before {
  transform: scale(1);
}

/* The follow-up: folded away until a score is picked, then its height eases open. It never
   widens the scale (contain), and keeps room for the box's focus halo */
.kodza-rating-scale__follow {
  display: grid;
  grid-template-rows: 0fr;
  contain: inline-size;
  visibility: hidden;
  transition:
    grid-template-rows calc(0.5s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1),
    visibility 0s linear calc(0.5s * var(--kz-motion-scale, 1));
}

.kodza-rating-scale:has(.kodza-rating-scale__input:checked) .kodza-rating-scale__follow {
  grid-template-rows: 1fr;
  visibility: visible;
  transition-delay: 0s;
}

.kodza-rating-scale__follow-inner {
  min-height: 0;
  margin: 0 -4px;
  padding: 0 4px;
  overflow: hidden;
}

/* The box of the family's Text Field, its label folded up for good: the question */
.kodza-rating-scale__box {
  position: relative;
  display: block;
  box-sizing: border-box;
  margin: 18px 0 4px;
  padding: 30px 16px 12px;
  border-radius: calc(12px * var(--kz-radius-scale, 1));
  background: var(--kz-rating-scale-bg);
  box-shadow: inset 0 0 0 1px var(--kz-rating-scale-border);
  cursor: text;
  opacity: 0;
  transform: translateY(-6px);
  transition:
    box-shadow calc(0.25s * var(--kz-motion-scale, 1)) ease,
    opacity calc(0.3s * var(--kz-motion-scale, 1)) ease,
    transform calc(0.45s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}

.kodza-rating-scale:has(.kodza-rating-scale__input:checked) .kodza-rating-scale__box {
  opacity: 1;
  transform: none;
  transition-delay: 0s, calc(0.08s * var(--kz-motion-scale, 1)), calc(0.08s * var(--kz-motion-scale, 1));
}

.kodza-rating-scale__box:hover {
  box-shadow: inset 0 0 0 1px var(--kz-rating-scale-border-hover);
}

.kodza-rating-scale__box:focus-within {
  box-shadow: inset 0 0 0 1.5px var(--kz-rating-scale-fg), 0 0 0 4px var(--kz-rating-scale-soft);
}

/* The question rolls: a higher score sends the old one up and brings the new one from below,
   a lower score the other way round */
.kodza-rating-scale__ask {
  position: absolute;
  top: 11px;
  right: 16px;
  left: 16px;
  display: grid;
  overflow: hidden;
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 12px);
  font-weight: var(--kz-font-label-weight, inherit);
  line-height: var(--kz-font-label-leading, 1.4);
  letter-spacing: var(--kz-font-label-tracking, inherit);
  text-transform: var(--kz-font-label-case, inherit);
  color: var(--kz-rating-scale-muted);
  pointer-events: none;
}

.kodza-rating-scale__question {
  grid-area: 1 / 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  opacity: 0;
  visibility: hidden;
  transform: translateY(105%);
  transition:
    transform calc(0.42s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1),
    opacity calc(0.25s * var(--kz-motion-scale, 1)) ease,
    visibility 0s linear calc(0.42s * var(--kz-motion-scale, 1));
}

.kodza-rating-scale:has(.kodza-rating-scale__option:not([data-end]) .kodza-rating-scale__input:checked) .kodza-rating-scale__question--low,
.kodza-rating-scale:has([data-end="high"] .kodza-rating-scale__input:checked) .kodza-rating-scale__question--low,
.kodza-rating-scale:has([data-end="high"] .kodza-rating-scale__input:checked) .kodza-rating-scale__question--mid {
  transform: translateY(-105%);
}

.kodza-rating-scale:has([data-end="low"] .kodza-rating-scale__input:checked) .kodza-rating-scale__question--low,
.kodza-rating-scale:has(.kodza-rating-scale__option:not([data-end]) .kodza-rating-scale__input:checked) .kodza-rating-scale__question--mid,
.kodza-rating-scale:has([data-end="high"] .kodza-rating-scale__input:checked) .kodza-rating-scale__question--high {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: 0s;
}

.kodza-rating-scale__text {
  display: block;
  box-sizing: border-box;
  width: 100%;
  min-height: 44px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 15px;
  line-height: 22px;
  outline: none;
  resize: none;
}

@media (prefers-reduced-motion: reduce) {
  .kodza-rating-scale__option,
  .kodza-rating-scale__ink,
  .kodza-rating-scale__end,
  .kodza-rating-scale__end::before,
  .kodza-rating-scale__follow,
  .kodza-rating-scale__box,
  .kodza-rating-scale__question {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }

  .kodza-rating-scale__option:hover {
    transform: none;
  }
}

/* Theming example — on a dark background:
.dark-section .kodza-rating-scale {
  --kz-rating-scale-bg: #161616;
  --kz-rating-scale-fg: #f2f2f0;
  --kz-rating-scale-muted: rgba(242, 242, 240, 0.62);
  --kz-rating-scale-border: rgba(255, 255, 255, 0.12);
  --kz-rating-scale-border-hover: rgba(255, 255, 255, 0.3);
  --kz-rating-scale-ink: #f2f2f0;
  --kz-rating-scale-on-ink: #0a0a0a;
  --kz-rating-scale-soft: rgba(255, 255, 255, 0.08);
  --kz-rating-scale-shadow: rgba(0, 0, 0, 0.6);
}
*/

/* kodza-guard 04d7d9daf84c — generated by Kodza, do not edit.
   Theme guard: keeps a site's own CSS (WordPress themes, Bootstrap, CSS resets…) off this
   component. :not(#_) is always true and weighs as much as an id, so these lines win over
   theme rules made of classes and tags. Customise the component through its --kz-* variables. */

/* What the browser gives, where the component sets it only in some states: at the lowest weight, so its own rules below put it back */

:where(.kodza-rating-scale__option):not(#_) {
  outline: revert;
  outline-offset: revert;
}

/* What the browser gives, on everything the component leaves to it */

.kodza-rating-scale:not(#_) {
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-height: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-rating-scale__legend:not(#_) {
  border: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  margin-top: revert;
  margin-right: revert;
  margin-left: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-style: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-rating-scale__scale:not(#_),
.kodza-rating-scale__value:not(#_),
.kodza-rating-scale__sr:not(#_),
.kodza-rating-scale__ink:not(#_),
.kodza-rating-scale__ends:not(#_),
.kodza-rating-scale__end:not(#_),
.kodza-rating-scale__follow:not(#_),
.kodza-rating-scale__follow-inner:not(#_),
.kodza-rating-scale__ask:not(#_),
.kodza-rating-scale__question:not(#_) {
  box-sizing: revert;
}

.kodza-rating-scale__option:not(#_) {
  border: revert;
  padding: revert;
  text-decoration: revert;
  list-style: revert;
  float: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-rating-scale__input:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
  white-space: revert;
  min-height: revert;
}

.kodza-rating-scale__box:not(#_) {
  border: revert;
  outline: revert;
  text-decoration: revert;
  list-style: revert;
  float: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-size: revert;
  font-weight: revert;
  font-style: revert;
  line-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-rating-scale__text:not(#_) {
  border-radius: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  float: revert;
  position: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  letter-spacing: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
  white-space: revert;
}

.kodza-rating-scale__legend:not(#_):where(:not([hidden])),
.kodza-rating-scale__input:not(#_):where(:not([hidden])) {
  display: revert;
}

.kodza-rating-scale__legend:not(#_):where([hidden]),
.kodza-rating-scale__input:not(#_):where([hidden]) {
  display: none;
}

.kodza-rating-scale__end:not(#_)::before {
  box-sizing: revert;
}

/* The component's own rules, one step stronger: real buttons and fields, what it sets on tags themes style (links, lists, headings, images…), and the root width (a theme may narrow it, never widen it) */

.kodza-rating-scale:not(#_) {
  display: block;
  padding: 0;
  border: 0;
  color: var(--kz-rating-scale-fg);
}

.kodza-rating-scale__legend:not(#_) {
  margin-bottom: 14px;
  padding: 0;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.4;
  text-wrap: pretty;
}

.kodza-rating-scale__option:not(#_) {
  position: relative;
  display: grid;
  box-sizing: border-box;
  margin: 0;
  border-radius: calc(10px * var(--kz-radius-scale, 1));
  background: var(--kz-rating-scale-bg);
  box-shadow: inset 0 0 0 1px var(--kz-rating-scale-border);
  transition: transform calc(0.25s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow calc(0.25s * var(--kz-motion-scale, 1)) ease;
}

.kodza-rating-scale__input:not(#_) {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: inherit;
}

@media (hover: hover) {
  .kodza-rating-scale__option:hover:not(#_) {
    box-shadow: inset 0 0 0 1px var(--kz-rating-scale-border-hover),
        0 8px 14px -10px var(--kz-rating-scale-shadow);
  }
}

.kodza-rating-scale__option:has(.kodza-rating-scale__input:focus-visible):not(#_) {
  outline: 2px solid var(--kz-rating-scale-fg);
  outline-offset: 2px;
}

.kodza-rating-scale__box:not(#_) {
  position: relative;
  display: block;
  box-sizing: border-box;
  margin: 18px 0 4px;
  padding: 30px 16px 12px;
  border-radius: calc(12px * var(--kz-radius-scale, 1));
  background: var(--kz-rating-scale-bg);
  box-shadow: inset 0 0 0 1px var(--kz-rating-scale-border);
  transition: box-shadow calc(0.25s * var(--kz-motion-scale, 1)) ease,
    opacity calc(0.3s * var(--kz-motion-scale, 1)) ease,
    transform calc(0.45s * var(--kz-motion-scale, 1)) cubic-bezier(0.22, 1, 0.36, 1);
}

.kodza-rating-scale:has(.kodza-rating-scale__input:checked) .kodza-rating-scale__box:not(#_) {
  transition-delay: 0s, calc(0.08s * var(--kz-motion-scale, 1)), calc(0.08s * var(--kz-motion-scale, 1));
}

.kodza-rating-scale__box:hover:not(#_) {
  box-shadow: inset 0 0 0 1px var(--kz-rating-scale-border-hover);
}

.kodza-rating-scale__box:focus-within:not(#_) {
  box-shadow: inset 0 0 0 1.5px var(--kz-rating-scale-fg), 0 0 0 4px var(--kz-rating-scale-soft);
}

.kodza-rating-scale__text:not(#_) {
  display: block;
  box-sizing: border-box;
  width: 100%;
  min-height: 44px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 15px;
  line-height: 22px;
  outline: none;
  resize: none;
}

@media (prefers-reduced-motion: reduce) {
  .kodza-rating-scale__option:not(#_),
  .kodza-rating-scale__box:not(#_) {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }
}
```

Free to use, from Kodza (https://kodza.dev).
