Zum Inhalt
← Mikroanimationen

Toggle-Schalter

Der Daumen federt in die neue Position, wird beim Drücken breiter und die Spur wechselt die Farbe: der Zustand ist sofort lesbar und fühlt sich haptisch an.

Ein binärer Schalter. Der Daumen gleitet von links nach rechts, die Spur wechselt die Farbe. Hochwertige Varianten federn beim Ankommen, dehnen den Daumen beim Drücken (Squash) oder tauschen ein Icon im Daumen, etwa Haken und Kreuz oder Sonne und Mond. Seit 2026 gibt es dafür nativ <input type="checkbox" switch> mit accent-color.

Der Zustand ist an Position und Farbe sofort lesbar. Die Feder und der Squash machen den Wechsel haptisch, ohne die Bedienung zu verlangsamen.

Wann einsetzen

Für Einstellungen mit sofortiger Wirkung: dunkle Ansicht, Ton, Benachrichtigungen, Preis monatlich oder jährlich.

Wann nicht

Für Optionen, die erst mit „Speichern“ wirken: dann eine Checkbox. Für mehr als zwei Zustände: dann ein Segment-Schalter.

Varianten

  • Schlicht: Daumen und Spur 200 ms ease-in-out, wie bei Dub
  • Feder: Daumen schwingt leicht über, Squash beim Drücken (Breite +20 %)
  • Icon im Daumen: Haken/Kreuz oder Sonne/Mond
  • Nativer Schalter mit switch-Attribut und accent-color
  • Ziehbarer Daumen (das Polyfill unterstützt Drag)

Umsetzung

Nativ: <label>Benachrichtigungen <input type="checkbox" switch></label> und input[switch] { accent-color: var(--akzent) }. Eigene Optik: echte Checkbox mit role="switch" unsichtbar über die Spur legen, Daumen als eigenes Element. Daumen per translate bewegen, Weg = Spurbreite minus Höhe. Feder als linear()-Kurve (350 ms), Spur background-color 200 ms ease-in-out. Squash: beim Drücken die Daumenbreite um 20 % erhöhen (150 ms) und translate so rechnen, dass der Daumen an seiner Seite bleibt. Im Skript das change-Event nutzen und .checked lesen. Fokusring sichtbar lassen (outline 3px, offset 4px).

Robust umsetzen

Telefon. Die ganze Zeile als Label klickbar machen, Tippfläche mindestens 44 px. Den Squash per pointerdown/pointerup setzen, weil :active auf Touch unzuverlässig ist.

Ohne Bewegung. Der Daumen springt, die Farbe wechselt sofort. Kein Squash, kein Überschwingen.

Barrierefrei. Native Checkbox mit role="switch" bzw. switch-Attribut, damit Screenreader „Schalter, an/aus“ ansagen. Leertaste schaltet. Zustand nicht nur über Farbe zeigen: Position und gegebenenfalls Icon. Das Label ist klickbar, der Fokusring sichtbar.

Code

<!DOCTYPE html>
<!--
  Toggle-Schalter
  Fundus · Mikroanimationen · https://cecodesign.de/lab/fundus/mikroanimationen/toggle-switch/
  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>Toggle-Schalter · 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: 'Hedvig Letters Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/hedvig-letters-serif:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Google Sans Flex'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/google-sans-flex:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 1000; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

/* Farbwelt nach Rezept „praxis-gesundheit-ruhig“: kühles Off-White, Petrol als Vertrauenston,
   dunkles Grüngrau statt Schwarz, Apricot nur als Akzent. */
