Zum Inhalt
← Mikroanimationen

Animierte Checkbox und Radio

Native Checkboxen und Radios mit eigener Optik: Häkchen zeichnet sich, Maske skaliert oder Quadrat füllt sich, die Semantik bleibt.

Native Checkboxen und Radios bekommen per appearance: none eine eigene Optik und behalten ihre Semantik. Beim Anwählen skaliert das Icon von 0 auf 1, zeichnet sich per stroke-dashoffset ein oder ein Füllquadrat wächst aus der Mitte. Der Radio-Punkt wächst mit. Der Zustand indeterminate zeigt einen Strich. Das Icon liegt als SVG-Maske auf currentColor und funktioniert so auch im forced-colors-Modus.

Die kleine Bewegung bestätigt die Eingabe sofort und lässt das Formular sorgfältig wirken. Weil das native Input bleibt, gehen Tastaturbedienung, Formularversand und Screenreader-Ansage nicht verloren.

Wann einsetzen

Formulare, Filter, Einwilligungen, Auswahllisten (z. B. Bilder für Abzüge) und To-do-Listen.

Wann nicht

Keine großen Effekte wie Konfetti bei Pflicht-Checkboxen (AGB, Datenschutz). Keine langen Federn in dichten Listen, in denen viele Kästen schnell nacheinander angeklickt werden.

Varianten

  • Maske skaliert: Icon als SVG-Maske, scale 0 → 1 in 150 ms ease (Chrome-Leitfaden)
  • Häkchen zeichnet sich: stroke-dashoffset in 250–350 ms
  • Füllquadrat wächst: scale 0 → 1 in 300 ms cubic-bezier(0.19, 1, 0.22, 1), Rahmenfarbe 300 ms ease-out (Fantasy)
  • Label wird bei To-dos durchgestrichen
  • Komponenten aus Base UI oder Radix mit Motion

Umsetzung

.cb { appearance: none; display: inline-grid; place-content: center; inline-size: 1.25em; aspect-ratio: 1; border: 2px solid currentColor } .cb::before { content: ''; inline-size: .7em; aspect-ratio: 1; background: currentColor; mask: var(--icon) no-repeat center / contain; scale: 0 } @media (prefers-reduced-motion: no-preference) { .cb::before { transition: scale 150ms ease } } .cb:checked { --icon: var(--icon-check) } .cb:indeterminate { --icon: var(--icon-dash) } .cb:is(:checked, :indeterminate)::before { scale: 1 } .cb:focus-visible { outline: 2px solid currentColor; outline-offset: 2px }. Für die Zeichnen-Variante liegt ein SVG-Pfad mit pathLength="1" über dem Input; :has(.cb:checked) setzt stroke-dashoffset auf 0. indeterminate setzt man per JS (input.indeterminate = true).

Robust umsetzen

Telefon. Das Label ist die Trefferfläche, mindestens 44 px hoch. Auf Touch den Kasten etwas größer zeichnen (z. B. 22 px).

Ohne Bewegung. Das Icon erscheint ohne Skalierung und ohne Zeichnen, höchstens mit kurzem Einblenden (100 ms).

Barrierefrei. Das native Input bleibt im DOM und fokussierbar. indeterminate per JS-Property setzen. Im forced-colors-Modus Icon und Rahmen in CanvasText zeichnen. Den Fokusring nie entfernen.

Code

