# Was This Helpful

> A question and two thumbs: the chosen one fills with ink, No unfolds an optional note, then the thanks rolls in where the question was. Posts to the form action or to your code.

Forms · feedback, helpful, thumbs, docs, form, rating · HTML, CSS, vanilla JS · no dependencies · updated 2026-11-04
Demo: https://kodza.dev/components/was-this-helpful/
Source on GitHub: https://github.com/kodza-dev/kodza/tree/main/components/was-this-helpful

## Files

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

- [`kodza-was-this-helpful.css`](https://kodza.dev/registry/was-this-helpful/kodza-was-this-helpful.css) · 30.1 KB
- [`kodza-was-this-helpful.js`](https://kodza.dev/registry/was-this-helpful/kodza-was-this-helpful.js) · 12.5 KB
- [`kodza-was-this-helpful.html`](https://kodza.dev/registry/was-this-helpful/kodza-was-this-helpful.html) · 3.1 KB · the markup, to adapt and paste into your page

**Can you run shell commands? Download the CSS and the JS, don't retype them.** 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-was-this-helpful.css https://kodza.dev/registry/was-this-helpful/kodza-was-this-helpful.css
curl -fsSL --create-dirs -o kodza/kodza-was-this-helpful.js https://kodza.dev/registry/was-this-helpful/kodza-was-this-helpful.js
```

Then load the files 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 and the JS from the Code section only if you can't run commands, exactly as they are. (Windows PowerShell: `curl.exe`.)

## API

- Root: `.kodza-helpful`
- Variables (set them on the root element itself: it declares its own defaults):
  - `--kz-helpful-bg: #ffffff`
  - `--kz-helpful-fg: #0a0a0a`
  - `--kz-helpful-muted: rgba(10, 10, 10, 0.6)`
  - `--kz-helpful-line: rgba(10, 10, 10, 0.14)`
  - `--kz-helpful-line-hover: rgba(10, 10, 10, 0.3)`
  - `--kz-helpful-focus: #0a0a0a`
  - `--kz-helpful-halo: rgba(10, 10, 10, 0.04)`
  - `--kz-helpful-error: #0a0a0a` — monochrome by default: an icon and a message, never colour alone
  - `--kz-helpful-ink: #0a0a0a` — the chosen thumb, the tick and the Send button
  - `--kz-helpful-ink-hover: #2a2a2a`
  - `--kz-helpful-on-ink: #ffffff`
  - `--kz-helpful-width: var(--kz-field-width, 380px)`
  - `--kz-helpful-thumb: 40px` — the size of a thumb's round button
  - `--kz-helpful-ease: cubic-bezier(0.22, 1, 0.36, 1)`
  - `--kz-helpful-swap: cubic-bezier(0.65, 0, 0.35, 1)` — the question leaving, the thanks arriving
- Attributes:
  - `data-fail`
  - `data-failed`
  - `data-for` (e.g. `yes`, `no`)
  - `data-more`
  - `data-state` (e.g. `sending`, `done`)
  - `data-value`
- Events: `kodza:helpful` (bubbling, from the root)
- Hooks on the root element: `root.kodzaHelpful` (see the comment at the top of the JS)
- Lifecycle: `kodza:init` / `kodza:destroy` on `document`, with `{ detail: { root } }` to target a container.

## Keyboard

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

- In Tell us more (focus in `.kodza-helpful__text`):
  - `Cmd/Ctrl+Enter`: Sends the note
  - `Escape`: Closes it, focus back on No
- On a thumb (focus in `.kodza-helpful__choice`):
  - `Enter` / `Space`: Yes sends it; No opens Tell us more

## 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-helpful {
    --kz-helpful-bg: #161616;
    --kz-helpful-fg: #f2f2f0;
    --kz-helpful-muted: rgba(242, 242, 240, 0.62);
    --kz-helpful-line: rgba(255, 255, 255, 0.14);
    --kz-helpful-line-hover: rgba(255, 255, 255, 0.3);
    --kz-helpful-focus: #f2f2f0;
    --kz-helpful-halo: rgba(255, 255, 255, 0.06);
    --kz-helpful-error: #f2f2f0;
    --kz-helpful-ink: #f2f2f0;
    --kz-helpful-ink-hover: #d6d6d4;
    --kz-helpful-on-ink: #0a0a0a;
  }
}
```

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

## Use it

### Plain HTML page

- CSS: `kodza/kodza-was-this-helpful.css` next to the page (the Files commands as they are), linked in `<head>`: `<link rel="stylesheet" href="kodza/kodza-was-this-helpful.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.
- JS: `kodza/kodza-was-this-helpful.js`, loaded with `<script src="kodza/kodza-was-this-helpful.js" defer></script>` in `<head>`. Without a shell, paste it in a `<script>` before `</body>` (or in `<head>`: it waits for the page). One copy per page, whatever the number of instances.
- 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). 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); `--kz-radius-pill` sets the pills (`0` squares them).
- Width: a form sets it once for every Kodza field, on the form or on `:root` (`--kz-field-width: 100%` fills the column). Unset, nothing changes.
- 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.
- Content added later (AJAX navigation, a SPA route, a builder preview): start it with `document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root: container } }))`; before removing it, the same call with `'kodza:destroy'` stops it and puts the original markup back.

### Claude artifact (claude.ai)