:root {
  --grund: #f3f6f5; --tinte: #263231; --leise: #586664; --linie: rgba(38, 50, 49, .13); --flaeche: rgba(0, 84, 80, .07);
  --aus-farbe: rgba(38, 50, 49, .22); --an-farbe: #005450; --daumen: #ffffff; --akzent: #ffc776;
  --himmel: #dcebe6; --huegel-1: #bcd6cc; --huegel-2: #86b3a3; --huegel-3: #2f6c60; --weg: #f6f1e6; --krone: #4f8c7c; --krone-dunkel: #2b5f54;
  --karte-text: #1f3a37; --pille: rgba(255, 255, 255, .88); --pille-text: #263231;
  /* Gemessen bei Dub: Spur und Daumen 200 ms ease-in-out */
  --io: cubic-bezier(0.42, 0, 0.58, 1);
  --dauer: 200ms;
  /* Feder für den Daumenweg (350 ms, ca. 9 % Überschwingen), als linear() aus einer gedämpften Feder */
  --feder: linear(0, 0.101, 0.322, 0.568, 0.783, 0.941, 1.038, 1.085, 1.095, 1.083, 1.061, 1.038, 1.018, 1.004, 0.995, 0.992, 0.991, 0.992, 0.995, 0.997, 0.999, 1);
  --feder-dauer: 350ms;
  --squash-dauer: 150ms;
}
/* Abendtermine: die ganze Seite wechselt in die Abendstimmung */
.abend {
  --grund: #0f1e1d; --tinte: #e4ecea; --leise: #9fb1ad; --linie: rgba(228, 236, 234, .13); --flaeche: rgba(228, 236, 234, .07);
  --aus-farbe: rgba(228, 236, 234, .24); --an-farbe: #f2b866; --daumen: #fbfaf6;
  --himmel: #1b3433; --huegel-1: #284845; --huegel-2: #1f3d3a; --huegel-3: #13302d; --weg: #c9bfa8; --krone: #2b514b; --krone-dunkel: #1a3a35;
  --karte-text: #eef3f1; --pille: rgba(15, 30, 29, .82); --pille-text: #e4ecea;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Google Sans Flex', system-ui, sans-serif; font-optical-sizing: auto; -webkit-font-smoothing: antialiased;
  transition: background-color var(--dauer) var(--io), color var(--dauer) var(--io); }
body { min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.label { font-weight: 560; font-size: 12.5px; line-height: 1.3; letter-spacing: .07em; text-transform: uppercase; color: var(--leise); }
.serif { font-family: 'Hedvig Letters Serif', Georgia, serif; font-weight: 400; }

/* ---------- Kopf: Marke, Telefon, Termin immer sichtbar ---------- */
.kopf { display: flex; align-items: center; gap: 18px; padding: 14px clamp(16px, 4vw, 44px); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: center; gap: 11px; margin-right: auto; text-decoration: none; color: inherit; }
.marke svg { width: 30px; height: 30px; flex: none; }
.marke .zeichen-grund { fill: var(--an-farbe); transition: fill var(--dauer) var(--io); }
.marke .zeichen-linie { fill: none; stroke: var(--grund); stroke-width: 2.2; stroke-linecap: round; transition: stroke var(--dauer) var(--io); }
.marke b { display: block; font-family: 'Hedvig Letters Serif', Georgia, serif; font-weight: 400; font-size: 17px; line-height: 1.1; letter-spacing: -.005em; }
.marke small { display: block; font-size: 12.5px; color: var(--leise); margin-top: 1px; }
.kopf .tel { font-size: 15px; color: var(--leise); font-variant-numeric: tabular-nums; text-decoration: none; }
.kopf .termin { font-weight: 560; font-size: 15px; padding: 10px 16px; border-radius: 999px; background: var(--an-farbe); color: var(--grund); text-decoration: none; white-space: nowrap;
  transition: background-color var(--dauer) var(--io), color var(--dauer) var(--io); }

/* ---------- Bühne ---------- */
.buehne { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr); gap: clamp(24px, 4vw, 56px); align-items: center;
  padding: clamp(20px, 3.4vw, 44px) clamp(16px, 4vw, 44px); max-width: 1180px; width: 100%; margin: 0 auto; }

/* Karte: Stadtpark-Illustration, wechselt mit den Abendterminen in die Abendstimmung */
.karte { position: relative; border-radius: 18px; overflow: hidden; aspect-ratio: 4 / 4.4; background: var(--himmel); isolation: isolate; color: var(--karte-text);
  transition: background-color var(--dauer) var(--io), color var(--dauer) var(--io); }