<!DOCTYPE html>
<!--
  Animierte Checkbox und Radio
  Fundus · Mikroanimationen · https://cecodesign.de/lab/fundus/mikroanimationen/checkbox-radio-animiert/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Animierte Checkbox und Radio · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-display: swap; }
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@font-face { font-family: 'Fragment Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fragment-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }

/* Rezept „Bild zuerst“: neutrale Fläche, kleine Typo, das Bild bekommt die größte Fläche */
:root {
  --grund: #f4f4f2; --tinte: #141414; --leise: #666666; --linie: rgba(20, 20, 20, .12); --platz: #e6e6e6;
  --rand: rgba(20, 20, 20, .42);
  /* Icons als SVG-Maske: Farbe kommt aus currentColor */
  --icon-haken: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2 6.4 4.8 9 10 3.2' fill='none' stroke='%23000' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --icon-strich: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6h7' stroke='%23000' stroke-width='1.9' stroke-linecap='round'/%3E%3C/svg%3E");
  --ease: cubic-bezier(0.25, 0.1, 0.25, 1);       /* ease, Chrome-Leitfaden, 150 ms */
  --expo: cubic-bezier(0.19, 1, 0.22, 1);         /* Fantasy, 300 ms */
  --ease-out: cubic-bezier(0, 0, 0.58, 1);        /* Rahmenfarbe bei Fantasy, 300 ms */
  --io: cubic-bezier(0.455, 0.03, 0.515, 0.955);  /* Rezept: easeInOutQuad, nur für die Oberfläche */
  --box: 18px;
}
/* Variante bestimmt Dauer und Kurve des Zustandswechsels */
[data-variante="maske"]    { --d: 150ms; --k: var(--ease); }
[data-variante="zeichnen"] { --d: 300ms; --k: var(--expo); }
[data-variante="fuellen"]  { --d: 300ms; --k: var(--expo); }

* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; font-size: 15px; line-height: 1.5; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
fieldset { border: 0; min-width: 0; }
.mono { font: 400 11.5px/1.35 'Fragment Mono', ui-monospace, monospace; letter-spacing: .02em; color: var(--leise); font-variant-numeric: tabular-nums; }
.versal { text-transform: uppercase; letter-spacing: .08em; }

/* ---------- Kopf: Name links, Navigation klein in der Ecke ---------- */
.kopf { display: flex; align-items: baseline; justify-content: space-between; gap: 24px; padding: 22px clamp(16px, 3.4vw, 40px) 0; }
.marke { font: 400 22px/1 'Instrument Serif', serif; letter-spacing: -.01em; color: inherit; text-decoration: none; }
.marke small { font: 400 11.5px/1 'Fragment Mono', monospace; color: var(--leise); letter-spacing: .02em; margin-left: 10px; }
.nav { display: flex; gap: 22px; list-style: none; font-size: 14px; }
.nav a { color: inherit; text-decoration: none; }
.nav a[aria-current] { text-decoration: underline; text-underline-offset: .3em; text-decoration-thickness: 1px; }

/* ---------- Bühne: Kontaktbogen links, Bestellung rechts ---------- */
/* Kontaktbogen so breit, dass zwei Reihen in die Höhe passen */
.buehne { --bogen: calc((100svh - 320px) * 1.2 + 44px); display: grid; grid-template-columns: minmax(0, max(560px, var(--bogen))) minmax(272px, 1fr); gap: clamp(28px, 5vw, 80px); align-items: start; padding: clamp(22px, 3vw, 36px) clamp(16px, 3.4vw, 40px) 28px; }
.bogen-kopf { display: flex; align-items: end; justify-content: space-between; gap: 16px; margin-bottom: 18px; }
.bogen-kopf h1 { font: 400 clamp(30px, 3.4vw, 44px)/1.05 'Instrument Serif', serif; letter-spacing: -.01em; }
.bogen-kopf h1 + .mono { margin-top: 6px; }

.raster { list-style: none; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px 22px; }
.bild { display: block; cursor: pointer; -webkit-tap-highlight-color: transparent; }
.rahmen { display: block; aspect-ratio: 4 / 5; background: var(--platz); outline: 1px solid transparent; outline-offset: 4px; transition: outline-color var(--d) var(--io); }
.rahmen svg { display: block; width: 100%; height: 100%; }
.bild:has(.cb:checked) .rahmen { outline-color: var(--tinte); }
.unter { display: flex; align-items: center; gap: 10px; min-height: 44px; padding-top: 6px; }
.unter .nr { color: var(--tinte); }
.unter .titel { font-size: 13.5px; line-height: 1.25; }
.unter .titel em { font-style: normal; color: var(--leise); font: 400 11.5px/1 'Fragment Mono', monospace; margin-left: 4px; }

.alle { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; cursor: pointer; font-size: 13.5px; white-space: nowrap; }

/* ---------- Das Muster: native Inputs mit appearance:none ---------- */
.kasten { position: relative; display: inline-grid; flex: none; width: var(--box); height: var(--box); }
.kasten > * { grid-area: 1 / 1; }
.cb, .rb {
  appearance: none; -webkit-appearance: none; margin: 0; width: var(--box); height: var(--box);
  display: grid; place-content: center; border: 1.5px solid var(--rand); background: transparent; color: var(--tinte);
  cursor: pointer; transition: border-color var(--d) var(--k), background-color var(--d) var(--k);
}
.cb { border-radius: 2px; }
.rb { border-radius: 50%; }
.cb::before { content: ''; width: 12px; height: 12px; background: currentColor; -webkit-mask: var(--icon, var(--icon-haken)) center / contain no-repeat; mask: var(--icon, var(--icon-haken)) center / contain no-repeat; scale: 0; transition: scale var(--d) var(--k); }
.cb:indeterminate { --icon: var(--icon-strich); }
.rb::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: currentColor; scale: 0; transition: scale var(--d) var(--k); }
.cb:is(:checked, :indeterminate), .rb:checked { border-color: var(--tinte); }
.cb:is(:checked, :indeterminate)::before, .rb:checked::before { scale: 1; }
:is(.bild, .alle, .zeile):hover .cb:not(:checked, :indeterminate), .zeile:hover .rb:not(:checked) { border-color: var(--tinte); }
.cb:focus-visible, .rb:focus-visible { outline: 2px solid var(--tinte); outline-offset: 2px; }
.zeichen { width: 100%; height: 100%; pointer-events: none; overflow: visible; display: none; }
.zeichen path { fill: none; stroke: var(--tinte); stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 1 1.1; stroke-dashoffset: 1.05; transition: stroke-dashoffset var(--d) var(--k); }

/* Variante 1 · Maske: Kasten füllt sich, Icon skaliert 0 → 1 (150 ms ease) */
[data-variante="maske"] .cb:is(:checked, :indeterminate) { background: var(--tinte); color: var(--grund); }

/* Variante 2 · Zeichnen: Häkchen zeichnet sich per stroke-dashoffset (300 ms) */
[data-variante="zeichnen"] .cb::before { display: none; }
[data-variante="zeichnen"] .zeichen { display: block; }
[data-variante="zeichnen"] .kasten:has(.cb:checked:not(:indeterminate)) .haken,
[data-variante="zeichnen"] .kasten:has(.cb:indeterminate) .strich { stroke-dashoffset: 0; }