- Paste the CSS in a `<style>` and the JS in a `<script>` at the end, 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>`, `KODZA-JS` inside the `<script>`, then `sed -e '/^KODZA-CSS$/{r kodza/kodza-was-this-helpful.css' -e 'd' -e '}' -e '/^KODZA-JS$/{r kodza/kodza-was-this-helpful.js' -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).
- 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); `--kz-radius-pill` sets the pills (`0` squares them).
- Width: a form sets it once for every Kodza field, on the form or on `:root` (`--kz-field-width: 100%` fills the column). Unset, nothing changes.
- 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 and JS: the downloaded files go 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-was-this-helpful', $uri . 'kodza-was-this-helpful.css', [], filemtime($dir . 'kodza-was-this-helpful.css'));
    wp_enqueue_script('kodza-was-this-helpful', $uri . 'kodza-was-this-helpful.js', [], filemtime($dir . 'kodza-was-this-helpful.js'), true);
  });
  ```
  Scoped to `kodza-` classes, the CSS never touches the theme. One copy of the JS, whatever the number of blocks.
- 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).
- Theme with AJAX page transitions: fire `kodza:init` on the new content after each swap (and `kodza:destroy` on the old one).
- Performance plugin (WP Rocket, LiteSpeed Cache, Perfmatters…) with **Delay JavaScript execution** turned on: add `kodza` to the scripts it must not delay. Otherwise the component waits for the visitor's first scroll, tap or mouse move, and that first click can be lost.
- Colours: by default, match the theme's palette. After the component's CSS, point these variables at the theme's colours, on the component root, Kodza's value as the fallback:
  `.kodza-helpful { --kz-helpful-fg: var(TEXT, #0a0a0a); --kz-helpful-bg: var(BACKGROUND, #ffffff); --kz-helpful-ink: var(ACCENT, #0a0a0a); --kz-helpful-on-ink: var(BACKGROUND, #ffffff); }`
  TEXT, BACKGROUND and ACCENT (the colour the theme fills its own buttons with) are the theme's own colour variables — block themes (theme.json, like Twenty Twenty-Five): `--wp--preset--color--contrast`, `--wp--preset--color--base`, `--wp--preset--color--contrast` (they fill their buttons with it; their accent-1… colours are decorative); Astra: `--ast-global-color-2`, `--ast-global-color-4`, `--ast-global-color-0`; Kadence: `--global-palette3`, `--global-palette9`, `--global-palette1`; GeneratePress: `--contrast`, `--base-3`, `--accent`; Blocksy: `--theme-palette-color-4`, `--theme-palette-color-8`, `--theme-palette-color-1`. Use only these names, and only if the site really has them (the theme's CSS, or Styles → Colors): the `--wp--preset--color--black`, `--white`, `--vivid-…` that WordPress adds everywhere are not the theme's palette. Another theme, or one without these variables (Hello Elementor, OceanWP): leave the variables alone, it keeps Kodza's monochrome; same when the developer asks for monochrome or the Kodza look. Leave the translucent greys (`rgba(10, 10, 10, …)`) as they are: they suit any light background. On a dark palette, set the other variables from Dark mode too.
- 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 and JS: 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.
- The Elementor editor redraws each widget after the page has loaded: without the script below, the component stays still in the editor preview (the published page works). Add it once per site, in the footer like the component's script (`kodza/elementor-editor.js`, or a code-snippets plugin):

```js
// Kodza in the Elementor editor: the editor redraws each HTML widget after the page has loaded.
// Start Kodza in what it draws, stop it in what it replaces or removes. Once per site.
window.addEventListener('elementor/frontend/init', () => {
  if (!elementorFrontend.isEditMode()) return;
  class KodzaWidget extends elementorModules.frontend.handlers.Base {
    onInit() {
      super.onInit();
      this.kodza = [...this.$element[0].children];
      document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root: this.$element[0] } }));
    }
    onDestroy() {
      super.onDestroy();
      this.kodza.forEach((root) => document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root } })));
    }
  }
  elementorFrontend.elementsHandler.attachHandler('html', KodzaWidget);
});
```

- No child theme: everything can go in the HTML widget instead, `<style>` with the CSS, the HTML, then `<script>` with the JS. It works on the page and in the editor without the script above.

### Shopify

- Files: `assets/kodza-was-this-helpful.css` and `assets/kodza-was-this-helpful.js` (the Files commands with `-o assets/…`).
- In the section: `{{ 'kodza-was-this-helpful.css' | asset_url | stylesheet_tag }}` and `<script src="{{ 'kodza-was-this-helpful.js' | asset_url }}" defer></script>`, then the HTML.
- The theme editor re-renders sections: restart them with
  `document.addEventListener('shopify:section:load', (e) => document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root: e.target } })));`
  and the same with `shopify:section:unload` → `kodza:destroy`.

### React / Next.js

- Files, side by side (e.g. in `components/kodza/`): `WasThisHelpful.tsx` (below), the CSS block under Code saved as `was-this-helpful.css` and the JS block as `was-this-helpful.js`, both as they are. The HTML block is already inside the component: skip it.
- Use it: `import { WasThisHelpful } from '@/components/kodza/WasThisHelpful';` then `<WasThisHelpful />`. Props: `vars` (the `--kz-…` variables listed under API, e.g. `vars={{ '--kz-helpful-bg': … }}`), `onHelpful` (the `kodza:helpful` event), `className`, `style` and the other props of its `<form>` (`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 './was-this-helpful.css'` line there.
- Safe under StrictMode, client-side navigation and prop changes: when a prop changes, the `key` gives React a fresh element, `kodza:destroy` stops the script on the old one and `kodza:init` starts it on the new one.
- Built from your data (tabs, rows, options)? Add what the markup is made of to the `key`, as plain values (ids, text, numbers). What the visitor changes (the open tab, the sort, what they type) is the script's: follow it with `onClick` or `onInput` on the root, and change it from your code as a visitor would (click the tab; set the field's value, then dispatch a bubbling `input` event). Fields stay uncontrolled (`defaultValue`, `name`).

```tsx
'use client';
// Kodza — Was This Helpful, for React. From https://kodza.dev/components/was-this-helpful/
// 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: was-this-helpful.css and was-this-helpful.js, the component's CSS and JS from Kodza, as they are.
// Yours to edit: the markup is plain JSX below.
// The script changes this markup once React has rendered it (it adds layers, splits text, moves
// nodes), so React must not patch it afterwards. When a prop changes, the key gives React a fresh
// element: kodza:destroy stops the script on the old one, kodza:init starts it on the new one.
// data-kodza-manual keeps the script from starting it on its own: the effect below does, once
// React has hydrated the markup (on a page that streams in, the script can load first).
// Built from your data (tabs, rows, options)? Put what the markup is made of in the key, as plain
// values (ids, text, numbers): a change starts the script again on fresh markup. What the visitor
// changes (the open tab, the sort, what they type) is the script's: keep it out of the key, follow
// it with onClick or onInput on the root, and change it from your code as a visitor would (click
// the tab; set the field's value, then dispatch a bubbling input event).
import { useRef, useEffect, type ComponentPropsWithoutRef, type ComponentRef, type CSSProperties } from 'react';
import './was-this-helpful.css';

export type WasThisHelpfulProps = Omit<ComponentPropsWithoutRef<'form'>, "children" | "vars" | "onHelpful"> & {
  /** Settings, as CSS variables on the root */
  vars?: {
    /** default: #ffffff */
    "--kz-helpful-bg"?: string | number;
    /** default: #0a0a0a */
    "--kz-helpful-fg"?: string | number;
    /** default: rgba(10, 10, 10, 0.6) */
    "--kz-helpful-muted"?: string | number;
    /** default: rgba(10, 10, 10, 0.14) */
    "--kz-helpful-line"?: string | number;
    /** default: rgba(10, 10, 10, 0.3) */
    "--kz-helpful-line-hover"?: string | number;
    /** default: #0a0a0a */
    "--kz-helpful-focus"?: string | number;
    /** default: rgba(10, 10, 10, 0.04) */
    "--kz-helpful-halo"?: string | number;
    /** monochrome by default: an icon and a message, never colour alone — default: #0a0a0a */
    "--kz-helpful-error"?: string | number;
    /** the chosen thumb, the tick and the Send button — default: #0a0a0a */
    "--kz-helpful-ink"?: string | number;
    /** default: #2a2a2a */
    "--kz-helpful-ink-hover"?: string | number;
    /** default: #ffffff */
    "--kz-helpful-on-ink"?: string | number;
    /** default: var(--kz-field-width, 380px) */
    "--kz-helpful-width"?: string | number;
    /** the size of a thumb's round button — default: 40px */
    "--kz-helpful-thumb"?: string | number;
    /** default: cubic-bezier(0.22, 1, 0.36, 1) */
    "--kz-helpful-ease"?: string | number;
    /** the question leaving, the thanks arriving — default: cubic-bezier(0.65, 0, 0.35, 1) */
    "--kz-helpful-swap"?: string | number;
  };
  /** "kodza:helpful", fired by the script (bubbles from the root) */
  onHelpful?: (event: CustomEvent) => void;
  className?: string;
};

export function WasThisHelpful({ vars, onHelpful, className, style, ...rest }: WasThisHelpfulProps) {
  const ref = useRef<ComponentRef<'form'>>(null);
  const classes = ["kodza-helpful", className].filter(Boolean).join(' ');
  // Everything the markup is made of: when it changes, React gets a fresh element (see the top)
  const key = JSON.stringify([classes, vars, style, rest]);

  // Starts the script on this element once React has rendered it, stops it when it goes
  useEffect(() => {
    const root = ref.current;
    let live = true;
    // @ts-ignore: a plain script, no types to find
    import('./was-this-helpful.js').then(() => {
      if (live) document.dispatchEvent(new CustomEvent('kodza:init', { detail: { root } }));
    });
    return () => {
      live = false;
      document.dispatchEvent(new CustomEvent('kodza:destroy', { detail: { root } }));
    };
  }, [key]);

  useEffect(() => {
    const root = ref.current;
    if (!root || !onHelpful) return;
    const listener = (event: Event) => onHelpful(event as CustomEvent);
    root.addEventListener("kodza:helpful", listener);
    return () => root.removeEventListener("kodza:helpful", listener);
  }, [onHelpful, key]);

  return (
    <form action="https://example.com/feedback" method="post" key={key} {...rest} className={classes} data-kodza-manual style={{ ...vars, ...style } as CSSProperties} ref={ref}>
      <div className="kodza-helpful__stage">
        <div className="kodza-helpful__ask" role="group">
          <p className="kodza-helpful__label">
            <span className="kodza-helpful__question">Was this page helpful?</span>
            {' '}
            <span className="kodza-helpful__error" aria-live="polite" />
          </p>
          <div className="kodza-helpful__choices">
            <button className="kodza-helpful__choice" type="submit" name="helpful" value="yes" aria-label="Yes">
              <span className="kodza-helpful__face" aria-hidden="true">
                <svg viewBox="0 0 24 24">
                  <path d="M3 11a1 1 0 0 1 1-1h3v10H4a1 1 0 0 1-1-1z" />
                  <path d="M7 10l3.6-6.2a1.6 1.6 0 0 1 2.9 1.2L12.6 10h5.6a2 2 0 0 1 2 2.4l-1.3 6A2 2 0 0 1 17 20H7" />
                </svg>
              </span>
              {' '}
              <span className="kodza-helpful__ink" aria-hidden="true">
                <svg viewBox="0 0 24 24">
                  <path d="M3 11a1 1 0 0 1 1-1h3v10H4a1 1 0 0 1-1-1z" />
                  <path d="M7 10l3.6-6.2a1.6 1.6 0 0 1 2.9 1.2L12.6 10h5.6a2 2 0 0 1 2 2.4l-1.3 6A2 2 0 0 1 17 20H7" />
                </svg>
              </span>
            </button>
            {' '}
            <button className="kodza-helpful__choice kodza-helpful__choice--no" type="submit" name="helpful" value="no" aria-label="No">
              <span className="kodza-helpful__face" aria-hidden="true">
                <svg viewBox="0 0 24 24">
                  <path d="M3 11a1 1 0 0 1 1-1h3v10H4a1 1 0 0 1-1-1z" />
                  <path d="M7 10l3.6-6.2a1.6 1.6 0 0 1 2.9 1.2L12.6 10h5.6a2 2 0 0 1 2 2.4l-1.3 6A2 2 0 0 1 17 20H7" />
                </svg>
              </span>
              {' '}
              <span className="kodza-helpful__ink" aria-hidden="true">
                <svg viewBox="0 0 24 24">
                  <path d="M3 11a1 1 0 0 1 1-1h3v10H4a1 1 0 0 1-1-1z" />
                  <path d="M7 10l3.6-6.2a1.6 1.6 0 0 1 2.9 1.2L12.6 10h5.6a2 2 0 0 1 2 2.4l-1.3 6A2 2 0 0 1 17 20H7" />
                </svg>
              </span>
            </button>
          </div>
        </div>
        <p className="kodza-helpful__done" role="status" tabIndex={-1}>
          <span className="kodza-helpful__tick" aria-hidden="true">
            <svg viewBox="0 0 24 24">
              <path d="M7.5 12.5l3 3 6-6.5" pathLength="1" />
            </svg>
          </span>
          {' '}
          <span className="kodza-helpful__thanks" data-for="yes">Glad it helped.</span>
          {' '}
          <span className="kodza-helpful__thanks" data-for="no">Thanks. We'll fix it.</span>
        </p>
      </div>
      <div className="kodza-helpful__more">
        <div className="kodza-helpful__more-inner">
          <label className="kodza-helpful__box">
            <span className="kodza-helpful__small">Tell us more</span>
            {' '}
            <textarea className="kodza-helpful__text" name="more" rows={2} placeholder="What was missing?" />
          </label>
          <div className="kodza-helpful__row">
            <p className="kodza-helpful__note">
              <span className="kodza-helpful__hint">Optional. We read every one.</span>
              {' '}
              <span className="kodza-helpful__error" aria-live="polite" />
            </p>
            <button className="kodza-helpful__send" type="submit" name="helpful" value="no">
              <span className="kodza-helpful__word">Send</span>
              {' '}
              <span className="kodza-helpful__spinner" aria-hidden="true">
                <svg viewBox="0 0 24 24">
                  <circle cx="12" cy="12" r="8.5" />
                  <circle cx="12" cy="12" r="8.5" pathLength="100" />
                </svg>
              </span>
            </button>
          </div>
        </div>
      </div>
    </form>
  );
}
```

## Code

```html
<!-- action: where the answer goes (your endpoint, a WordPress plugin), or cancel "kodza:helpful" and send it yourself -->
<form class="kodza-helpful" action="https://example.com/feedback" method="post">
  <div class="kodza-helpful__stage">
    <div class="kodza-helpful__ask" role="group">
      <p class="kodza-helpful__label">
        <span class="kodza-helpful__question">Was this page helpful?</span>
        <span class="kodza-helpful__error" aria-live="polite"></span>
      </p>
      <div class="kodza-helpful__choices">
        <button class="kodza-helpful__choice" type="submit" name="helpful" value="yes" aria-label="Yes">
          <span class="kodza-helpful__face" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 11a1 1 0 0 1 1-1h3v10H4a1 1 0 0 1-1-1z" /><path d="M7 10l3.6-6.2a1.6 1.6 0 0 1 2.9 1.2L12.6 10h5.6a2 2 0 0 1 2 2.4l-1.3 6A2 2 0 0 1 17 20H7" /></svg></span>
          <span class="kodza-helpful__ink" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 11a1 1 0 0 1 1-1h3v10H4a1 1 0 0 1-1-1z" /><path d="M7 10l3.6-6.2a1.6 1.6 0 0 1 2.9 1.2L12.6 10h5.6a2 2 0 0 1 2 2.4l-1.3 6A2 2 0 0 1 17 20H7" /></svg></span>
        </button>
        <button class="kodza-helpful__choice kodza-helpful__choice--no" type="submit" name="helpful" value="no" aria-label="No">
          <span class="kodza-helpful__face" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 11a1 1 0 0 1 1-1h3v10H4a1 1 0 0 1-1-1z" /><path d="M7 10l3.6-6.2a1.6 1.6 0 0 1 2.9 1.2L12.6 10h5.6a2 2 0 0 1 2 2.4l-1.3 6A2 2 0 0 1 17 20H7" /></svg></span>
          <span class="kodza-helpful__ink" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 11a1 1 0 0 1 1-1h3v10H4a1 1 0 0 1-1-1z" /><path d="M7 10l3.6-6.2a1.6 1.6 0 0 1 2.9 1.2L12.6 10h5.6a2 2 0 0 1 2 2.4l-1.3 6A2 2 0 0 1 17 20H7" /></svg></span>
        </button>
      </div>
    </div>
    <p class="kodza-helpful__done" role="status" tabindex="-1">
      <span class="kodza-helpful__tick" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M7.5 12.5l3 3 6-6.5" pathLength="1" /></svg></span>
      <span class="kodza-helpful__thanks" data-for="yes">Glad it helped.</span>
      <span class="kodza-helpful__thanks" data-for="no">Thanks. We'll fix it.</span>
    </p>
  </div>
  <div class="kodza-helpful__more">
    <div class="kodza-helpful__more-inner">
      <label class="kodza-helpful__box">
        <span class="kodza-helpful__small">Tell us more</span>
        <textarea class="kodza-helpful__text" name="more" rows="2" placeholder="What was missing?"></textarea>
      </label>
      <div class="kodza-helpful__row">
        <p class="kodza-helpful__note">
          <span class="kodza-helpful__hint">Optional. We read every one.</span>
          <span class="kodza-helpful__error" aria-live="polite"></span>
        </p>
        <button class="kodza-helpful__send" type="submit" name="helpful" value="no">
          <span class="kodza-helpful__word">Send</span>
          <span class="kodza-helpful__spinner" aria-hidden="true"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5" /><circle cx="12" cy="12" r="8.5" pathLength="100" /></svg></span>
        </button>
      </div>
    </div>
  </div>
</form>
```

```css
/* Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license */
/* Kodza — Was This Helpful
   The question and two thumbs. The chosen thumb fills with ink (Yes from the bottom, No from
   the top) and nods. Yes is sent at once; No unfolds a small "Tell us more", sent by its own
   button, so the note stays optional. Then the question rises out and the thanks comes up in
   its place, its tick drawing in. A failed send swaps its line for the error.
   The form posts to its own action, or your code sends it: see component.js.
   Everything is tuned with the --kz-helpful-* variables. Its width follows --kz-field-width,
   shared by every Kodza form field. */
.kodza-helpful {
  --kz-helpful-bg: #ffffff;
  --kz-helpful-fg: #0a0a0a;
  --kz-helpful-muted: rgba(10, 10, 10, 0.6);
  --kz-helpful-line: rgba(10, 10, 10, 0.14);
  --kz-helpful-line-hover: rgba(10, 10, 10, 0.3);
  --kz-helpful-focus: #0a0a0a;
  --kz-helpful-halo: rgba(10, 10, 10, 0.04);
  --kz-helpful-error: #0a0a0a; /* monochrome by default: an icon and a message, never colour alone */
  --kz-helpful-ink: #0a0a0a; /* the chosen thumb, the tick and the Send button */
  --kz-helpful-ink-hover: #2a2a2a;
  --kz-helpful-on-ink: #ffffff;
  --kz-helpful-width: var(--kz-field-width, 380px);
  --kz-helpful-thumb: 40px; /* the size of a thumb's round button */
  --kz-helpful-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --kz-helpful-swap: cubic-bezier(0.65, 0, 0.35, 1); /* the question leaving, the thanks arriving */

  display: grid;
  width: 100%;
  max-width: var(--kz-helpful-width);
  margin: 0;
  color: var(--kz-helpful-fg);
}

/* The question and the thanks share one cell: the height never jumps. The clip lets the focus
   rings show, and hides what rolls past it: the thanks comes up from behind its bottom edge */
.kodza-helpful__stage {
  display: grid;
  clip-path: inset(-8px -8px -6px);
}

.kodza-helpful__ask,
.kodza-helpful__done {
  grid-area: 1 / 1;
  transition:
    transform calc(0.6s * var(--kz-motion-scale, 1)) var(--kz-helpful-swap),
    opacity calc(0.45s * var(--kz-motion-scale, 1)) ease,
    visibility 0s linear 0s;
}

.kodza-helpful__ask {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  min-width: 0;
  transition:
    transform calc(0.6s * var(--kz-motion-scale, 1)) var(--kz-helpful-swap),
    opacity calc(0.4s * var(--kz-motion-scale, 1)) cubic-bezier(0.4, 0, 1, 1),
    visibility 0s linear 0s;
}

/* The question, and the error that takes its line when a Yes could not be sent */
.kodza-helpful__label {
  display: grid;
  min-width: 0;
  margin: 0;
  overflow: hidden;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: -0.01em;
}

.kodza-helpful__question,
.kodza-helpful__hint,
.kodza-helpful__error {
  grid-area: 1 / 1;
  min-width: 0;
  transition:
    opacity calc(0.25s * var(--kz-motion-scale, 1)) ease,
    transform calc(0.4s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

.kodza-helpful__error {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--kz-helpful-error);
  opacity: 0;
  transform: translateY(100%);
}

.kodza-helpful__error::before {
  content: "";
  flex: none;
  width: 14px;
  height: 14px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6.5' fill='none' stroke='black' stroke-width='1.5'/%3E%3Cpath d='M8 4.5v4.2M8 10.9v.1' stroke='black' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='6.5' fill='none' stroke='black' stroke-width='1.5'/%3E%3Cpath d='M8 4.5v4.2M8 10.9v.1' stroke='black' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* The thumbs: a face for the rest, and an ink copy cut by clip-path that fills over it */
.kodza-helpful__choices {
  display: flex;
  flex: none;
  gap: 8px;
}

.kodza-helpful__choice {
  position: relative;
  display: inline-grid;
  place-items: center;
  box-sizing: border-box;
  width: var(--kz-helpful-thumb);
  height: var(--kz-helpful-thumb);
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--kz-helpful-bg);
  box-shadow: inset 0 0 0 1px var(--kz-helpful-line);
  color: inherit;
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition:
    box-shadow calc(0.25s * var(--kz-motion-scale, 1)) ease,
    transform calc(0.2s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

.kodza-helpful__face,
.kodza-helpful__ink {
  display: grid;
  place-items: center;
}

.kodza-helpful__face {
  grid-area: 1 / 1;
}

.kodza-helpful__ink {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--kz-helpful-ink);
  color: var(--kz-helpful-on-ink);
  clip-path: inset(100% 0 0 0);
  pointer-events: none;
  transition: clip-path calc(0.45s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

/* No is the same thumb upside down: its ink pours from the top */
.kodza-helpful__choice--no .kodza-helpful__ink {
  clip-path: inset(0 0 100% 0);
}

.kodza-helpful__choice[aria-pressed="true"] .kodza-helpful__ink {
  clip-path: inset(0 0 0 0);
}

.kodza-helpful svg {
  display: block;
  overflow: visible;
  fill: none;
  stroke: currentColor;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.kodza-helpful__choice svg {
  width: 18px;
  height: 18px;
  stroke-width: 1.6;
  transition: transform calc(0.35s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

.kodza-helpful__choice--no svg {
  rotate: 180deg;
}

@media (hover: hover) {
  .kodza-helpful__choice:hover {
    box-shadow: inset 0 0 0 1px var(--kz-helpful-line-hover);
  }

  .kodza-helpful:not([data-state]) .kodza-helpful__choice:not([aria-pressed="true"]):hover svg {
    transform: rotate(-12deg);
  }
}

.kodza-helpful__choice:active {
  transform: scale(0.94);
}

.kodza-helpful__choice:focus-visible {
  outline: 2px solid var(--kz-helpful-focus);
  outline-offset: 2px;
}

/* Chosen, the thumb nods once */
.kodza-helpful__choice[aria-pressed="true"] svg {
  animation: kodza-helpful-nod calc(0.6s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

@keyframes kodza-helpful-nod {
  0% {
    transform: rotate(-12deg);
  }

  45% {
    transform: rotate(6deg) scale(1.1);
  }

  100% {
    transform: none;
  }
}

/* A Yes on its way: a ring turns around the thumb, once its ink is in */
.kodza-helpful__choice::after {
  content: "";
  position: absolute;
  inset: -4px;
  box-sizing: border-box;
  border: 1.5px solid transparent;
  border-top-color: var(--kz-helpful-ink);
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  transition: opacity calc(0.2s * var(--kz-motion-scale, 1)) ease;
}

.kodza-helpful[data-state="sending"]:not([data-more]) .kodza-helpful__choice[aria-pressed="true"]::after {
  opacity: 1;
  transition-delay: calc(0.4s * var(--kz-motion-scale, 1));
  animation: kodza-helpful-turn calc(0.9s * var(--kz-motion-scale, 1)) linear infinite;
}

.kodza-helpful[data-state="sending"] .kodza-helpful__choice,
.kodza-helpful[data-state="sending"] .kodza-helpful__send {
  cursor: progress;
}

@keyframes kodza-helpful-turn {
  to {
    transform: rotate(360deg);
  }
}

/* The thanks: waits below, comes up as the question rises out */
.kodza-helpful__done {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  margin: 0;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: -0.01em;
  text-wrap: pretty;
  outline: none;
  opacity: 0;
  visibility: hidden;
  transform: translateY(calc(100% + 12px));
  transition-delay: 0s, 0s, calc(0.6s * var(--kz-motion-scale, 1));
}

.kodza-helpful__done:focus-visible {
  outline: 2px solid var(--kz-helpful-focus);
  outline-offset: 4px;
  border-radius: calc(8px * var(--kz-radius-scale, 1));
}

.kodza-helpful__tick {
  display: grid;
  flex: none;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--kz-helpful-ink);
  color: var(--kz-helpful-on-ink);
  transform: scale(0.6);
  transition: transform calc(0.5s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

.kodza-helpful__tick svg {
  width: 16px;
  height: 16px;
  stroke-width: 2.2;
}

.kodza-helpful__tick path {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  transition: stroke-dashoffset calc(0.2s * var(--kz-motion-scale, 1)) ease;
}

.kodza-helpful__thanks:empty {
  display: none;
}

.kodza-helpful[data-state="done"] .kodza-helpful__ask {
  opacity: 0;
  visibility: hidden;
  transform: translateY(calc(-100% - 12px));
  transition-delay: 0s, calc(0.14s * var(--kz-motion-scale, 1)), calc(0.6s * var(--kz-motion-scale, 1));
}

.kodza-helpful[data-state="done"] .kodza-helpful__done {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition-delay: calc(0.06s * var(--kz-motion-scale, 1)), calc(0.22s * var(--kz-motion-scale, 1)), 0s;
}

.kodza-helpful[data-state="done"] .kodza-helpful__tick {
  transform: none;
  transition-delay: calc(0.3s * var(--kz-motion-scale, 1));
}

.kodza-helpful[data-state="done"] .kodza-helpful__tick path {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset calc(0.4s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease) calc(0.45s * var(--kz-motion-scale, 1));
}

/* Tell us more: folded away until No is chosen, then its height eases open. It folds back as
   the thanks arrives, and keeps room for the box's focus halo */
.kodza-helpful__more {
  display: grid;
  grid-template-rows: 0fr;
  contain: inline-size;
  visibility: hidden;
  transition:
    grid-template-rows calc(0.5s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease),
    visibility 0s linear calc(0.5s * var(--kz-motion-scale, 1));
}

.kodza-helpful[data-more]:not([data-state="done"]) .kodza-helpful__more {
  grid-template-rows: 1fr;
  visibility: visible;
  transition-delay: 0s;
}

.kodza-helpful__more-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 */
.kodza-helpful__box {
  position: relative;
  display: block;
  box-sizing: border-box;
  margin: 16px 0 0;
  padding: 30px 16px 12px;
  border-radius: calc(12px * var(--kz-radius-scale, 1));
  background: var(--kz-helpful-bg);
  box-shadow: inset 0 0 0 1px var(--kz-helpful-line);
  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)) var(--kz-helpful-ease);
}

.kodza-helpful[data-more]:not([data-state="done"]) .kodza-helpful__box {
  opacity: 1;
  transform: none;
  transition-delay: 0s, calc(0.08s * var(--kz-motion-scale, 1)), calc(0.08s * var(--kz-motion-scale, 1));
}

@media (hover: hover) {
  .kodza-helpful__box:hover {
    box-shadow: inset 0 0 0 1px var(--kz-helpful-line-hover);
  }
}

.kodza-helpful__box:focus-within {
  box-shadow: inset 0 0 0 1.5px var(--kz-helpful-focus), 0 0 0 4px var(--kz-helpful-halo);
}

.kodza-helpful__small {
  position: absolute;
  top: 11px;
  right: 16px;
  left: 16px;
  overflow: hidden;
  color: var(--kz-helpful-muted);
  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-overflow: ellipsis;
  text-transform: var(--kz-font-label-case, inherit);
  white-space: nowrap;
  pointer-events: none;
}

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

.kodza-helpful__text::placeholder {
  color: var(--kz-helpful-muted);
  opacity: 1;
}

/* Under the box: the hint (or the error) and the Send button */
.kodza-helpful__row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-top: 10px;
  padding-bottom: 4px;
}

.kodza-helpful__note {
  display: grid;
  min-width: 0;
  margin: 0;
  padding: 0 0 0 16px;
  overflow: hidden;
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 13px);
  font-weight: var(--kz-font-label-weight, inherit);
  letter-spacing: var(--kz-font-label-tracking, inherit);
  line-height: var(--kz-font-label-leading, 1.4);
  text-transform: var(--kz-font-label-case, inherit);
}

.kodza-helpful__hint {
  color: var(--kz-helpful-muted);
}

.kodza-helpful__note .kodza-helpful__error {
  font-weight: var(--kz-font-label-weight, 500);
}

/* Send: its word and its ring share one cell, so its width never moves */
.kodza-helpful__send {
  display: inline-grid;
  flex: none;
  place-items: center;
  box-sizing: border-box;
  height: 36px;
  margin: 0;
  padding: 0 16px;
  overflow: hidden;
  border: 0;
  border-radius: var(--kz-radius-pill, 999px);
  background: var(--kz-helpful-ink);
  color: var(--kz-helpful-on-ink);
  font: inherit;
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 14px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, -0.01em);
  text-transform: var(--kz-font-label-case, none);
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition:
    background-color calc(0.2s * var(--kz-motion-scale, 1)) ease,
    transform calc(0.2s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

@media (hover: hover) {
  .kodza-helpful__send:hover {
    background: var(--kz-helpful-ink-hover);
  }
}

.kodza-helpful__send:active {
  transform: scale(0.97);
}

.kodza-helpful__send:focus-visible {
  outline: 2px solid var(--kz-helpful-focus);
  outline-offset: 2px;
}

.kodza-helpful__word,
.kodza-helpful__spinner {
  grid-area: 1 / 1;
  transition: transform calc(0.5s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

.kodza-helpful__word {
  display: block;
}

.kodza-helpful__spinner {
  width: 16px;
  height: 16px;
  transform: translateY(27px);
}

.kodza-helpful__spinner svg {
  width: 100%;
  height: 100%;
  stroke-width: 2.2;
}

.kodza-helpful__spinner circle:first-child {
  opacity: 0.25;
}

.kodza-helpful__spinner circle:nth-child(2) {
  stroke-dasharray: 28 100;
  transform: rotate(-90deg);
  transform-origin: 50% 50%;
}

.kodza-helpful[data-more]:is([data-state="sending"], [data-state="done"]) .kodza-helpful__word {
  transform: translateY(-27px);
}

.kodza-helpful[data-more]:is([data-state="sending"], [data-state="done"]) .kodza-helpful__spinner {
  transform: none;
}

.kodza-helpful[data-more]:is([data-state="sending"], [data-state="done"]) .kodza-helpful__spinner svg {
  animation: kodza-helpful-turn calc(0.9s * var(--kz-motion-scale, 1)) linear infinite;
}

/* A failed send: the error takes the line of what was sent, the question or the hint */
.kodza-helpful[data-failed]:not([data-more]) .kodza-helpful__question,
.kodza-helpful[data-failed][data-more] .kodza-helpful__hint {
  opacity: 0;
  transform: translateY(-100%);
}

.kodza-helpful[data-failed]:not([data-more]) .kodza-helpful__label .kodza-helpful__error,
.kodza-helpful[data-failed][data-more] .kodza-helpful__note .kodza-helpful__error {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .kodza-helpful__choice[aria-pressed="true"] svg,
  .kodza-helpful[data-state="sending"]:not([data-more]) .kodza-helpful__choice[aria-pressed="true"]::after,
  .kodza-helpful[data-more]:is([data-state="sending"], [data-state="done"]) .kodza-helpful__spinner svg {
    animation: none;
  }

  .kodza-helpful__ask,
  .kodza-helpful__done,
  .kodza-helpful__question,
  .kodza-helpful__hint,
  .kodza-helpful__error,
  .kodza-helpful__choice,
  .kodza-helpful__choice svg,
  .kodza-helpful__choice::after,
  .kodza-helpful__ink,
  .kodza-helpful__tick,
  .kodza-helpful__tick path,
  .kodza-helpful__more,
  .kodza-helpful__box,
  .kodza-helpful__word,
  .kodza-helpful__spinner {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }
}

/* Theming example — on a dark background:
.dark-section .kodza-helpful {
  --kz-helpful-bg: #161616;
  --kz-helpful-fg: #f2f2f0;
  --kz-helpful-muted: rgba(242, 242, 240, 0.62);
  --kz-helpful-line: rgba(255, 255, 255, 0.14);
  --kz-helpful-line-hover: rgba(255, 255, 255, 0.3);
  --kz-helpful-focus: #f2f2f0;
  --kz-helpful-halo: rgba(255, 255, 255, 0.06);
  --kz-helpful-error: #f2f2f0;
  --kz-helpful-ink: #f2f2f0;
  --kz-helpful-ink-hover: #d6d6d4;
  --kz-helpful-on-ink: #0a0a0a;
}
*/

/* kodza-guard 481a37d962b6 — 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-helpful__choice):not(#_),
:where(.kodza-helpful__send):not(#_) {
  outline: revert;
  outline-offset: revert;
}

:where(.kodza-helpful__done):not(#_) {
  border-radius: revert;
  outline-offset: revert;
}

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

.kodza-helpful:not(#_) {
  border: revert;
  padding: revert;
  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-helpful__stage:not(#_),
.kodza-helpful__ask:not(#_),
.kodza-helpful__question:not(#_),
.kodza-helpful__error:not(#_),
.kodza-helpful__choices:not(#_),
.kodza-helpful__face:not(#_),
.kodza-helpful__ink:not(#_),
.kodza-helpful__tick:not(#_),
.kodza-helpful__thanks:not(#_),
.kodza-helpful__more:not(#_),
.kodza-helpful__more-inner:not(#_),
.kodza-helpful__small:not(#_),
.kodza-helpful__row:not(#_),
.kodza-helpful__hint:not(#_),
.kodza-helpful__word:not(#_),
.kodza-helpful__spinner:not(#_) {
  box-sizing: revert;
}

.kodza-helpful__label:not(#_) {
  border: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: 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;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-helpful__choice:not(#_) {
  text-decoration: revert;
  list-style: revert;
  float: 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;
  min-height: revert;
}

.kodza-helpful__done:not(#_) {
  border: revert;
  padding: revert;
  text-decoration: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-family: revert;
  font-style: revert;
  text-transform: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-helpful__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-helpful__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;
  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-helpful__note:not(#_) {
  border: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: revert;
  background-color: revert;
  background-image: revert;
  box-shadow: revert;
  outline-offset: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  color: revert;
  font-style: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
}

.kodza-helpful__send:not(#_) {
  text-decoration: revert;
  list-style: revert;
  float: revert;
  position: revert;
  box-shadow: revert;
  appearance: revert;
  max-width: revert;
  max-height: revert;
  text-underline-offset: revert;
  text-align: revert;
  text-indent: revert;
  text-shadow: revert;
  vertical-align: revert;
  min-height: revert;
}

.kodza-helpful__face svg:where(:not([class*="kodza-"])):not(#_),
.kodza-helpful__ink svg:where(:not([class*="kodza-"])):not(#_) {
  border: revert;
  margin: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: 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;
}

.kodza-helpful__tick svg:where(:not([class*="kodza-"])):not(#_),
.kodza-helpful__spinner svg:where(:not([class*="kodza-"])):not(#_) {
  border: revert;
  margin: revert;
  padding: revert;
  border-radius: revert;
  outline: revert;
  text-decoration: revert;
  transition: revert;
  list-style: revert;
  box-sizing: revert;
  float: revert;
  position: 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;
}

.kodza-helpful__error: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-helpful:not(#_) {
  display: grid;
  color: var(--kz-helpful-fg);
}

.kodza-helpful:not(#_) {
  width: min(100%, var(--kz-helpful-width));
}

.kodza-helpful__done:not(#_) {
  transition: transform calc(0.6s * var(--kz-motion-scale, 1)) var(--kz-helpful-swap),
    opacity calc(0.45s * var(--kz-motion-scale, 1)) ease,
    visibility 0s linear 0s;
}

.kodza-helpful__label:not(#_) {
  display: grid;
  margin: 0;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: -0.01em;
}

.kodza-helpful__choice:not(#_) {
  position: relative;
  display: inline-grid;
  place-items: center;
  box-sizing: border-box;
  width: var(--kz-helpful-thumb);
  height: var(--kz-helpful-thumb);
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--kz-helpful-bg);
  box-shadow: inset 0 0 0 1px var(--kz-helpful-line);
  color: inherit;
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: box-shadow calc(0.25s * var(--kz-motion-scale, 1)) ease,
    transform calc(0.2s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

.kodza-helpful svg:not(#_) {
  display: block;
}

.kodza-helpful__choice svg:not(#_) {
  transition: transform calc(0.35s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

@media (hover: hover) {
  .kodza-helpful__choice:hover:not(#_) {
    box-shadow: inset 0 0 0 1px var(--kz-helpful-line-hover);
  }
}

.kodza-helpful__choice:active:not(#_) {
  transform: scale(0.94);
}

.kodza-helpful__choice:focus-visible:not(#_) {
  outline: 2px solid var(--kz-helpful-focus);
  outline-offset: 2px;
}

.kodza-helpful__choice:not(#_)::after {
  content: "";
  position: absolute;
  inset: -4px;
  box-sizing: border-box;
  border: 1.5px solid transparent;
  border-top-color: var(--kz-helpful-ink);
  border-radius: 50%;
  opacity: 0;
  pointer-events: none;
  transition: opacity calc(0.2s * var(--kz-motion-scale, 1)) ease;
}

.kodza-helpful[data-state="sending"]:not([data-more]) .kodza-helpful__choice[aria-pressed="true"]:not(#_)::after {
  opacity: 1;
  transition-delay: calc(0.4s * var(--kz-motion-scale, 1));
  animation: kodza-helpful-turn calc(0.9s * var(--kz-motion-scale, 1)) linear infinite;
}

.kodza-helpful[data-state="sending"] .kodza-helpful__choice:not(#_),
.kodza-helpful[data-state="sending"] .kodza-helpful__send:not(#_) {
  cursor: progress;
}

.kodza-helpful__done:not(#_) {
  display: flex;
  margin: 0;
  font-size: 15px;
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: -0.01em;
  outline: none;
  transition-delay: 0s, 0s, calc(0.6s * var(--kz-motion-scale, 1));
}

.kodza-helpful__done:focus-visible:not(#_) {
  outline: 2px solid var(--kz-helpful-focus);
  outline-offset: 4px;
  border-radius: calc(8px * var(--kz-radius-scale, 1));
}

.kodza-helpful[data-state="done"] .kodza-helpful__done:not(#_) {
  transition-delay: calc(0.06s * var(--kz-motion-scale, 1)), calc(0.22s * var(--kz-motion-scale, 1)), 0s;
}

.kodza-helpful__box:not(#_) {
  position: relative;
  display: block;
  box-sizing: border-box;
  margin: 16px 0 0;
  padding: 30px 16px 12px;
  border-radius: calc(12px * var(--kz-radius-scale, 1));
  background: var(--kz-helpful-bg);
  box-shadow: inset 0 0 0 1px var(--kz-helpful-line);
  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)) var(--kz-helpful-ease);
}

.kodza-helpful[data-more]:not([data-state="done"]) .kodza-helpful__box:not(#_) {
  transition-delay: 0s, calc(0.08s * var(--kz-motion-scale, 1)), calc(0.08s * var(--kz-motion-scale, 1));
}

@media (hover: hover) {
  .kodza-helpful__box:hover:not(#_) {
    box-shadow: inset 0 0 0 1px var(--kz-helpful-line-hover);
  }
}

.kodza-helpful__box:focus-within:not(#_) {
  box-shadow: inset 0 0 0 1.5px var(--kz-helpful-focus), 0 0 0 4px var(--kz-helpful-halo);
}

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

.kodza-helpful__text:not(#_)::placeholder {
  color: var(--kz-helpful-muted);
  opacity: 1;
}

.kodza-helpful__note:not(#_) {
  display: grid;
  margin: 0;
  padding: 0 0 0 16px;
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 13px);
  font-weight: var(--kz-font-label-weight, inherit);
  letter-spacing: var(--kz-font-label-tracking, inherit);
  line-height: var(--kz-font-label-leading, 1.4);
  text-transform: var(--kz-font-label-case, inherit);
}

.kodza-helpful__send:not(#_) {
  display: inline-grid;
  flex: none;
  place-items: center;
  box-sizing: border-box;
  height: 36px;
  margin: 0;
  padding: 0 16px;
  overflow: hidden;
  border: 0;
  border-radius: var(--kz-radius-pill, 999px);
  background: var(--kz-helpful-ink);
  color: var(--kz-helpful-on-ink);
  font: inherit;
  font-family: var(--kz-font-label, inherit);
  font-size: var(--kz-font-label-size, 14px);
  font-weight: var(--kz-font-label-weight, 500);
  line-height: 1;
  letter-spacing: var(--kz-font-label-tracking, -0.01em);
  text-transform: var(--kz-font-label-case, none);
  white-space: nowrap;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color calc(0.2s * var(--kz-motion-scale, 1)) ease,
    transform calc(0.2s * var(--kz-motion-scale, 1)) var(--kz-helpful-ease);
}

@media (hover: hover) {
  .kodza-helpful__send:hover:not(#_) {
    background: var(--kz-helpful-ink-hover);
  }
}

.kodza-helpful__send:active:not(#_) {
  transform: scale(0.97);
}

.kodza-helpful__send:focus-visible:not(#_) {
  outline: 2px solid var(--kz-helpful-focus);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .kodza-helpful[data-state="sending"]:not([data-more]) .kodza-helpful__choice[aria-pressed="true"]:not(#_)::after {
    animation: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .kodza-helpful__done:not(#_),
  .kodza-helpful__choice svg:not(#_),
  .kodza-helpful__box:not(#_) {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }
}

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

```js
// Kodza License: free to use, client work included. Not to resell or redistribute, ported or not: https://kodza.dev/docs/#print-license
// Kodza — Was This Helpful
// Yes is sent as soon as it is pressed. No opens "Tell us more" and waits for its Send button,
// so the note stays optional. Sending it is yours, two ways:
// - the form's action and method (your endpoint, a WordPress plugin): the script posts the
//   fields there, helpful=yes|no and more (the note, with No only). Another site's answer
//   cannot be read, so it counts as sent;
// - or cancel the "kodza:helpful" event (detail: { value, more, data, done, fail }) and send it
//   yourself, then call detail.done(), or detail.fail("Your message") to show it in place of the
//   line that was sent.
// root.kodzaHelpful = { done, fail, reset } does the same from anywhere. The failure message
// defaults to data-fail on the form. The motion is CSS.
(() => {
  const SELECTOR = '.kodza-helpful';
  // The answer waits at least this long (ms): the ink always fills, and a fast answer never
  // flashes the ring
  const MIN_WAIT = 900;
  const FAIL_TEXT = 'Could not send. Try again.';

  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  // The site's pace, --kz-motion-scale (set on :root, read on the instance): every duration of the
  // script times it, and 0 moves nothing, like reduced motion
  const paceOf = (el) => {
    const pace = parseFloat(getComputedStyle(el).getPropertyValue('--kz-motion-scale'));
    return pace >= 0 ? pace : 1;
  };
  let uid = 0;

  // One instance, with its own state: two copies on a page never interfere. Everything it
  // starts is tied to `signal` (listeners get { signal }, observers and timers stop on its
  // 'abort' event), so kodza:destroy stops it all.
  function mount(root, signal) {
    const pace = paceOf(root);
    const still = reduceMotion || !pace; // the still path: no motion asked, by the visitor or the site
    const form = root.closest('form');
    const ask = root.querySelector('.kodza-helpful__ask');
    const question = root.querySelector('.kodza-helpful__question');
    const yes = root.querySelector('.kodza-helpful__choice:not(.kodza-helpful__choice--no)');
    const no = root.querySelector('.kodza-helpful__choice--no');
    const done = root.querySelector('.kodza-helpful__done');
    const text = root.querySelector('.kodza-helpful__text');
    const sendButton = root.querySelector('.kodza-helpful__send');
    const errors = {
      ask: root.querySelector('.kodza-helpful__label .kodza-helpful__error'),
      more: root.querySelector('.kodza-helpful__note .kodza-helpful__error'),
    };
    if (!form || !ask || !yes || !no || !done || !errors.ask) return;
    const minWait = still ? 0 : MIN_WAIT * pace;

    if (question) {
      question.id = `kodza-helpful-${++uid}`;
      ask.setAttribute('aria-labelledby', question.id);
    }
    yes.setAttribute('aria-pressed', 'false');
    no.setAttribute('aria-pressed', 'false');

    // Each thanks is written when it shows: a status that receives text is read out
    const thanks = [...root.querySelectorAll('.kodza-helpful__thanks')];
    const said = new Map(thanks.map((el) => [el, el.textContent.trim()]));
    thanks.forEach((el) => { el.textContent = ''; });

    // Focus is only moved for a visitor on the keyboard: after a click, a ring would show
    let fromKeyboard = false;
    root.addEventListener('keydown', () => { fromKeyboard = true; }, { signal });
    root.addEventListener('pointerdown', () => { fromKeyboard = false; }, { signal });

    const press = (value) => {
      yes.setAttribute('aria-pressed', String(value === 'yes'));
      no.setAttribute('aria-pressed', String(value === 'no'));
      if (value) root.dataset.value = value;
      else delete root.dataset.value;
    };
    const open = (on) => {
      if (on) root.dataset.more = '';
      else delete root.dataset.more;
    };
    const set = (state) => {
      if (state) root.dataset.state = state;
      else delete root.dataset.state;
      if (state === 'sending') root.setAttribute('aria-busy', 'true');
      else root.removeAttribute('aria-busy');
      ask.inert = state === 'done';
      if (text) text.readOnly = Boolean(state);
      thanks.forEach((el) => {
        el.textContent = state === 'done' && el.dataset.for === root.dataset.value ? said.get(el) : '';
      });
    };

    let token = 0; // a reset or a new send voids the answer of the one before
    let timer = 0;
    let started = 0;
    // An answer lands once the ink has filled and the ring has turned long enough
    const later = (id, fn) => {
      clearTimeout(timer);
      timer = setTimeout(() => id === token && root.dataset.state === 'sending' && fn(), Math.max(0, minWait - (performance.now() - started)));
    };
    const finish = () => {
      set('done');
      if (fromKeyboard) done.focus({ preventScroll: true });
    };
    const failWith = (message) => {
      const inMore = root.dataset.more !== undefined && errors.more;
      set();
      if (!inMore) press(null);
      root.dataset.failed = '';
      (inMore ? errors.more : errors.ask).textContent = message || root.dataset.fail || FAIL_TEXT;
    };
    const reset = () => {
      token += 1;
      clearTimeout(timer);
      delete root.dataset.failed;
      errors.ask.textContent = '';
      if (errors.more) errors.more.textContent = '';
      if (text) text.value = '';
      press(null);
      open(false);
      set();
    };

    const send = (value) => {
      const id = ++token;
      started = performance.now();
      const data = new FormData(form);
      data.set('helpful', value);
      if (value === 'yes') data.delete('more');
      const more = value === 'no' && text ? text.value.trim() : '';
      set('sending');
      const ok = () => later(id, finish);
      const ko = (message) => later(id, () => failWith(message));
      const mine = root.dispatchEvent(new CustomEvent('kodza:helpful', { bubbles: true, cancelable: true, detail: { value, more, data, done: ok, fail: ko } }));
      if (!mine) return; // the page sends it, and calls done or fail
      const action = form.getAttribute('action');
      if (!action) {
        console.warn('Kodza Was This Helpful: the form has no action, and nothing handled kodza:helpful.');
        return ko();
      }
      const url = new URL(action, location.href);
      const method = (form.getAttribute('method') || 'get').toUpperCase();
      const fields = new URLSearchParams();
      data.forEach((field, name) => typeof field === 'string' && fields.append(name, field));
      if (method === 'GET') fields.forEach((field, name) => url.searchParams.append(name, field));
      const same = url.origin === location.origin;
      fetch(url, {
        method,
        body: method === 'GET' ? undefined : fields,
        mode: same ? 'same-origin' : 'no-cors',
        credentials: same ? 'same-origin' : 'omit',
        signal,
      }).then((response) => (response.type === 'opaque' || response.ok ? ok() : ko()), () => !signal.aborted && ko());
    };

    form.addEventListener('submit', (event) => {
      const button = event.submitter;
      if (button && !root.contains(button)) return; // another button of a larger form
      event.preventDefault();
      if (!button || root.dataset.state) return;
      delete root.dataset.failed;
      if (button === no) {
        // No only opens the note; pressed again, it lets go
        const on = no.getAttribute('aria-pressed') !== 'true';
        press(on ? 'no' : null);
        open(on);
        if (on && fromKeyboard && text) text.focus({ preventScroll: true });
      } else if (button === yes) {
        press('yes');
        open(false);
        send('yes');
      } else if (button === sendButton) {
        send('no');
      }
    }, { signal });

    if (text) {
      text.addEventListener('input', () => { delete root.dataset.failed; }, { signal });
      text.addEventListener('keydown', (event) => {
        if (event.key === 'Escape' && !root.dataset.state) {
          event.preventDefault();
          press(null);
          open(false);
          no.focus({ preventScroll: true });
        } else if (event.key === 'Enter' && (event.metaKey || event.ctrlKey) && sendButton) {
          event.preventDefault();
          form.requestSubmit(sendButton);
        }
      }, { signal });
    }

    root.kodzaHelpful = {
      done: () => later(token, finish),
      fail: (message) => later(token, () => failWith(message)),
      reset,
    };
    signal.addEventListener('abort', () => {
      token += 1;
      clearTimeout(timer);
      delete root.kodzaHelpful;
    });
  }

  // Kodza lifecycle, the same in every component: `kodza:init` starts the instances inside
  // event.detail.root (the whole page by default), `kodza:destroy` stops them and puts their
  // markup back. For themes and apps that swap content without a page load. Each running root
  // keeps its markup as written under KODZA, shared by every Kodza script on the page: a Kodza
  // component inside another one is saved as written, not half-started, and stopped with it. Two
  // components on one root (a reveal on a stat card) both start, and stop together.
  const KODZA = Symbol.for('kodza');
  const RUNNING = Symbol.for(`kodza ${SELECTOR}`); // a root this component runs on, for every copy of this script
  const instances = new WeakMap();
  const find = (scope = document) => [scope, ...scope.querySelectorAll(SELECTOR)].filter((el) => el.matches?.(SELECTOR));
  const attrsOf = (el) => [...el.attributes].map((a) => [a.name, a.value]);
  const put = (el, saved) => {
    [...el.attributes].forEach((a) => el.removeAttribute(a.name));
    saved.attrs.forEach(([name, value]) => el.setAttribute(name, value));
    el.innerHTML = saved.html;
  };
  // The markup inside a root as written: Kodza components already running in it give back their own
  const written = (root) => {
    const all = [...root.querySelectorAll('*')];
    if (!all.some((el) => el[KODZA])) return root.innerHTML;
    const copy = root.cloneNode(true);
    const twins = copy.querySelectorAll('*');
    let last = null;
    all.forEach((el, i) => {
      if (!el[KODZA] || last?.contains(el)) return;
      last = el;
      put(twins[i], el[KODZA]);
    });
    return copy.innerHTML;
  };
  const init = (scope) => find(scope).forEach((root) => {
    // Already running, maybe from another copy of this script. Marked is-ready with no Kodza component
    // running on it, a root is a copy of a started one (a cloned slide): it is left as it is
    if (root[RUNNING] || (root.classList.contains('is-ready') && !root[KODZA])) return;
    // Rendered by React (data-kodza-manual): React starts it, with an init aimed at that markup once
    // it has hydrated it. Any earlier start would change markup React has not taken over yet
    const owner = root.closest('[data-kodza-manual]');
    if (owner && !(scope && owner.contains(scope))) return;
    const controller = new AbortController();
    // Another Kodza component already running on this root: its markup as written is kept, and the
    // two stop together
    const before = root[KODZA];
    const saved = { html: before ? before.html : written(root), attrs: before ? before.attrs : attrsOf(root), stop: () => destroy(root) };
    if (before) before.above = saved;
    instances.set(root, { controller, saved, before });
    root[KODZA] = saved;
    root[RUNNING] = true;
    mount(root, controller.signal);
    root.classList.add('is-ready');
  });
  const destroy = (scope) => find(scope).forEach((root) => {
    const instance = instances.get(root);
    if (!instance) return;
    instances.delete(root);
    instance.saved.above?.stop(); // a Kodza component started after it on the same root stops first
    // The Kodza components running inside it stop first: once its markup is put back, their own
    // script could not find them any more
    root.querySelectorAll('*').forEach((el) => el[KODZA]?.stop());
    instance.controller.abort();
    delete root[RUNNING];
    // Started on top of another one: that one stops next, and puts the markup back
    if (instance.before) {
      delete instance.before.above;
      root[KODZA] = instance.before;
      return instance.before.stop();
    }
    delete root[KODZA];
    put(root, instance.saved);
  });
  document.addEventListener('kodza:init', (event) => init(event.detail?.root));
  document.addEventListener('kodza:destroy', (event) => destroy(event.detail?.root));
  init();
  // Loaded in <head>, the script runs before the markup exists: once more when it is parsed
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => init(), { once: true });
})();
```

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