.park { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.park * { transition: fill var(--dauer) var(--io), opacity var(--dauer) var(--io), translate var(--dauer) var(--io), stroke var(--dauer) var(--io); }
.park .himmel { fill: var(--himmel); }
.park .h1 { fill: var(--huegel-1); } .park .h2 { fill: var(--huegel-2); } .park .h3 { fill: var(--huegel-3); }
.park .weg { fill: var(--weg); }
.park .krone { fill: var(--krone); } .park .krone-d { fill: var(--krone-dunkel); }
.park .stamm { fill: var(--huegel-3); }
.park .bank { fill: none; stroke: var(--weg); stroke-width: 3; stroke-linecap: round; }
.park .sonne { fill: var(--akzent); }
.park .mondsichel { fill: #f3ead6; opacity: 0; translate: 0 18px; }
.park .sterne circle { fill: #f3ead6; opacity: 0; }
.park .figur { fill: #1f3a37; } .park .figur .jacke { fill: var(--akzent); }
.abend .park .sonne { translate: 0 60px; opacity: 0; }
.abend .park .mondsichel { opacity: 1; translate: 0 0; }
.abend .park .sterne circle { opacity: .8; }
.abend .park .figur { fill: #0b1a19; }
.park .fenster { fill: var(--akzent); opacity: 0; }
.abend .park .fenster { opacity: .9; }
.park .haus { fill: var(--huegel-1); }
.abend .park .haus { fill: #2e504c; }

.karte .oben { position: absolute; left: 22px; right: 22px; top: 20px; display: grid; gap: 8px; }
.karte .oben .label { color: inherit; opacity: .72; display: flex; justify-content: space-between; gap: 12px; }
.karte .behandlung { font-family: 'Hedvig Letters Serif', Georgia, serif; font-size: clamp(26px, 3.1vw, 38px); line-height: 1.08; letter-spacing: -.01em; max-width: 11em; }
.einheiten { display: flex; align-items: center; gap: 10px; font-size: 14px; font-variant-numeric: tabular-nums; }
.einheiten i { display: flex; gap: 4px; }
.einheiten i span { width: 8px; height: 8px; border-radius: 50%; box-shadow: inset 0 0 0 1.5px currentColor; opacity: .55; }
.einheiten i span.da { background: currentColor; opacity: 1; }

.karte .unten { position: absolute; left: 12px; right: 12px; bottom: 12px; display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 14px 14px 14px 22px; border-radius: 999px; background: var(--pille); color: var(--pille-text); backdrop-filter: blur(10px);
  transition: background-color var(--dauer) var(--io), color var(--dauer) var(--io); cursor: pointer; }
.unten .zeit { display: grid; gap: 3px; min-width: 0; }
.unten .zeit b { font-weight: 500; font-size: clamp(19px, 2.2vw, 27px); letter-spacing: -.015em; line-height: 1.05; white-space: nowrap; }
.unten .zeit .label { color: inherit; opacity: .66; font-size: 11.5px; }
.tauschen { display: grid; }
.tauschen > * { grid-area: 1 / 1; transition: opacity var(--dauer) var(--io), translate var(--dauer) var(--io); }
.tauschen > :last-child, .an .tauschen > :first-child { opacity: 0; translate: 0 6px; }
.an .tauschen > :last-child { opacity: 1; translate: 0 0; }

/* Liste */
.liste h1 { font-family: 'Hedvig Letters Serif', Georgia, serif; font-weight: 400; font-size: clamp(32px, 4.3vw, 54px); line-height: 1.06; letter-spacing: -.012em; margin: 12px 0 12px; text-wrap: balance; }
.liste h1 em { font-style: normal; color: var(--an-farbe); transition: color var(--dauer) var(--io); }
.liste .lead { color: var(--leise); font-size: 17px; line-height: 1.55; max-width: 34em; margin-bottom: clamp(16px, 2.4vw, 26px); text-wrap: pretty; }
.einstellungen { border-bottom: 1px solid var(--linie); }
.zeile { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 20px; min-height: 64px; padding: 15px 0; border-top: 1px solid var(--linie); cursor: pointer; }
.zeile .titel { font-weight: 540; font-size: 18px; letter-spacing: -.005em; display: block; }
.zeile .info { display: block; color: var(--leise); font-size: 15.5px; line-height: 1.45; margin-top: 3px; text-wrap: pretty; }
.zeile .preis { font-variant-numeric: tabular-nums; color: var(--tinte); }
.zeile .preis .tauschen { display: inline-grid; }

/* ---------- Der Schalter ----------
   Echte Checkbox mit role="switch" (und switch-Attribut für Safari) liegt unsichtbar über der Spur.
   Geometrie über Variablen: --b Breite, --h Höhe, --r Rand. Daumen = --h − 2·--r, Weg = --b − --h. */
.schalter { --b: 52px; --h: 30px; --r: 3px; --d: calc(var(--h) - 2 * var(--r)); --dw: var(--d); --an: 0;
  position: relative; display: inline-block; flex: none; width: var(--b); height: var(--h); }
.schalter.gross { --b: 96px; --h: 52px; --r: 4px; }
.schalter input { position: absolute; inset: 0; z-index: 2; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; appearance: none; -webkit-appearance: none; }
.spur { position: absolute; inset: 0; border-radius: 999px; background: var(--aus-farbe); transition: background-color var(--dauer) var(--io); }
.daumen { position: absolute; left: var(--r); top: var(--r); width: var(--dw); height: var(--d); border-radius: 999px; background: var(--daumen);
  box-shadow: 0 1px 2px rgba(0, 30, 28, .2), 0 2px 8px -2px rgba(0, 30, 28, .22);
  translate: calc(var(--an) * (var(--b) - 2 * var(--r) - var(--dw))) 0;
  transition: translate var(--feder-dauer) var(--feder), width var(--squash-dauer) var(--io), background-color var(--dauer) var(--io); }
.schalter:has(input:checked) { --an: 1; }
.schalter:has(input:checked) .spur { background: var(--an-farbe); }
@media (hover: hover) { .zeile:hover .spur, .karte .unten:hover .spur { box-shadow: inset 0 0 0 999px rgba(0, 30, 28, .06); } }
/* Squash: beim Drücken wird der Daumen 20 % breiter und bleibt an seiner Seite verankert */
:is(label:active, .gedrueckt) .schalter, .schalter.gedrueckt { --dw: calc(var(--d) * 1.2); }
.schalter:has(input:focus-visible) .spur { outline: 3px solid var(--an-farbe); outline-offset: 4px; }

/* Icons im Daumen (Variante „Icon“) */
.daumen svg { position: absolute; inset: 0; margin: auto; width: 62%; height: 62%; overflow: visible; fill: none; stroke: #263231; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; opacity: 0; transition: opacity var(--dauer) var(--io); }
[data-variante="icon"] .daumen svg { opacity: 1; }
.daumen .ja, .daumen .nein { transition: opacity var(--dauer) var(--io), scale var(--dauer) var(--io), rotate var(--dauer) var(--io); transform-origin: 12px 12px; transform-box: view-box; }
.daumen .ja { opacity: 0; scale: .4; rotate: -45deg; stroke: #005450; }
.daumen .nein { stroke: rgba(38, 50, 49, .45); }
.schalter:has(input:checked) .daumen .ja { opacity: 1; scale: 1; rotate: 0deg; }
.schalter:has(input:checked) .daumen .nein { opacity: 0; scale: .4; rotate: 45deg; }
/* Sonne → Mond: Strahlen ziehen ein, ein Schattenkreis schiebt sich über die Scheibe */
.daumen .strahlen { transition: scale var(--dauer) var(--io), rotate var(--dauer) var(--io), opacity var(--dauer) var(--io); transform-origin: 12px 12px; transform-box: view-box; }
.daumen .scheibe { transition: r var(--dauer) var(--io); }
.daumen .schatten { transition: translate var(--feder-dauer) var(--feder); translate: 11px -11px; }
.schalter:has(input:checked) .strahlen { scale: .3; rotate: 90deg; opacity: 0; }
.schalter:has(input:checked) .scheibe { r: 8px; }
.schalter:has(input:checked) .schatten { translate: 0 0; }
.daumen .mond { fill: #263231; stroke: none; }
.schalter:has(input:checked) .mond { fill: #8a5a14; }

/* Variante „Schlicht“: so wie gemessen, alles 200 ms ease-in-out, kein Squash */
[data-variante="schlicht"] .daumen { transition: translate var(--dauer) var(--io), background-color var(--dauer) var(--io); }
[data-variante="schlicht"] :is(label:active, .gedrueckt) .schalter, [data-variante="schlicht"] .schalter.gedrueckt { --dw: var(--d); }

/* ---------- Steuerung (dezent) ---------- */
.steuer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 20px; padding: 14px clamp(16px, 4vw, 44px) 18px; border-top: 1px solid var(--linie); }
.wahl { display: flex; gap: 2px; flex-wrap: wrap; }
.wahl button { font: 500 11px/1 'Geist Mono', monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); padding: 7px 9px; border-radius: 99px; transition: background-color 150ms var(--io), color 150ms var(--io); }
.wahl button[aria-pressed="true"] { color: var(--tinte); background: var(--flaeche); }
.wahl button:focus-visible { outline: 2px solid var(--an-farbe); outline-offset: 2px; }
.werte { text-transform: none; letter-spacing: .02em; text-align: right; max-width: 62ch; }

/* ---------- Vorführung: gedachter Finger ---------- */
.finger { position: fixed; left: 0; top: 0; z-index: 9; width: 34px; height: 34px; margin: -17px 0 0 -17px; border-radius: 50%; pointer-events: none; display: none;
  background: rgba(0, 84, 80, .16); border: 2px solid rgba(0, 84, 80, .7); box-shadow: 0 0 0 5px rgba(243, 246, 245, .4);
  transition: transform .7s cubic-bezier(0.16, 1, 0.3, 1), scale 150ms var(--io), background-color 150ms var(--io); }
.abend .finger { background: rgba(242, 184, 102, .2); border-color: rgba(242, 184, 102, .8); box-shadow: 0 0 0 5px rgba(15, 30, 29, .35); }
.finger.an { scale: .82; background: rgba(0, 84, 80, .34); }
.abend .finger.an { background: rgba(242, 184, 102, .4); }
.vorfuehrung .finger { display: block; }

.vorschau body { overflow: hidden; }
.vorschau .steuer { display: none; }
.vorschau .buehne { align-self: center; }

/* ---------- Touch: ganze Zeile als Tippfläche, etwas mehr Luft ---------- */
@media (hover: none) {
  .zeile { min-height: 72px; }
  .wahl button { padding: 10px 11px; }
}

/* ---------- Reduzierte Bewegung: Daumen springt, Farbe wechselt sofort ---------- */
.reduziert, .reduziert * { transition: none !important; }
.reduziert :is(label:active, .gedrueckt) .schalter, .reduziert .schalter.gedrueckt { --dw: var(--d); }

@media (max-width: 900px) { .kopf .tel { display: none; } }
@media (max-width: 760px) {
  .buehne { grid-template-columns: minmax(0, 1fr); gap: 26px; align-items: start; }
  .karte { aspect-ratio: 1 / 1.02; }
  .werte { text-align: left; }
}
@media (max-width: 420px) {
  .marke small { font-size: 12px; }
  .kopf .termin { padding: 9px 13px; font-size: 14px; }
  .karte .oben { left: 18px; right: 18px; top: 16px; }
  .karte .unten { padding: 10px 10px 10px 18px; }
  .zeile { gap: 14px; }
}
/* Sehr schmal (320 px): Terminknopf zweizeilig, Werte umbrechen */
@media (max-width: 360px) {
  .kopf { gap: 12px; }
  .kopf .termin { white-space: normal; text-align: center; line-height: 1.15; padding: 7px 12px; }
  .werte { overflow-wrap: anywhere; }
  .unten .zeit b { white-space: normal; font-size: 18px; }
}
/* 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="feder">
<header class="kopf">
  <a class="marke" href="#" aria-label="Physiotherapie am Stadtpark, Bonn">
    <svg viewBox="0 0 30 30" aria-hidden="true"><circle class="zeichen-grund" cx="15" cy="15" r="15"/><path class="zeichen-linie" d="M7 19.5c3-4.2 5.4-4.2 8 0s5 4.2 8 0M11 12.2a4 4 0 0 1 8 0"/></svg>
    <span><b>Physiotherapie am Stadtpark</b><small>Bonn</small></span>
  </a>
  <a class="tel" href="#">0228 / 96 41 30</a>
  <a class="termin" href="#">Termin vereinbaren</a>
</header>

<main class="buehne">
  <figure class="karte">
    <svg class="park" viewBox="0 0 400 440" preserveAspectRatio="xMidYMax slice" aria-hidden="true">
      <rect class="himmel" width="400" height="440"/>
      <g class="sterne"><circle cx="58" cy="168" r="1.4"/><circle cx="132" cy="190" r="1.1"/><circle cx="246" cy="160" r="1.3"/><circle cx="372" cy="214" r="1.1"/><circle cx="196" cy="204" r="1"/><circle cx="386" cy="150" r="1.2"/><circle cx="24" cy="214" r="1"/></g>
      <circle class="sonne" cx="322" cy="182" r="26"/>
      <path class="mondsichel" d="M322 158a24 24 0 1 0 17 41a20 20 0 1 1 -17 -41z"/>
      <g transform="translate(0 -50)">
      <path class="h1" d="M0 286C70 256 146 258 214 276S338 278 400 256V500H0Z"/>
      <rect class="haus" x="262" y="262" width="44" height="22" rx="2"/><path class="haus" d="M258 263l26-14 26 14z"/>
      <rect class="fenster" x="270" y="268" width="6" height="7" rx="1"/><rect class="fenster" x="281" y="268" width="6" height="7" rx="1"/><rect class="fenster" x="292" y="268" width="6" height="7" rx="1"/>
      <path class="h2" d="M0 322C88 294 182 304 252 322S360 322 400 306V500H0Z"/>
      <g><rect class="stamm" x="64" y="292" width="4" height="22"/><circle class="krone-d" cx="66" cy="284" r="17"/>
         <rect class="stamm" x="98" y="298" width="3.4" height="18"/><circle class="krone" cx="99.7" cy="292" r="12"/>
         <rect class="stamm" x="344" y="290" width="4" height="22"/><circle class="krone" cx="346" cy="282" r="15"/>
         <rect class="stamm" x="370" y="300" width="3" height="14"/><circle class="krone-d" cx="371.5" cy="295" r="10"/></g>
      <path class="h3" d="M0 372C104 346 250 362 400 340V500H0Z"/>
      <path class="weg" d="M138 500L150 440C176 404 296 398 268 364C248 340 206 334 236 312L244 313C222 330 262 340 280 364C312 404 222 414 226 440L234 500Z"/>
      <path class="bank" d="M146 308h24M148 308v7M168 308v7M146 302.5h24"/>
      <g class="figur" transform="translate(255 354) scale(1.3)">
        <circle cx="0" cy="-27" r="4.2"/>
        <rect class="jacke" x="-4.2" y="-22" width="8.4" height="14" rx="3.6"/>
        <path d="M-1.6 -8.6l-5 11.2 2.6 1 4.4-9.4zM1.6 -8.6l3.6 11.6 2.7-.9-3.4-11.4z"/>
      </g>
      <g><rect class="stamm" x="28" y="352" width="5" height="30"/><circle class="krone" cx="30.5" cy="340" r="24"/><circle class="krone-d" cx="44" cy="352" r="14"/></g>
      </g>
    </svg>
    <figcaption class="oben">
      <span class="label"><span>Ihr nächster Termin</span><span data-uhr>Vormittag</span></span>
      <span class="behandlung">Krankengymnastik, Rücken</span>
      <span class="einheiten"><i aria-hidden="true"><span class="da"></span><span class="da"></span><span class="da"></span><span class="da"></span><span class="da"></span><span></span><span></span><span></span><span></span><span></span></i>Einheit 6 von 10</span>
    </figcaption>
    <label class="unten" data-zeile>
      <span class="zeit">
        <span class="label">Abendtermine bevorzugen</span>
        <b class="tauschen"><span>Do 14.11. · 9:30 Uhr</span><span>Do 14.11. · 18:15 Uhr</span></b>
      </span>
      <span class="schalter gross">
        <input type="checkbox" role="switch" switch data-abend aria-label="Abendtermine bevorzugen, dunkle Abendansicht">
        <span class="spur"></span>
        <span class="daumen"><svg viewBox="0 0 24 24" aria-hidden="true">
          <mask id="m-mond"><rect x="-4" y="-4" width="32" height="32" fill="#fff"/><circle class="schatten" cx="15.5" cy="8.5" r="6.5" fill="#000"/></mask>
          <g class="strahlen"><path d="M12 1.5v2.6M12 19.9v2.6M1.5 12h2.6M19.9 12h2.6M4.6 4.6l1.8 1.8M17.6 17.6l1.8 1.8M4.6 19.4l1.8-1.8M17.6 6.4l1.8-1.8"/></g>
          <circle class="scheibe mond" cx="12" cy="12" r="5" mask="url(#m-mond)"/>
        </svg></span>
      </span>
    </label>
  </figure>

  <section class="liste">
    <p class="label">Patientenkonto · Erinnerungen</p>
    <h1>Wir erinnern Sie, <em>wann es Ihnen passt.</em></h1>
    <p class="lead">Drei Schalter, kein Speichern. Was Sie hier umlegen, gilt ab der nächsten Erinnerung.</p>
    <div class="einstellungen">
      <label class="zeile" data-zeile>
        <span><span class="titel">SMS am Vortag</span><span class="info">Um 18 Uhr, mit Uhrzeit, Raum und Therapeutin.</span></span>
        <span class="schalter"><input type="checkbox" role="switch" switch checked><span class="spur"></span><span class="daumen"><svg viewBox="0 0 24 24" aria-hidden="true"><path class="ja" d="M6 12.5l4 4 8-9"/><path class="nein" d="M8 8l8 8M16 8l-8 8"/></svg></span></span>
      </label>
      <label class="zeile" data-zeile>
        <span><span class="titel">Übungsplan nach jeder Behandlung</span><span class="info">Ihre Übungen für zu Hause, mit kurzen Videos, per E-Mail.</span></span>
        <span class="schalter"><input type="checkbox" role="switch" switch><span class="spur"></span><span class="daumen"><svg viewBox="0 0 24 24" aria-hidden="true"><path class="ja" d="M6 12.5l4 4 8-9"/><path class="nein" d="M8 8l8 8M16 8l-8 8"/></svg></span></span>
      </label>
      <label class="zeile" data-zeile data-preiszeile>
        <span><span class="titel">Gerätetraining jährlich zahlen</span><span class="info">Zweimal pro Woche, betreut · <span class="preis"><span class="tauschen"><span>49 € im Monat</span><span>41 € im Monat, spart 96 €</span></span></span></span></span>
        <span class="schalter"><input type="checkbox" role="switch" switch><span class="spur"></span><span class="daumen"><svg viewBox="0 0 24 24" aria-hidden="true"><path class="ja" d="M6 12.5l4 4 8-9"/><path class="nein" d="M8 8l8 8M16 8l-8 8"/></svg></span></span>
      </label>
    </div>
  </section>
</main>

<footer class="steuer">
  <div class="wahl" data-varianten aria-label="Variante">
    <button type="button" aria-pressed="false" data-v="schlicht">Schlicht</button>
    <button type="button" aria-pressed="true" data-v="feder">Feder + Squash</button>
    <button type="button" aria-pressed="false" data-v="icon">Icon im Daumen</button>
  </div>
  <p class="mono werte" data-werte aria-live="polite"></p>
</footer>

<div class="finger" data-finger aria-hidden="true"></div>

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

  // ---------- Wirkung der Schalter: change-Event, .checked lesen ----------
  function abgleichen() {
    $$('[data-zeile]').forEach(z => z.classList.toggle('an', z.querySelector('input').checked));
    const abend = $('[data-abend]').checked;
    html.classList.toggle('abend', abend);
    $('[data-uhr]').textContent = abend ? 'Abend' : 'Vormittag';
  }
  $$('[data-zeile] input').forEach(i => i.addEventListener('change', abgleichen));
  abgleichen();

  // Squash auch für Touch: :active greift auf Mobilgeräten unzuverlässig, daher eigene Klasse
  $$('[data-zeile]').forEach(z => {
    const los = () => z.classList.remove('gedrueckt');
    z.addEventListener('pointerdown', () => z.classList.add('gedrueckt'));
    ['pointerup', 'pointercancel', 'pointerleave'].forEach(t => z.addEventListener(t, los));
  });

  // ---------- Varianten und Werte ----------
  const WERTE = {
    schlicht: 'Daumen + Spur 200 ms cubic-bezier(0.42, 0, 0.58, 1) · kein Squash · Weg 22 px',
    feder: 'Daumen 350 ms Feder linear() +9 % · Squash +20 % 150 ms · Spur 200 ms cubic-bezier(0.42, 0, 0.58, 1)',
    icon: 'Feder + Icon 200 ms cubic-bezier(0.42, 0, 0.58, 1) · rotate 45° · scale 0.4 → 1'
  };
  function werte() {
    $('[data-werte]').textContent = reduziert()
      ? 'reduziert · Daumen springt, Farbe wechselt sofort · 0 ms'
      : 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)));
  werte();
  new MutationObserver(werte).observe(html, { attributes: true, attributeFilter: ['class'] });

  // ---------- Vorführung: ein gedachter Finger legt die Schalter um ----------
  const finger = $('[data-finger]');
  const zeilen = $$('[data-zeile]');
  // Reihenfolge: Abendtermine an, Übungsplan, jährlich, Abendtermine aus, SMS …
  const ABLAUF = [0, 2, 3, 0, 1, 2, 3, 1];
  const VARIANTEN = ['feder', 'icon', 'schlicht'];
  let uhr = null, schritt = 0, runde = 0;

  function fingerAuf(el) {
    const r = el.querySelector('.schalter').getBoundingClientRect();
    finger.style.transform = `translate(${r.left + r.width * 0.5}px, ${r.top + r.height * 0.5}px)`;
  }
  function tippen(zeile) {
    const sch = zeile.querySelector('.schalter'), input = zeile.querySelector('input');
    fingerAuf(zeile);
    uhr = setTimeout(() => {
      finger.classList.add('an'); sch.classList.add('gedrueckt');
      uhr = setTimeout(() => {
        finger.classList.remove('an'); sch.classList.remove('gedrueckt');
        input.checked = !input.checked; abgleichen();
        uhr = setTimeout(weiterTippen, 650);
      }, 260);       // gehalten, damit der Squash sichtbar ist
    }, 620);         // Finger fährt hin
  }
  function weiterTippen() {
    if (schritt >= ABLAUF.length) {
      schritt = 0; runde = (runde + 1) % VARIANTEN.length;
      variante(VARIANTEN[runde]);
    }
    tippen(zeilen[ABLAUF[schritt++]]);
  }
  function vorfuehren() {
    pause(); grundzustand();
    html.classList.add('vorfuehrung');
    schritt = 0; runde = 0; variante('feder');
    const r = zeilen[1].getBoundingClientRect();
    finger.style.transition = 'none';
    finger.style.transform = `translate(${r.left + r.width * 0.5}px, ${r.bottom + 40}px)`;
    finger.offsetWidth; finger.style.transition = '';
    uhr = setTimeout(weiterTippen, 300);
  }
  function grundzustand() {
    zeilen.forEach((z, i) => { z.querySelector('input').checked = i === 1; z.querySelector('.schalter').classList.remove('gedrueckt'); });
    finger.classList.remove('an');
    abgleichen();
  }
  function pause() { clearTimeout(uhr); uhr = null; }
  function neustart() { pause(); html.classList.remove('vorfuehrung'); grundzustand(); variante('feder'); }
  function weiter() { if (!uhr && html.classList.contains('vorfuehrung')) uhr = setTimeout(weiterTippen, 200); }
  addEventListener('resize', () => { if (uhr && zeilen[ABLAUF[Math.max(0, schritt - 1)]]) fingerAuf(zeilen[ABLAUF[Math.max(0, schritt - 1)]]); });

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