/* Variante 3 · Füllen: Quadrat 10/18 wächst aus der Mitte (Fantasy: 1,6 rem in 2,7 rem) */
[data-variante="fuellen"] .cb, [data-variante="fuellen"] .rb { transition: border-color var(--d) var(--ease-out); }
[data-variante="fuellen"] .cb::before { -webkit-mask: none; mask: none; width: 10px; height: 10px; border-radius: 1px; }
[data-variante="fuellen"] .cb:indeterminate::before { height: 2px; }
[data-variante="fuellen"] .rb::before { width: 10px; height: 10px; }

/* ---------- Bestellung ---------- */
.abzug-rahmen { container-type: inline-size; }
.abzug { display: grid; gap: 20px; padding-top: 6px; }
/* breite Spalte: Papier und Format nebeneinander, Summe neben den Extras */
@container (min-width: 500px) {
  .abzug { grid-template-columns: 1fr 1fr; gap: 26px 36px; align-items: start; }
  .abzug-kopf { grid-column: 1 / -1; }
  .summe { align-self: end; }
}
.abzug-kopf h2 { font: 400 22px/1.1 'Instrument Serif', serif; letter-spacing: -.01em; }
.abzug-kopf p { font-size: 13px; line-height: 1.45; color: var(--leise); margin-top: 6px; max-width: 34ch; }
.vorschau .abzug-kopf { display: none; }
.abzug legend { margin-bottom: 4px; }
.zeile { display: flex; align-items: center; gap: 12px; min-height: 44px; border-bottom: 1px solid var(--linie); cursor: pointer; }
.zeile:first-of-type { border-top: 1px solid var(--linie); }
.zeile .was { flex: 1; font-size: 14px; line-height: 1.25; }
.zeile .was small { display: block; font-size: 12px; color: var(--leise); }
.zeile .preis { font: 400 11.5px/1 'Fragment Mono', monospace; color: var(--leise); font-variant-numeric: tabular-nums; }
.summe { display: grid; grid-template-columns: 1fr auto; align-items: end; gap: 4px 12px; padding-top: 4px; }
.summe strong { font: 400 30px/1 'Instrument Serif', serif; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.senden { grid-column: 1 / -1; margin-top: 12px; min-height: 44px; border-radius: 0; background: var(--tinte); color: var(--grund); font-size: 14px; transition: opacity 200ms var(--io); }
.senden:disabled { opacity: .35; cursor: default; }
.senden:focus-visible { outline: 2px solid var(--tinte); outline-offset: 3px; }

/* ---------- Steuerung (dezent) ---------- */
.steuer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 20px; padding: 12px clamp(16px, 3.4vw, 40px) 16px; border-top: 1px solid var(--linie); }
.wahl { display: flex; gap: 2px; flex-wrap: wrap; }
.wahl button { font: 400 11px/1 'Fragment Mono', monospace; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); padding: 8px 10px; border-radius: 99px; transition: background-color 200ms var(--io), color 200ms var(--io); }
.wahl button[aria-pressed="true"] { color: var(--tinte); background: var(--platz); }
.wahl button:focus-visible { outline: 2px solid var(--tinte); outline-offset: 2px; }
.werte { text-align: right; max-width: 70ch; }

/* ---------- Vorführung: gedachter Zeiger ---------- */
.zeiger { position: fixed; left: 0; top: 0; z-index: 9; width: 20px; height: 20px; pointer-events: none; display: none; transition: transform .6s cubic-bezier(0.16, 1, 0.3, 1), scale 150ms var(--io); transform-origin: 2px 2px; }
.zeiger.an { scale: .82; }
.vorfuehrung .zeiger { display: block; }

.vorschau body { overflow: hidden; }
.vorschau .steuer { display: none; }
.vorschau .buehne { padding-top: 26px; grid-template-columns: minmax(0, 1fr) 272px; }
:root.vorschau { --box: 21px; }
.vorschau .cb::before { width: 13px; height: 13px; }
.vorschau [data-variante="fuellen"] :is(.cb, .rb)::before { width: 11px; height: 11px; }
.vorschau .rb::before { width: 9px; height: 9px; }

/* forced-colors: Icon in Systemfarbe, Fokus bleibt sichtbar */
@media (forced-colors: active) {
  .cb::before, .rb::before { background: CanvasText; }
  [data-variante="maske"] .cb:is(:checked, :indeterminate) { background: Canvas; color: CanvasText; }
  .zeichen path { stroke: CanvasText; }
  .cb, .rb { border-color: CanvasText; }
}

/* ---------- Touch: größere Kästen, ganze Zeile tippbar ---------- */
@media (hover: none), (pointer: coarse) {
  :root { --box: 22px; }
  .cb::before { width: 14px; height: 14px; }
  .rb::before { width: 10px; height: 10px; }
  [data-variante="fuellen"] .cb::before, [data-variante="fuellen"] .rb::before { width: 12px; height: 12px; }
  .zeile { min-height: 52px; }
  .wahl button { padding: 11px 12px; }
}

/* ---------- Reduzierte Bewegung: keine Skalierung, kein Zeichnen, nur kurzes Einblenden ---------- */
.reduziert * { transition-duration: 0s !important; }
.reduziert .cb::before, .reduziert .rb::before { scale: 1; opacity: 0; transition: opacity 100ms linear !important; }
.reduziert .cb:is(:checked, :indeterminate)::before, .reduziert .rb:checked::before { opacity: 1; }
.reduziert .zeichen path { transition: opacity 100ms linear !important; opacity: 0; }
.reduziert [data-variante="zeichnen"] .kasten:has(.cb:checked:not(:indeterminate)) .haken,
.reduziert [data-variante="zeichnen"] .kasten:has(.cb:indeterminate) .strich { opacity: 1; }

