Druck-Feedback (Press/Tap)
Knöpfe, Tabs und Karten geben beim Drücken kurz nach und federn mit leichtem Überschwinger zurück.
Beim Drücken gibt das Element kurz nach: Es schrumpft minimal (scale 0.95 bis 0.98) oder sinkt um 1 bis 2 px ein, der Schatten wird kleiner. Beim Loslassen federt es zurück, oft mit leichtem Überschwinger. Optional kommen ein kurzer Glow, ein Ripple oder eine Vibration dazu.
Der Knopf fühlt sich an wie ein echter Schalter. Das ist besonders auf Touch wichtig, wo es kein Hover gibt. Kleiner Aufwand, großer Gewinn an Wertigkeit.
Wann einsetzen
Alle Buttons, Karten mit Klickziel, Toggles, Icon-Buttons und Tabs.
Wann nicht
Nicht bei Textlinks im Fließtext. Skalierung unter 0.94 wirkt billig. Deaktivierte Buttons bekommen keinen Druckzustand.
Varianten
- Skalieren: :active { scale: 0.97 } mit kurzem Rücksprung. Belegt sind 0.95 (Apple), 0.96 (Build in Amsterdam), 0.975 (Instrument) und 0.98 (Collins).
- Einsinken: translateY 1 bis 2 px, Schatten wird kleiner.
- Squash: scaleX 1.02 und scaleY 0.96 beim Drücken.
- In Motion (React): whileTap={{ scale: 0.97 }}, zusammen mit whileFocus.
- Haptik: navigator.vibrate(8 bis 15) auf Android. iOS ignoriert das.
Umsetzung
.btn { transition: scale .5s var(--feder); } .btn:not(:disabled):active { scale: .96; transition: scale .1s cubic-bezier(0.45, 0.02, 0.09, 0.98); }. Einfedern schnell (80 bis 100 ms), Ausfedern langsamer mit einer Feder als linear()-Kurve. Große Flächen wie Karten weniger stark skalieren (0.98). Robuster als reines :active ist eine Klasse, die bei pointerdown gesetzt und bei pointerup, pointercancel und pointerleave entfernt wird. Eine Mindest-Druckzeit von etwa 100 ms sorgt dafür, dass auch ein sehr kurzer Tipp sichtbar einfedert. Nur scale, translate und Schatten animieren, keine Layout-Eigenschaften.
Robust umsetzen
Telefon. Wichtigster Ersatz für Hover. Auf iOS greift :active erst mit einem (leeren) touchstart-Listener zuverlässig. touch-action: manipulation und -webkit-tap-highlight-color: transparent setzen.
Ohne Bewegung. Farb- oder Helligkeitswechsel statt Skalierung, z. B. brightness(0.88) für 80 ms.
Barrierefrei. Auch die Tastatur drückt sichtbar: Bei Leertaste und Enter die Druck-Klasse per keydown setzen und per keyup entfernen. Der Fokusring bleibt erhalten. Für Toggles aria-pressed, für Auswahlgruppen role=radio mit aria-checked.
Code
<!DOCTYPE html>
<!--
Druck-Feedback (Press/Tap)
Fundus · Mikroanimationen · https://cecodesign.de/lab/fundus/mikroanimationen/druck-feedback/
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>Druck-Feedback · 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: 'Bricolage Grotesque'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/bricolage-grotesque:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'Onest'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/onest:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Shantell Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/shantell-sans:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 300 800; 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; }
:root {
/* Rezept „kurs-coaching-lernweg“: kühler heller Grund, fast schwarze Tinte, ein Markenton, eigene Erfolgsfarbe */
--grund: #eef1fa; --karte: #ffffff; --tinte: #12142b; --leise: rgba(18, 20, 43, .62); --linie: rgba(18, 20, 43, .12);
--marke: #4655e6; --marke-hell: #7491ff; --gelb: #f7c325; --koralle: #ff7a59; --flieder: #c9b8ff; --erfolg: #10bf7a;
/* Einfedern: gemessene Kurve (Build in Amsterdam), schnell */
--kurve: cubic-bezier(0.45, 0.02, 0.09, 0.98);
/* Ausfedern: gedämpfte Feder, Dämpfung 0,5, ca. 16 % Überschwinger */
--feder: linear(0, 0.044, 0.155, 0.307, 0.476, 0.643, 0.795, 0.923, 1.023, 1.095, 1.139, 1.16, 1.162, 1.15, 1.129, 1.102, 1.075, 1.048, 1.025, 1.006, 0.991, 0.981, 0.976, 0.973, 0.974, 0.977, 0.981, 0.985, 0.99, 0.994, 0.997, 1, 1.002, 1.003, 1.004, 1.004, 1.004, 1.004, 1.003, 1.002, 1);
--ein: 100ms; --aus: 500ms;
/* Variante „Skalieren“ (Standard) */
--s: .96; --s-gross: .98; --t: 0 0; --schatten: 0 10px 22px -12px rgba(70, 85, 230, .75); --schatten-an: var(--schatten);
}
[data-variante="einsinken"] { --s: 1; --s-gross: 1; --t: 0 2px; --schatten-an: 0 3px 6px -4px rgba(70, 85, 230, .7); }
[data-variante="squash"] { --s: 1.02 .96; --s-gross: 1.01 .98; }
.zeitlupe { --ein: 400ms; --aus: 2000ms; }
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Onest', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 1fr auto; background: var(--grund); }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.mono { font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.label { font: 600 12px/1.2 'Onest', sans-serif; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.notiz { font: 500 15px/1.3 'Shantell Sans', 'Comic Neue', sans-serif; font-variation-settings: 'INFM' 40, 'BNCE' 0; color: var(--marke); }
/* ---------- Der Effekt ---------- */
.drueck {
-webkit-tap-highlight-color: transparent; touch-action: manipulation; user-select: none; -webkit-user-select: none;
transition: scale var(--aus) var(--feder), translate var(--aus) var(--feder), box-shadow var(--aus) var(--feder), filter 80ms linear;
}
.drueck.gedrueckt {
scale: var(--s); translate: var(--t);
transition-duration: var(--ein); transition-timing-function: var(--kurve);
}
.drueck.gross.gedrueckt { scale: var(--s-gross); }
.drueck:disabled { cursor: not-allowed; }
.drueck:focus-visible { outline: 2px solid var(--marke); outline-offset: 3px; }
/* ---------- Kopf ---------- */
.kopf { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px clamp(16px, 4vw, 44px); }
.marke { display: flex; align-items: center; gap: 10px; font: 700 18px/1 'Bricolage Grotesque', sans-serif; font-variation-settings: 'opsz' 24; letter-spacing: -.02em; }
.marke svg { width: 26px; height: 26px; flex: none; }
.marke span { font: 500 14px/1 'Onest', sans-serif; letter-spacing: 0; color: var(--leise); }
.korb { display: inline-flex; align-items: center; gap: 10px; padding: 7px 7px 7px 16px; border-radius: 99px; background: var(--karte); font-size: 14px; font-weight: 600; box-shadow: 0 0 0 1px var(--linie); }
.korb b { display: grid; place-items: center; min-width: 26px; height: 26px; padding: 0 8px; border-radius: 99px; background: var(--tinte); color: #fff; font: 600 13px/1 'Onest', sans-serif; font-variant-numeric: tabular-nums; transition: background-color .3s linear; }
.korb.neu b { background: var(--erfolg); }
/* ---------- Bühne ---------- */
.buehne { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.08fr); gap: clamp(24px, 4vw, 60px); align-items: center; padding: clamp(10px, 2vw, 28px) clamp(16px, 4vw, 44px) clamp(18px, 3vw, 36px); max-width: 1120px; width: 100%; margin: 0 auto; }
/* Plakatfläche: eigene SVG-Illustration je Instrument auf Punkteraster */
.plakat { position: relative; aspect-ratio: 4 / 5; max-height: 540px; justify-self: center; width: 100%; max-width: 430px; border-radius: 22px; background: var(--marke); overflow: hidden; isolation: isolate; }
.plakat svg.bild { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.instrument { opacity: 0; transition: opacity .3s cubic-bezier(0.22, 1, 0.36, 1); }
.plakat[data-instrument="klavier"] .i-klavier, .plakat[data-instrument="gitarre"] .i-gitarre, .plakat[data-instrument="trommeln"] .i-trommeln { opacity: 1; }
.sticker { position: absolute; z-index: 2; top: 18px; right: 18px; width: 96px; height: 96px; border-radius: 50%; background: var(--gelb); color: var(--tinte); display: grid; place-content: center; text-align: center; rotate: 9deg;
font: 700 13px/1.12 'Bricolage Grotesque', sans-serif; font-variation-settings: 'opsz' 14; letter-spacing: -.01em; box-shadow: 0 8px 18px -10px rgba(18, 20, 43, .6); }
.sticker small { display: block; margin-top: 3px; font: 600 11px/1 'Onest', sans-serif; font-variant-numeric: tabular-nums; }
.plakat .fuss { position: absolute; z-index: 2; left: 18px; right: 18px; bottom: 16px; display: flex; justify-content: space-between; gap: 12px; color: #fff; }
.plakat .fuss .label { color: rgba(255, 255, 255, .86); }
.plakat .hand { position: absolute; z-index: 2; left: 20px; top: 20px; color: #fff; rotate: -6deg; font-size: 16px; }
.info { display: grid; gap: 16px; max-width: 480px; }
h1 { font: 750 clamp(2.25rem, 1.4rem + 3.4vw, 4.25rem)/1.02 'Bricolage Grotesque', sans-serif; font-variation-settings: 'opsz' 84; letter-spacing: -.025em; text-wrap: balance; }
h1 mark { position: relative; z-index: 0; color: inherit; background: none; white-space: nowrap; }
h1 mark::before { content: ''; position: absolute; z-index: -1; left: -.06em; right: -.08em; bottom: .06em; height: .36em; border-radius: .2em .5em .3em .45em; background: var(--gelb); rotate: -1.2deg; }
.text { font-size: 17px; line-height: 1.55; color: var(--leise); max-width: 40ch; text-wrap: pretty; }
.preis { display: flex; align-items: baseline; gap: 10px; font: 700 26px/1 'Bricolage Grotesque', sans-serif; font-variation-settings: 'opsz' 28; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
.preis .label { font-family: 'Onest', sans-serif; }
.gruppe { display: grid; gap: 8px; }
.tabs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; padding: 4px; border-radius: 14px; background: var(--karte); box-shadow: 0 0 0 1px var(--linie); }
.tab { padding: 11px 8px; border-radius: 10px; font-size: 15px; font-weight: 600; color: var(--leise); background-color: transparent; }
.tab[aria-checked="true"] { background-color: var(--tinte); color: #fff; }
.abos { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.abo { text-align: left; padding: 13px 15px; border-radius: 14px; background: var(--karte); box-shadow: 0 0 0 1px var(--linie); display: grid; gap: 3px; }
.abo strong { font-weight: 650; font-size: 15px; }
.abo span { font-size: 13.5px; color: var(--leise); font-variant-numeric: tabular-nums; }
.abo i { font-style: normal; }
.abo[aria-checked="true"] { box-shadow: 0 0 0 2px var(--tinte); }
.kauf { display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: stretch; }
.menge { display: flex; align-items: center; border-radius: 99px; background: var(--karte); box-shadow: 0 0 0 1px var(--linie); padding: 4px; }
.menge output { min-width: 26px; text-align: center; font: 650 16px 'Onest', sans-serif; font-variant-numeric: tabular-nums; }
.rund { width: 42px; height: 42px; border-radius: 50%; display: grid; place-items: center; }
.rund svg { width: 16px; height: 16px; }
.menge .rund:not(:disabled):hover { background: rgba(18, 20, 43, .06); }
.menge .rund:disabled { opacity: .3; }
.cta { border-radius: 99px; background: var(--marke); color: #fff; font-size: 16px; font-weight: 650; padding: 0 22px; min-height: 54px; box-shadow: var(--schatten); display: flex; align-items: center; justify-content: center; gap: 10px; white-space: nowrap; }
.cta.gedrueckt { box-shadow: var(--schatten-an); }
.cta small { font-size: 12px; font-weight: 600; padding: 4px 8px; border-radius: 99px; background: rgba(255, 255, 255, .1); }
@media (max-width: 1100px) { .cta small { display: none; } }
.merken { width: 54px; height: 54px; border-radius: 50%; background: var(--karte); box-shadow: 0 0 0 1px var(--linie); }
.merken svg { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.7; transition: fill .2s linear, stroke .2s linear; }
.merken[aria-pressed="true"] svg { fill: var(--gelb); stroke: var(--tinte); }
.meldung { min-height: 1.4em; display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 550; color: var(--tinte); }
.meldung:not(:empty)::before { content: ''; flex: none; width: 18px; height: 18px; border-radius: 50%; background: var(--erfolg) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 18'%3E%3Cpath d='M5 9.2l2.6 2.6L13 6.4' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 18px no-repeat; }
/* ---------- 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); }
.schalter { display: flex; gap: 2px; flex-wrap: wrap; }
.schalter button { font: 500 11px/1 'Geist Mono', monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); padding: 7px 9px; border-radius: 99px; }
.schalter button[aria-pressed="true"] { color: var(--tinte); background: var(--karte); box-shadow: 0 0 0 1px var(--linie); }
.werte { text-transform: none; letter-spacing: .02em; font-size: 11px; text-align: right; }
/* ---------- 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(18, 20, 43, .14); border: 2px solid rgba(18, 20, 43, .72); box-shadow: 0 0 0 6px rgba(255, 255, 255, .55); transition: transform .7s cubic-bezier(0.16, 1, 0.3, 1); }
.finger::after { content: ''; position: absolute; inset: -1.5px; border-radius: 50%; border: 1.5px solid rgba(18, 20, 43, .5); opacity: 0; }
.finger i { position: absolute; inset: 0; border-radius: 50%; transition: scale var(--aus) var(--feder); }
.finger.an { background: rgba(18, 20, 43, .3); }
.finger.an::after { animation: ring .55s cubic-bezier(0.16, 1, 0.3, 1); }
@keyframes ring { from { opacity: .8; scale: 1; } to { opacity: 0; scale: 2.1; } }
.vorfuehrung .finger { display: block; }
.vorschau body { overflow: hidden; }
.vorschau .steuer { display: none; }
/* ---------- Touch: kein Hover, Druck ist das Feedback ---------- */
@media (hover: none) {
.menge .rund:not(:disabled):hover { background: none; }
}
/* ---------- Reduzierte Bewegung: Helligkeit statt Größe ---------- */
.reduziert .drueck, .reduziert .drueck.gedrueckt { scale: none; translate: none; transition: filter 80ms linear, background-color 80ms linear; }
.reduziert .drueck.gedrueckt { filter: brightness(.88); }
.reduziert .tab.gedrueckt, .reduziert .abo.gedrueckt, .reduziert .rund.gedrueckt, .reduziert .merken.gedrueckt { background-color: rgba(18, 20, 43, .1); filter: none; }
.reduziert .tab[aria-checked="true"].gedrueckt { background-color: var(--tinte); filter: brightness(1.6); }
.reduziert .instrument { transition: none; }
.reduziert .finger { transition: none; }
.reduziert .finger.an::after { animation: none; }
@media (max-width: 760px) {
.buehne { grid-template-columns: 1fr; gap: 20px; padding-top: 4px; }
.plakat { aspect-ratio: 16 / 11; max-width: none; border-radius: 18px; }
.sticker { width: 80px; height: 80px; font-size: 11.5px; top: 12px; right: 12px; }
.sticker small { font-size: 10px; }
.plakat .hand { font-size: 14px; left: 16px; top: 14px; }
.plakat .fuss { left: 16px; right: 16px; bottom: 12px; }
.plakat .motiv { transform-origin: 200px 250px; transform: translate(-34px, -14px) scale(.72); }
.werte { text-align: left; }
}
@media (max-width: 420px) {
.marke span { display: none; }
.text { font-size: 16px; }
.abo { padding: 11px 12px; }
.abo i { display: none; }
.rund { width: 38px; height: 38px; }
.menge output { min-width: 22px; }
.cta { padding: 0 14px; font-size: 15px; }
.merken { width: 50px; height: 50px; }
.plakat .fuss .label:last-child { display: none; }
}
/* Zugang: 320 px ohne Querscrollen */
@media (max-width: 360px) {
.buehne { grid-template-columns: minmax(0, 1fr); }
.info { min-width: 0; }
.tabs { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.tab { padding-inline: 4px; font-size: 14px; }
.abos { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.abo { padding: 11px 10px; }
.kauf { grid-template-columns: auto minmax(0, 1fr) auto; gap: 6px; }
.cta { white-space: normal; text-align: center; line-height: 1.15; padding: 0 10px; }
.kopf { gap: 10px; }
.korb { padding-left: 12px; gap: 8px; }
.werte { overflow-wrap: anywhere; }
}
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body data-variante="skalieren">
<header class="kopf">
<p class="marke"><svg viewBox="0 0 26 26" aria-hidden="true"><rect x="2" y="9" width="6" height="15" rx="3" fill="#4655e6"/><rect x="10" y="2" width="6" height="22" rx="3" fill="#12142b"/><rect x="18" y="13" width="6" height="11" rx="3" fill="#ff7a59"/></svg>Klangwerk <span>Musikschule · Münster</span></p>
<button class="korb drueck" type="button" data-korb aria-label="Anmeldungen, 0">Anmeldungen <b data-anzahl>0</b></button>
</header>
<main class="buehne">
<figure class="plakat" data-plakat data-instrument="klavier" aria-hidden="true">
<svg class="bild" viewBox="0 0 400 500" preserveAspectRatio="xMidYMid slice">
<defs>
<pattern id="raster" width="25" height="25" patternUnits="userSpaceOnUse"><circle cx="12.5" cy="12.5" r="2" fill="rgba(255,255,255,.2)"/></pattern>
</defs>
<rect width="400" height="500" fill="#4655e6"/>
<rect width="400" height="500" fill="url(#raster)"/>
<!-- Melodie aus Rasterpunkten -->
<g>
<circle cx="37.5" cy="112.5" r="7" fill="#f7c325"/><circle cx="62.5" cy="87.5" r="7" fill="#fff"/><circle cx="87.5" cy="87.5" r="7" fill="#ff7a59"/>
<circle cx="112.5" cy="62.5" r="7" fill="#c9b8ff"/><circle cx="137.5" cy="87.5" r="7" fill="#fff"/><circle cx="162.5" cy="112.5" r="7" fill="#f7c325"/>
<circle cx="187.5" cy="137.5" r="7" fill="#12142b"/>
<circle cx="237.5" cy="437.5" r="7" fill="#fff"/><circle cx="262.5" cy="412.5" r="7" fill="#f7c325"/><circle cx="287.5" cy="412.5" r="7" fill="#ff7a59"/>
<circle cx="312.5" cy="387.5" r="7" fill="#c9b8ff"/><circle cx="337.5" cy="412.5" r="7" fill="#12142b"/><circle cx="362.5" cy="437.5" r="7" fill="#fff"/>
</g>
<g class="motiv">
<!-- Klavier -->
<g class="instrument i-klavier">
<circle cx="282" cy="196" r="74" fill="#ff7a59"/>
<g transform="rotate(-9 200 285)">
<rect x="34" y="212" width="332" height="150" rx="16" fill="#12142b"/>
<rect x="34" y="212" width="332" height="22" rx="11" fill="#2a2d52"/>
<g fill="#fff">
<rect x="50" y="240" width="35" height="108" rx="5"/><rect x="88" y="240" width="35" height="108" rx="5"/><rect x="126" y="240" width="35" height="108" rx="5"/>
<rect x="164" y="240" width="35" height="108" rx="5"/><rect x="202" y="240" width="35" height="108" rx="5" fill="#f7c325"/><rect x="240" y="240" width="35" height="108" rx="5"/>
<rect x="278" y="240" width="35" height="108" rx="5"/><rect x="316" y="240" width="35" height="108" rx="5"/>
</g>
<g fill="#12142b">
<rect x="74" y="240" width="22" height="64" rx="4"/><rect x="113" y="240" width="22" height="64" rx="4"/>
<rect x="189" y="240" width="22" height="64" rx="4"/><rect x="227" y="240" width="22" height="64" rx="4"/><rect x="265" y="240" width="22" height="64" rx="4"/>
<rect x="341" y="240" width="10" height="64" rx="3"/>
</g>
</g>
</g>
<!-- Gitarre -->
<g class="instrument i-gitarre">
<circle cx="120" cy="190" r="64" fill="#f7c325"/>
<g transform="rotate(32 200 260) translate(0 -6)">
<rect x="187" y="40" width="26" height="200" rx="4" fill="#12142b"/>
<rect x="179" y="14" width="42" height="46" rx="10" fill="#12142b"/>
<g fill="#c9b8ff"><circle cx="173" cy="26" r="5"/><circle cx="173" cy="46" r="5"/><circle cx="227" cy="26" r="5"/><circle cx="227" cy="46" r="5"/></g>
<circle cx="200" cy="350" r="82" fill="#ff7a59"/>
<circle cx="200" cy="262" r="62" fill="#ff7a59"/>
<circle cx="200" cy="300" r="25" fill="#12142b"/>
<rect x="172" y="372" width="56" height="11" rx="5" fill="#12142b"/>
<g stroke="rgba(255,255,255,.3)" stroke-width="2"><path d="M187 90h26M187 128h26M187 162h26M187 192h26M187 218h26"/></g>
<g stroke="#fff" stroke-width="1.3"><path d="M193.5 36V377M198 36V377M202 36V377M206.5 36V377"/></g>
</g>
</g>
<!-- Trommeln -->
<g class="instrument i-trommeln" transform="translate(0 -14)">
<circle cx="300" cy="200" r="60" fill="#c9b8ff"/>
<ellipse cx="200" cy="370" rx="124" ry="34" fill="#d9a400"/>
<rect x="76" y="250" width="248" height="120" fill="#f7c325"/>
<polyline points="88,262 120,358 152,262 184,358 216,262 248,358 280,262 312,358" fill="none" stroke="#12142b" stroke-width="5" stroke-linejoin="round"/>
<path d="M76 362a124 34 0 0 0 248 0" fill="none" stroke="#12142b" stroke-width="9"/>
<ellipse cx="200" cy="250" rx="124" ry="34" fill="#fff" stroke="#12142b" stroke-width="9"/>
<g stroke-linecap="round">
<path d="M104 132L198 238" stroke="#12142b" stroke-width="10"/>
<path d="M318 120L218 236" stroke="#12142b" stroke-width="10"/>
</g>
<circle cx="198" cy="238" r="9" fill="#ff7a59"/><circle cx="218" cy="236" r="9" fill="#ff7a59"/>
</g>
</g>
</svg>
<span class="notiz hand">probier's aus!</span>
<p class="sticker">Schnupper-<br>woche<small>10.–15.11.</small></p>
<div class="fuss"><span class="label" data-raum>Raum 2 · Flügelzimmer</span><span class="label">5 bis 12 Jahre</span></div>
</figure>
<div class="info">
<p class="notiz">Kurs 01 · 45 Min. pro Woche</p>
<h1>In zehn Wochen zum <mark>ersten Vorspiel</mark></h1>
<p class="text">Für Kinder von 5 bis 12, ganz ohne Vorkenntnisse. Erst eine kostenlose Probestunde, dann jede Woche. Im Februar spielen alle beim Hauskonzert.</p>
<p class="preis"><span data-preis>39 €</span><span class="label">im Monat</span></p>
<div class="gruppe">
<p class="label" id="l-instrument">Instrument</p>
<div class="tabs" role="radiogroup" aria-labelledby="l-instrument" data-tabs>
<button class="tab drueck" type="button" role="radio" aria-checked="true" data-instrument="klavier" data-raum="Raum 2 · Flügelzimmer">Klavier</button>
<button class="tab drueck" type="button" role="radio" aria-checked="false" data-instrument="gitarre" data-raum="Raum 5 · Saitenzimmer">Gitarre</button>
<button class="tab drueck" type="button" role="radio" aria-checked="false" data-instrument="trommeln" data-raum="Keller · Trommelraum">Trommeln</button>
</div>
</div>
<div class="abos" role="radiogroup" aria-label="Unterricht" data-abos>
<button class="abo drueck gross" type="button" role="radio" aria-checked="true" data-preis="39 €"><strong>In der Gruppe</strong><span>39 € <i>· bis 4 Kinder</i></span></button>
<button class="abo drueck gross" type="button" role="radio" aria-checked="false" data-preis="64 €"><strong>Einzeln</strong><span>64 € <i>· 30 Min.</i></span></button>
</div>
<div class="kauf">
<div class="menge" aria-label="Anzahl Kinder">
<button class="rund drueck" type="button" data-minus aria-label="Ein Kind weniger" disabled><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
<output data-menge aria-live="polite">1</output>
<button class="rund drueck" type="button" data-plus aria-label="Ein Kind mehr"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8h10M8 3v10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg></button>
</div>
<button class="cta drueck" type="button" data-cta>Probestunde buchen <small>kostenlos</small></button>
<button class="merken rund drueck" type="button" data-merken aria-pressed="false" aria-label="Kurs merken"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6.5 4.5h11v15.5L12 16.2 6.5 20Z" stroke-linejoin="round"/></svg></button>
</div>
<p class="meldung" data-meldung role="status"></p>
</div>
</main>
<footer class="steuer">
<div class="schalter" data-varianten aria-label="Variante">
<button type="button" aria-pressed="true" data-v="skalieren">Skalieren</button>
<button type="button" aria-pressed="false" data-v="einsinken">Einsinken</button>
<button type="button" aria-pressed="false" data-v="squash">Squash</button>
<button type="button" aria-pressed="false" data-zeitlupe>Zeitlupe ×4</button>
</div>
<p class="mono werte" data-werte></p>
</footer>
<div class="finger" data-finger aria-hidden="true"><i></i></div>
<script>
(() => {
const html = document.documentElement, body = document.body;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
// ---------- Druck-Zustand: Klasse statt nur :active ----------
// pointerdown setzt, pointerup/-cancel/-leave lösen. Ein sehr kurzer Tipp bleibt
// mindestens EIN_MS gedrückt, damit das Einfedern zu sehen ist.
const EIN_MS = 100, VIBRATION_MS = 10;
const faktor = () => html.classList.contains('zeitlupe') ? 4 : 1;
const seit = new WeakMap();
function druecken(el) { if (el.disabled) return; seit.set(el, performance.now()); el.classList.add('gedrueckt'); }
function loesen(el) {
if (!el.classList.contains('gedrueckt')) return;
const rest = EIN_MS * faktor() - (performance.now() - (seit.get(el) || 0));
if (rest > 0) setTimeout(() => el.classList.remove('gedrueckt'), rest);
else el.classList.remove('gedrueckt');
}
$$('.drueck').forEach(el => {
el.addEventListener('pointerdown', e => {
if (e.button !== 0) return;
druecken(el);
if (e.pointerType === 'touch' && navigator.vibrate) navigator.vibrate(VIBRATION_MS); // Android; iOS ignoriert
});
['pointerup', 'pointercancel', 'pointerleave'].forEach(t => el.addEventListener(t, () => loesen(el)));
// Tastatur: Leertaste und Enter drücken sichtbar mit
el.addEventListener('keydown', e => { if ((e.key === ' ' || e.key === 'Enter') && !e.repeat) druecken(el); });
el.addEventListener('keyup', e => { if (e.key === ' ' || e.key === 'Enter') loesen(el); });
el.addEventListener('blur', () => el.classList.remove('gedrueckt'));
});
document.addEventListener('touchstart', () => {}, { passive: true }); // iOS: :active zuverlässig
// ---------- Was die Knöpfe tun ----------
const MAX_KINDER = 4;
let menge = 1, korb = 0, meldUhr = 0;
const mengeEl = $('[data-menge]'), minus = $('[data-minus]'), plus = $('[data-plus]');
function setzeMenge(n) { menge = Math.max(1, Math.min(MAX_KINDER, n)); mengeEl.textContent = menge; minus.disabled = menge <= 1; plus.disabled = menge >= MAX_KINDER; }
plus.addEventListener('click', () => setzeMenge(menge + 1));
minus.addEventListener('click', () => setzeMenge(menge - 1));
const plakat = $('[data-plakat]'), raum = $('[data-raum]');
function radio(gruppe) {
$$(gruppe + ' [role=radio]').forEach(b => b.addEventListener('click', () => {
$$(gruppe + ' [role=radio]').forEach(x => x.setAttribute('aria-checked', x === b));
if (b.dataset.preis) $('[data-preis]').textContent = b.dataset.preis;
if (b.dataset.instrument) { plakat.dataset.instrument = b.dataset.instrument; raum.textContent = b.dataset.raum; }
}));
}
radio('[data-tabs]'); radio('[data-abos]');
const merken = $('[data-merken]');
merken.addEventListener('click', () => merken.setAttribute('aria-pressed', merken.getAttribute('aria-pressed') !== 'true'));
const korbEl = $('[data-korb]'), meldung = $('[data-meldung]');
$('[data-cta]').addEventListener('click', () => {
korb += menge;
korbEl.querySelector('b').textContent = korb;
korbEl.setAttribute('aria-label', `Anmeldungen, ${korb}`);
korbEl.classList.add('neu');
const instrument = $('[data-tabs] [aria-checked=true]').textContent;
meldung.textContent = `${menge} ${menge > 1 ? 'Kinder' : 'Kind'} · Probestunde ${instrument} · angemeldet`;
clearTimeout(meldUhr);
meldUhr = setTimeout(() => { meldung.textContent = ''; korbEl.classList.remove('neu'); }, 2400);
});
korbEl.addEventListener('click', () => {});
// ---------- Varianten und Werte ----------
const WERTE = {
skalieren: 'scale 0.96 · Karte 0.98',
einsinken: 'translateY 2px · Schatten 10 → 3px',
squash: 'scale 1.02 / 0.96 · Karte 1.01 / 0.98'
};
function werte() {
const v = body.dataset.variante, f = faktor();
$('[data-werte]').textContent = reduziert()
? 'reduziert · brightness 0.88 · 80 ms linear'
: `${WERTE[v]} · ein ${EIN_MS * f} ms cubic-bezier(0.45, 0.02, 0.09, 0.98) · aus ${500 * f} ms Feder linear()`;
}
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)));
$('[data-zeitlupe]').addEventListener('click', e => {
html.classList.toggle('zeitlupe');
e.currentTarget.setAttribute('aria-pressed', html.classList.contains('zeitlupe'));
werte();
});
werte();
new MutationObserver(werte).observe(html, { attributes: true, attributeFilter: ['class'] });
// ---------- Vorführung: ein gedachter Finger tippt sich durch die Anmeldung ----------
const finger = $('[data-finger]');
const ABLAUF = [
['[data-tabs] .tab:nth-child(2)'], ['[data-plus]'], ['[data-plus]'], ['[data-abos] .abo:nth-child(2)'],
['[data-cta]', 'cta'], ['[data-merken]'],
['[data-tabs] .tab:nth-child(3)'], ['[data-minus]'], ['[data-minus]'], ['[data-abos] .abo:nth-child(1)'],
['[data-cta]', 'cta'], ['[data-merken]', 'variante']
];
const REIHE = ['skalieren', 'einsinken', 'squash'];
let uhr = 0, schritt = 0, runde = 0, aktiv = false;
function warte(fn, ms) { uhr = setTimeout(fn, ms); }
function zielVon(el) { const r = el.getBoundingClientRect(); return [r.left + r.width * (el.matches('.cta') ? .4 : .5), r.top + r.height * .56]; }
function naechster() {
if (!aktiv) return;
const [sel, art] = ABLAUF[schritt];
const el = $(sel);
const [x, y] = zielVon(el);
finger.style.transform = `translate(${x}px, ${y}px)`;
warte(() => {
// drücken
druecken(el); finger.classList.add('an'); finger.firstElementChild.style.scale = '.8';
const halten = (art === 'cta' ? 260 : 170) * faktor();
warte(() => {
el.classList.remove('gedrueckt'); finger.classList.remove('an'); finger.firstElementChild.style.scale = '';
el.click();
schritt = (schritt + 1) % ABLAUF.length;
if (art === 'variante') { runde++; variante(REIHE[runde % REIHE.length]); }
warte(naechster, art === 'cta' ? 900 : 520);
}, halten);
}, reduziert() ? 260 : 720);
}
function grundzustand() {
setzeMenge(1); korb = 0; korbEl.querySelector('b').textContent = '0'; korbEl.classList.remove('neu'); meldung.textContent = '';
korbEl.setAttribute('aria-label', 'Anmeldungen, 0');
$$('[role=radio]').forEach(b => b.setAttribute('aria-checked', b.matches(':first-child')));
$('[data-preis]').textContent = '39 €'; merken.setAttribute('aria-pressed', 'false');
plakat.dataset.instrument = 'klavier'; raum.textContent = 'Raum 2 · Flügelzimmer';
$$('.gedrueckt').forEach(el => el.classList.remove('gedrueckt'));
}
function vorfuehren() {
pause(); grundzustand();
html.classList.add('vorfuehrung');
schritt = 0; runde = 0; variante('skalieren');
const r = $('[data-cta]').getBoundingClientRect();
finger.style.transition = 'none';
finger.style.transform = `translate(${r.right + 40}px, ${innerHeight - 40}px)`;
finger.offsetWidth; finger.style.transition = '';
aktiv = true; warte(naechster, 200);
}
function pause() { aktiv = false; clearTimeout(uhr); }
function weiter() { if (!aktiv && html.classList.contains('vorfuehrung')) { aktiv = true; naechster(); } }
function neustart() { pause(); html.classList.remove('vorfuehrung'); finger.classList.remove('an'); grundzustand(); variante(body.dataset.variante); }
})();
</script>
</body>
</html>