@media (max-width: 860px) {
  .buehne { grid-template-columns: 1fr; }
  .werte { text-align: left; }
}
@media (max-width: 620px) {
  .nav li:not(:last-child) { display: none; }
  .marke small { display: none; }
  .raster { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 14px; }
  .unter .titel em { display: none; }
  .bogen-kopf { flex-wrap: wrap; }
}
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body data-variante="zeichnen">
<header class="kopf">
  <a class="marke" href="#">Lena Kalchberg<small>Fotografie, Graz</small></a>
  <ul class="nav">
    <li><a href="#" aria-current="page">Serien</a></li>
    <li><a href="#">Aufträge</a></li>
    <li><a href="#">Über</a></li>
    <li><a href="#">Kontakt</a></li>
  </ul>
</header>

<main class="buehne">
  <section class="bogen" aria-labelledby="serie">
    <div class="bogen-kopf">
      <div>
        <h1 id="serie">Mur, früh</h1>
        <p class="mono">Serie 14 · Graz, Oktober 2026 · Auswahl für Pirker &amp; Partner</p>
      </div>
      <label class="alle">
        <span class="kasten"><input class="cb" type="checkbox" data-alle aria-describedby="zaehler"><svg class="zeichen" viewBox="0 0 18 18" aria-hidden="true"><path class="haken" pathLength="1" d="M4.6 9.4 7.7 12.3 13.4 5.9"/><path class="strich" pathLength="1" d="M5 9h8"/></svg></span>
        Alle <span class="mono" id="zaehler" data-zaehler aria-live="polite">2 / 6</span>
      </label>
    </div>

    <ul class="raster" data-raster>
      <li><label class="bild">
        <span class="rahmen"><svg viewBox="0 0 400 500" role="img" aria-label="Kunsthaus in der Morgendämmerung, beleuchtete Glasfront">
          <defs>
            <linearGradient id="k-himmel" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#17223a"/><stop offset=".5" stop-color="#3f4d72"/><stop offset=".78" stop-color="#b98a86"/><stop offset="1" stop-color="#e3b18c"/></linearGradient>
            <linearGradient id="k-haut" x1="0" y1="0" x2=".3" y2="1"><stop offset="0" stop-color="#6f9fd0"/><stop offset=".55" stop-color="#335f92"/><stop offset="1" stop-color="#1d3658"/></linearGradient>
            <linearGradient id="k-spiegel" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f2c98c" stop-opacity=".55"/><stop offset="1" stop-color="#f2c98c" stop-opacity="0"/></linearGradient>
          </defs>
          <rect width="400" height="500" fill="url(#k-himmel)"/>
          <path d="M0 330h46v-26h30v26h18v-40h26v40h270v30H0z" fill="#232838" opacity=".8"/>
          <path d="M28 372C26 268 120 222 214 224c104 2 162 62 160 148Z" fill="url(#k-haut)"/>
          <path d="M70 300c30-46 86-64 150-60" stroke="#a9cdf0" stroke-width="3" fill="none" opacity=".45" stroke-linecap="round"/>
          <g fill="#4f80b6"><rect x="150" y="196" width="18" height="40" rx="9" transform="rotate(-18 159 216)"/><rect x="214" y="190" width="18" height="44" rx="9" transform="rotate(8 223 212)"/><rect x="276" y="206" width="18" height="40" rx="9" transform="rotate(28 285 226)"/></g>
          <g fill="#c5e0fa" opacity=".8"><ellipse cx="150" cy="198" rx="7" ry="4" transform="rotate(-18 150 198)"/><ellipse cx="226" cy="191" rx="7" ry="4" transform="rotate(8 226 191)"/><ellipse cx="292" cy="208" rx="7" ry="4" transform="rotate(28 292 208)"/></g>
          <rect x="40" y="350" width="332" height="30" fill="#f4cf92"/>
          <g stroke="#7a5d3a" stroke-width="2" opacity=".55"><path d="M80 350v30M120 350v30M160 350v30M200 350v30M240 350v30M280 350v30M320 350v30"/></g>
          <rect y="380" width="400" height="120" fill="#151820"/>
          <rect x="40" y="384" width="332" height="70" fill="url(#k-spiegel)"/>
          <path d="M0 470h400" stroke="#2a2f3b" stroke-width="2"/>
        </svg></span>
        <span class="unter"><span class="kasten"><input class="cb" type="checkbox" name="bild" value="01"><svg class="zeichen" viewBox="0 0 18 18" aria-hidden="true"><path class="haken" pathLength="1" d="M4.6 9.4 7.7 12.3 13.4 5.9"/></svg></span><span class="mono nr">01</span><span class="titel">Kunsthaus<em>06:31</em></span></span>
      </label></li>

      <li><label class="bild">
        <span class="rahmen"><svg viewBox="0 0 400 500" role="img" aria-label="Uhrturm auf dem Schlossberg vor Morgenhimmel">
          <defs>
            <linearGradient id="u-himmel" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#9fb4c6"/><stop offset=".55" stop-color="#e5c6ad"/><stop offset="1" stop-color="#f5d9b7"/></linearGradient>
            <radialGradient id="u-sonne" cx=".78" cy=".7" r=".45"><stop offset="0" stop-color="#fff3df"/><stop offset="1" stop-color="#fff3df" stop-opacity="0"/></radialGradient>
          </defs>
          <rect width="400" height="500" fill="url(#u-himmel)"/>
          <rect width="400" height="500" fill="url(#u-sonne)"/>
          <path d="M0 372q90-30 190-12t210-14v154H0z" fill="#8c958f" opacity=".55"/>
          <path d="M0 500V404q56-42 116-30 44-40 104-24 74-36 180 26v124z" fill="#2c362e"/>
          <g fill="#232c25"><circle cx="40" cy="398" r="22"/><circle cx="82" cy="384" r="26"/><circle cx="262" cy="350" r="24"/><circle cx="306" cy="362" r="28"/><circle cx="352" cy="382" r="22"/></g>
          <rect x="170" y="300" width="60" height="60" fill="#5b5148"/>
          <rect x="178" y="212" width="44" height="100" fill="#e4d7c2"/>
          <rect x="200" y="212" width="22" height="100" fill="#cdbea6"/>
          <rect x="166" y="198" width="68" height="16" fill="#4a392d"/>
          <g stroke="#2e231c" stroke-width="2"><path d="M172 198v16M184 198v16M196 198v16M208 198v16M220 198v16M230 198v16"/></g>
          <rect x="176" y="152" width="48" height="46" fill="#ebdfcb"/>
          <rect x="200" y="152" width="24" height="46" fill="#d6c8b0"/>
          <circle cx="200" cy="175" r="13" fill="#f8f2e6" stroke="#3a2f28" stroke-width="2"/>
          <path d="M200 175v-8M200 175l6 3" stroke="#3a2f28" stroke-width="2" stroke-linecap="round"/>
          <path d="M164 154 200 112l36 42z" fill="#393536"/>
          <path d="M200 154V112l36 42z" fill="#2b2829"/>
          <path d="M200 112V92" stroke="#2b2829" stroke-width="2"/>
          <rect x="196" y="236" width="8" height="18" fill="#7b6a58"/>
        </svg></span>
        <span class="unter"><span class="kasten"><input class="cb" type="checkbox" name="bild" value="02"><svg class="zeichen" viewBox="0 0 18 18" aria-hidden="true"><path class="haken" pathLength="1" d="M4.6 9.4 7.7 12.3 13.4 5.9"/></svg></span><span class="mono nr">02</span><span class="titel">Uhrturm<em>06:48</em></span></span>
      </label></li>

      <li><label class="bild">
        <span class="rahmen"><svg viewBox="0 0 400 500" role="img" aria-label="Murinsel im Morgendunst auf dem Fluss">
          <defs>
            <linearGradient id="m-wasser" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#8aa2a3"/><stop offset="1" stop-color="#3a5153"/></linearGradient>
            <linearGradient id="m-dunst" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f4f2ec" stop-opacity="0"/><stop offset=".5" stop-color="#f4f2ec" stop-opacity=".75"/><stop offset="1" stop-color="#f4f2ec" stop-opacity="0"/></linearGradient>
            <linearGradient id="m-kuppel" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e3e9ea"/><stop offset="1" stop-color="#9fb0b5"/></linearGradient>
          </defs>
          <rect width="400" height="500" fill="#dcdcd5"/>
          <g fill="#a3aaa9"><rect x="0" y="140" width="56" height="70"/><rect x="60" y="120" width="40" height="90"/><rect x="104" y="150" width="70" height="60"/><rect x="180" y="132" width="48" height="78"/><rect x="232" y="146" width="76" height="64"/><rect x="312" y="126" width="42" height="84"/><rect x="358" y="152" width="42" height="58"/></g>
          <rect y="200" width="400" height="300" fill="url(#m-wasser)"/>
          <rect y="168" width="400" height="80" fill="url(#m-dunst)"/>
          <ellipse cx="210" cy="402" rx="130" ry="14" fill="#c9d4d6" opacity=".25"/>
          <path d="M0 336q60-12 112-22M302 304q52-10 98-24" stroke="#2c3a3c" stroke-width="5" fill="none"/>
          <ellipse cx="208" cy="318" rx="126" ry="30" fill="#b4c1c5"/>
          <path d="M92 314q74-118 228-6z" fill="url(#m-kuppel)"/>
          <g fill="none" stroke="#7d9196" stroke-width="1.6" opacity=".8"><path d="M118 310q60-80 170-2"/><path d="M146 308q50-58 120 0"/><path d="M140 262q60 16 142 22"/><path d="M180 232q14 44 6 80"/><path d="M234 236q-4 40 10 74"/></g>
          <g stroke="#f1f1ec" stroke-width="1.4" opacity=".35"><path d="M60 372h70M250 380h90M110 428h60M200 446h110M40 470h80"/></g>
        </svg></span>
        <span class="unter"><span class="kasten"><input class="cb" type="checkbox" name="bild" value="03"><svg class="zeichen" viewBox="0 0 18 18" aria-hidden="true"><path class="haken" pathLength="1" d="M4.6 9.4 7.7 12.3 13.4 5.9"/></svg></span><span class="mono nr">03</span><span class="titel">Murinsel<em>07:02</em></span></span>
      </label></li>

      <li><label class="bild">
        <span class="rahmen"><svg viewBox="0 0 400 500" role="img" aria-label="Ziegeldächer der Altstadt von oben">
          <defs>
            <symbol id="d-dach" viewBox="0 0 140 70" overflow="visible"><path d="M0 70 70 0l70 70z" fill="currentColor"/><path d="M70 0l70 70H70z" fill="#000" opacity=".2"/><path d="M14 56h112M28 42h84M42 28h56" stroke="#000" stroke-width="2" opacity=".12"/></symbol>
          </defs>
          <rect width="400" height="500" fill="#e8dccb"/>
          <rect y="70" width="400" height="430" fill="#6d4433"/>
          <use href="#d-dach" x="-40" y="60" width="140" height="70" color="#c0694a"/>
          <use href="#d-dach" x="90" y="40" width="160" height="80" color="#b35e40"/>
          <use href="#d-dach" x="240" y="56" width="150" height="74" color="#c97a55"/>
          <use href="#d-dach" x="20" y="140" width="170" height="86" color="#a9563b"/>
          <use href="#d-dach" x="180" y="130" width="140" height="96" color="#c46f4c"/>
          <use href="#d-dach" x="310" y="150" width="130" height="76" color="#b8623f"/>
          <use href="#d-dach" x="-60" y="236" width="150" height="90" color="#c97a55"/>
          <use href="#d-dach" x="80" y="222" width="180" height="104" color="#bb6644"/>
          <use href="#d-dach" x="250" y="240" width="160" height="86" color="#a75439"/>
          <use href="#d-dach" x="0" y="336" width="160" height="96" color="#b35e40"/>
          <use href="#d-dach" x="150" y="330" width="150" height="102" color="#c87650"/>
          <use href="#d-dach" x="290" y="344" width="150" height="88" color="#b05b3e"/>
          <use href="#d-dach" x="-40" y="436" width="170" height="90" color="#bf6a48"/>
          <use href="#d-dach" x="120" y="430" width="160" height="96" color="#a9563b"/>
          <use href="#d-dach" x="270" y="440" width="170" height="86" color="#c46f4c"/>
          <g fill="#8a7a6c"><rect x="196" y="58" width="12" height="22"/><rect x="104" y="168" width="10" height="20"/><rect x="330" y="186" width="12" height="20"/><rect x="190" y="260" width="12" height="24"/><rect x="60" y="372" width="10" height="20"/></g>
        </svg></span>
        <span class="unter"><span class="kasten"><input class="cb" type="checkbox" name="bild" value="04"><svg class="zeichen" viewBox="0 0 18 18" aria-hidden="true"><path class="haken" pathLength="1" d="M4.6 9.4 7.7 12.3 13.4 5.9"/></svg></span><span class="mono nr">04</span><span class="titel">Sporgasse<em>07:15</em></span></span>
      </label></li>

      <li><label class="bild">
        <span class="rahmen"><svg viewBox="0 0 400 500" role="img" aria-label="Brücke im Nebel, eine Person mit Schirm">
          <defs>
            <linearGradient id="n-luft" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e6e8e3"/><stop offset="1" stop-color="#c7cec9"/></linearGradient>
            <linearGradient id="n-band" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#f3f4f0" stop-opacity="0"/><stop offset=".5" stop-color="#f3f4f0" stop-opacity=".7"/><stop offset="1" stop-color="#f3f4f0" stop-opacity="0"/></linearGradient>
          </defs>
          <rect width="400" height="500" fill="url(#n-luft)"/>
          <g fill="#aab4ae" opacity=".55"><ellipse cx="30" cy="250" rx="60" ry="70"/><ellipse cx="380" cy="240" rx="50" ry="64"/></g>
          <rect y="300" width="400" height="200" fill="#b8c2bc"/>
          <path d="M0 262h400v12H0z" fill="#7f8b85"/>
          <path d="M0 274h400v30c-30 0-40-26-70-26s-40 26-70 26-40-26-70-26-40 26-70 26-40-26-70-26S30 304 0 304z" fill="#8f9a94"/>
          <g stroke="#7f8b85" stroke-width="2"><path d="M0 248h400"/><path d="M20 248v14M60 248v14M100 248v14M140 248v14M180 248v14M220 248v14M260 248v14M300 248v14M340 248v14M380 248v14"/></g>
          <path d="M0 306h400v8c-30 0-40 20-70 20s-40-20-70-20-40 20-70 20-40-20-70-20-40 20-70 20-40-20-70-20S30 334 0 334z" fill="#a1aca6" opacity=".6"/>
          <g fill="#2f3532"><path d="M240 222a16 9 0 0 1 32 0z"/><rect x="254" y="222" width="5" height="26"/></g>
          <path d="M256 222v-4" stroke="#2f3532" stroke-width="2"/>
          <rect y="180" width="400" height="90" fill="url(#n-band)"/>
          <rect y="320" width="400" height="80" fill="url(#n-band)" opacity=".7"/>
        </svg></span>
        <span class="unter"><span class="kasten"><input class="cb" type="checkbox" name="bild" value="05"><svg class="zeichen" viewBox="0 0 18 18" aria-hidden="true"><path class="haken" pathLength="1" d="M4.6 9.4 7.7 12.3 13.4 5.9"/></svg></span><span class="mono nr">05</span><span class="titel">Hauptbrücke<em>07:40</em></span></span>
      </label></li>

      <li><label class="bild">
        <span class="rahmen"><svg viewBox="0 0 400 500" role="img" aria-label="Kriegssteig: Stufen im Zickzack durch den Fels">
          <defs>
            <linearGradient id="s-fels" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#cfb08a"/><stop offset="1" stop-color="#94754f"/></linearGradient>
          </defs>
          <rect width="400" height="500" fill="url(#s-fels)"/>
          <g fill="#6f7b4b" opacity=".85"><ellipse cx="340" cy="90" rx="50" ry="30"/><ellipse cx="40" cy="220" rx="44" ry="26"/><ellipse cx="360" cy="300" rx="40" ry="24"/><ellipse cx="60" cy="440" rx="50" ry="26"/></g>
          <path d="M66 506 306 392 96 300l218-96-196-94 124-74" fill="none" stroke="#000" stroke-opacity=".18" stroke-width="30" stroke-linejoin="round" transform="translate(6 8)"/>
          <path d="M66 506 306 392 96 300l218-96-196-94 124-74" fill="none" stroke="#eadfcb" stroke-width="28" stroke-linejoin="round"/>
          <path d="M66 506 306 392 96 300l218-96-196-94 124-74" fill="none" stroke="#c9b99e" stroke-width="28" stroke-linejoin="round" stroke-dasharray="2 9" opacity=".9"/>
          <path d="M66 490 306 376 96 284l218-96-196-94 124-74" fill="none" stroke="#4e3d2c" stroke-width="2" stroke-linejoin="round"/>
          <g fill="#2a2621"><circle cx="196" cy="326" r="5"/><rect x="192" y="331" width="8" height="18" rx="3"/></g>
        </svg></span>
        <span class="unter"><span class="kasten"><input class="cb" type="checkbox" name="bild" value="06"><svg class="zeichen" viewBox="0 0 18 18" aria-hidden="true"><path class="haken" pathLength="1" d="M4.6 9.4 7.7 12.3 13.4 5.9"/></svg></span><span class="mono nr">06</span><span class="titel">Kriegssteig<em>08:05</em></span></span>
      </label></li>
    </ul>
  </section>

  <div class="abzug-rahmen"><form class="abzug" data-form onsubmit="return false">
    <div class="abzug-kopf">
      <h2>Abzüge bestellen</h2>
      <p>Gedruckt im eigenen Labor in der Annenstraße, nummeriert, in zehn Werktagen bei Ihnen.</p>
    </div>
    <fieldset class="papier">
      <legend class="mono versal">Papier</legend>
      <label class="zeile"><span class="kasten"><input class="rb" type="radio" name="papier" value="baryt" checked></span><span class="was">Baryt, matt<small>tiefes Schwarz, leichte Struktur</small></span></label>
      <label class="zeile"><span class="kasten"><input class="rb" type="radio" name="papier" value="baumwolle"></span><span class="was">Baumwolle, seidenmatt<small>weich, für Nebel und Dunst</small></span></label>
      <label class="zeile"><span class="kasten"><input class="rb" type="radio" name="papier" value="glanz"></span><span class="was">Fotopapier, glänzend<small>satte Farben, Spiegelung</small></span></label>
    </fieldset>
    <fieldset class="format">
      <legend class="mono versal">Format</legend>
      <label class="zeile"><span class="kasten"><input class="rb" type="radio" name="format" value="24"></span><span class="was">13 × 18 cm</span><span class="preis">24 €</span></label>
      <label class="zeile"><span class="kasten"><input class="rb" type="radio" name="format" value="38" checked></span><span class="was">20 × 25 cm</span><span class="preis">38 €</span></label>
      <label class="zeile"><span class="kasten"><input class="rb" type="radio" name="format" value="64"></span><span class="was">30 × 40 cm</span><span class="preis">64 €</span></label>
    </fieldset>
    <fieldset class="extras">
      <legend class="mono versal">Extras</legend>
      <label class="zeile"><span class="kasten"><input class="cb" type="checkbox" name="passepartout" value="14"><svg class="zeichen" viewBox="0 0 18 18" aria-hidden="true"><path class="haken" pathLength="1" d="M4.6 9.4 7.7 12.3 13.4 5.9"/></svg></span><span class="was">Passepartout, weiß</span><span class="preis">+ 14 €</span></label>
      <label class="zeile"><span class="kasten"><input class="cb" type="checkbox" name="signiert" value="0"><svg class="zeichen" viewBox="0 0 18 18" aria-hidden="true"><path class="haken" pathLength="1" d="M4.6 9.4 7.7 12.3 13.4 5.9"/></svg></span><span class="was">Handsigniert</span><span class="preis">inkl.</span></label>
    </fieldset>
    <div class="summe">
      <span class="mono" data-anzahl>2 Abzüge</span>
      <strong data-summe aria-live="polite">€ 76,00</strong>
      <button class="senden" type="submit" data-senden>Auswahl senden</button>
    </div>
  </form></div>
</main>

<footer class="steuer">
  <div class="wahl" data-varianten aria-label="Variante">
    <button type="button" aria-pressed="false" data-v="maske">Maske</button>
    <button type="button" aria-pressed="true" data-v="zeichnen">Zeichnen</button>
    <button type="button" aria-pressed="false" data-v="fuellen">Füllen</button>
  </div>
  <p class="mono werte" data-werte aria-live="polite"></p>
</footer>

<svg class="zeiger" data-zeiger viewBox="0 0 20 20" aria-hidden="true"><path d="M2 1.5 17 10l-6.6 1.5L7.2 17.8Z" fill="#141414" stroke="#f4f4f2" stroke-width="1.3" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement, body = document.body;
  const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
  const reduziert = () => html.classList.contains('reduziert');

  const bilder = $$('[name="bild"]');
  const alle = $('[data-alle]');
  const euro = new Intl.NumberFormat('de-AT', { style: 'currency', currency: 'EUR' });

  // ---------- Zustand: Zähler, Sammel-Checkbox (indeterminate per JS-Property), Summe ----------
  function abgleichen() {
    const n = bilder.filter(b => b.checked).length;
    alle.checked = n === bilder.length;
    alle.indeterminate = n > 0 && n < bilder.length;
    $('[data-zaehler]').textContent = `${n} / ${bilder.length}`;
    const preis = +$('[name="format"]:checked').value + ($('[name="passepartout"]').checked ? 14 : 0);
    $('[data-anzahl]').textContent = n === 1 ? '1 Abzug' : `${n} Abzüge`;
    $('[data-summe]').textContent = euro.format(n * preis);
    $('[data-senden]').disabled = n === 0;
  }
  alle.addEventListener('change', () => { bilder.forEach(b => { b.checked = alle.checked; }); abgleichen(); });
  $('[data-form]').addEventListener('change', abgleichen);
  bilder.forEach(b => b.addEventListener('change', abgleichen));

  // ---------- Varianten und Werte ----------
  const WERTE = {
    maske: 'Maske scale 0 → 1 · 150 ms cubic-bezier(0.25, 0.1, 0.25, 1) · Kasten füllt sich 150 ms · Radio-Punkt gleich',
    zeichnen: 'Häkchen stroke-dashoffset 1 → 0 · 300 ms cubic-bezier(0.19, 1, 0.22, 1) · Radio-Punkt scale 0 → 1 · 300 ms',
    fuellen: 'Quadrat 10/18 px scale 0 → 1 · 300 ms cubic-bezier(0.19, 1, 0.22, 1) · Rahmenfarbe 300 ms ease-out'
  };
  function werte() {
    $('[data-werte]').textContent = reduziert()
      ? 'reduziert · keine Skalierung, kein Zeichnen · Icon blendet in 100 ms linear ein'
      : WERTE[body.dataset.variante];
  }
  function variante(v) {
    body.dataset.variante = v;
    $$('[data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v));
    werte();
  }
  $$('[data-v]').forEach(b => b.addEventListener('click', () => variante(b.dataset.v)));
  new MutationObserver(werte).observe(html, { attributes: true, attributeFilter: ['class'] });

  function grundzustand() {
    bilder.forEach((b, i) => { b.checked = i === 0 || i === 3; });
    $('[name="papier"][value="baryt"]').checked = true;
    $('[name="format"][value="38"]').checked = true;
    $('[name="passepartout"]').checked = false;
    $('[name="signiert"]').checked = false;
    abgleichen();
  }

  // ---------- Vorführung: ein gedachter Zeiger klickt sich durch die Auswahl ----------
  const zeiger = $('[data-zeiger]');
  const ABLAUF = [
    () => bilder[0], () => bilder[3],                    // Ausgangsauswahl lösen
    () => bilder[1], () => bilder[2], () => bilder[4],   // drei Bilder wählen, Sammel-Checkbox wird indeterminate
    () => $('[name="papier"][value="baumwolle"]'),
    () => $('[name="format"][value="64"]'),
    () => $('[name="passepartout"]'),
    () => alle,                                          // alle wählen
    () => alle                                           // alle abwählen
  ];
  const VARIANTEN = ['zeichnen', 'maske', 'fuellen'];
  let uhr = null, schritt = 0, runde = 0;

  function zeigerAuf(el) {
    const r = el.getBoundingClientRect();
    zeiger.style.transform = `translate(${r.left + r.width * 0.55}px, ${r.top + r.height * 0.6}px)`;
  }
  function klicken(el) {
    zeigerAuf(el);
    uhr = setTimeout(() => {
      zeiger.classList.add('an');
      uhr = setTimeout(() => {
        zeiger.classList.remove('an');
        el.click();                                      // echtes click() → change-Event wie beim Nutzer
        uhr = setTimeout(naechster, 560);
      }, 140);
    }, 560);
  }
  function naechster() {
    if (schritt >= ABLAUF.length) {
      schritt = 0; runde = (runde + 1) % VARIANTEN.length;
      variante(VARIANTEN[runde]); grundzustand();
      uhr = setTimeout(naechster, 700);
      return;
    }
    klicken(ABLAUF[schritt++]());
  }
  function vorfuehren() {
    pause(); grundzustand();
    html.classList.add('vorfuehrung');
    schritt = 0; runde = 0; variante(VARIANTEN[0]);
    const r = bilder[1].getBoundingClientRect();
    zeiger.style.transition = 'none';
    zeiger.style.transform = `translate(${r.left}px, ${r.bottom + 60}px)`;
    zeiger.getBoundingClientRect(); zeiger.style.transition = '';
    uhr = setTimeout(naechster, 400);
  }
  function pause() { clearTimeout(uhr); uhr = null; }
  function neustart() { pause(); html.classList.remove('vorfuehrung'); zeiger.classList.remove('an'); grundzustand(); variante('zeichnen'); }
  function weiter() { if (!uhr && html.classList.contains('vorfuehrung')) uhr = setTimeout(naechster, 200); }
  // Nutzer übernimmt: Vorführung endet beim ersten echten Klick
  addEventListener('pointerdown', e => { if (e.isTrusted && html.classList.contains('vorfuehrung') && !html.classList.contains('vorschau')) { pause(); html.classList.remove('vorfuehrung'); } });

  grundzustand(); variante('zeichnen');

})();
</script>
</body>
</html>