Menü-Button-Morph: Burger zu X und Label-Tausch „Menu/Close“
Der Menü-Button zeigt seinen Zustand selbst: Linien drehen zum X, das Label rollt von „Menü“ zu „Schließen“.
Der Menü-Button trägt ein Textlabel („Menü“) neben zwei oder drei Linien. Beim Öffnen drehen die äußeren Linien zum X (+45° und −45°), eine mittlere Linie zieht sich zusammen. Gleichzeitig rollt das Label in einer einzeiligen Maske nach oben: „Menü“ fährt hinaus, „Schließen“ kommt von unten nach. Bei Hover rücken die Linien leicht auseinander. Optional steht neben „Schließen“ ein Kürzel-Hinweis [Esc].
Der Zustand wechselt genau dort, wo geklickt wurde. Das Wort sagt eindeutiger als jedes Icon, was der nächste Klick tut, und wirkt dabei editorial.
Wann einsetzen
Bei jeder Overlay- oder Drawer-Navigation, besonders wenn die Seite sonst wenig Bedienelemente zeigt.
Wann nicht
Wenn die Navigation dauerhaft sichtbar ist. Dann braucht es keinen Burger.
Varianten
- Zwei Linien zum X
- Drei Linien: Mittellinie verschwindet, dazu Kürzel-Hinweis „Schließen [Esc]“ (basement.studio)
- Kreis-Button, der beim Öffnen wächst und zur Menüfläche wird
- Label-Roll allein, ohne Linien
- Lottie-Animation statt CSS
Umsetzung
<button aria-expanded="false" aria-controls="menue"><span class="wort"><span class="rolle"><span>Menü</span><span aria-hidden="true">Schließen</span></span></span><span class="striche" aria-hidden="true"><i></i><i></i></span></button>. .wort ist eine Zeile hoch (height: 1.25em; overflow: hidden), .rolle stapelt beide Wörter als Grid und verschiebt sich geöffnet um translateY(−1.25em). Beide Wörter teilen eine Spalte, so bleibt die Breite stabil. Linien absolut zentriert, geschlossen um ±Abstand versetzt, geöffnet translate 0 und rotate(±45deg). Die Kurve cubic-bezier(.65,.05,0,1) gilt für Linien (400 ms) und Label (450 ms). Den Zustand nur über aria-expanded steuern. Das zweite Wort ist aria-hidden, damit der Name nicht doppelt gelesen wird; der Name bleibt „Menü“, aria-expanded meldet den Zustand.
Robust umsetzen
Telefon. Trefferfläche mindestens 44 × 44 px. Das Label darf mobil entfallen, das Icon bleibt. Hover-Effekte nur unter @media (hover: hover).
Ohne Bewegung. Kein Roll, das Label wechselt sofort. Das X erscheint ohne Drehung. Die Menüfläche blendet nur kurz ein.
Barrierefrei. Echter <button> mit aria-expanded und aria-controls. Sichtbarer Fokusring. Escape schließt und setzt den Fokus zurück auf den Button. Die geschlossene Menüfläche ist inert.
Code
<!DOCTYPE html>
<!--
Menü-Button-Morph: Burger zu X und Label-Tausch „Menu/Close“
Fundus · Navigation · https://cecodesign.de/lab/fundus/navigation/menue-icon-und-label-morph/
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>Menü-Button-Morph: Burger zu X und Label-Tausch · 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: 'Special Gothic'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/special-gothic:vf@latest/latin-standard-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 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 handwerk-werkstatt-klartext: Materialfarben, ein Signalton nur für Anfrage und Anruf */
--grund: #efedea; --zement: #d3cdc2; --holz: #c3b4a5; --salbei: #bdc1a9; --tinte: #1a1a1a;
--leise: rgba(26, 26, 26, .72); --linie: rgba(26, 26, 26, .16);
--signal: #fb6100;
/* Menü-Button: gemessene Kurve aus der Osmo Side Navigation */
--kurve: cubic-bezier(0.65, 0.05, 0, 1);
--icon: 400ms; /* Linien drehen */
--roll: 450ms; /* Label rollt */
--versatz: 50ms; /* untere Linie folgt */
--auf: 500ms; /* Fläche öffnet */
--zu: 280ms; /* Fläche schließt, schneller */
--zu-kurve: cubic-bezier(0.4, 0, 1, 1);
--ui: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--knopf-gr: 15px;
--rand: clamp(16px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; overflow-x: hidden; }
a { color: inherit; text-decoration: none; }
.gewerk { font: 500 13px/1.2 'Special Gothic', 'Arial Narrow', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.mono { font: 500 11px/1.5 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
/* ---------- Kopf ---------- */
.kopf {
position: sticky; top: 0; z-index: 30;
display: flex; align-items: center; gap: 24px;
padding: 18px var(--rand); min-height: 84px;
}
.marke { display: flex; align-items: center; gap: 12px; margin-right: auto; }
.marke svg { width: 30px; height: 30px; flex: none; }
.marke b { display: block; font: 700 21px/1 'Special Gothic', sans-serif; font-stretch: 118%; letter-spacing: .02em; }
.marke small { display: block; margin-top: 4px; color: var(--leise); }
.kopf-info { color: var(--leise); }
.anruf {
display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 18px 0 14px;
background: var(--signal); color: var(--tinte); border-radius: 999px;
font: 700 15px/1 'Special Gothic', sans-serif; letter-spacing: .01em; font-variant-numeric: tabular-nums;
transition: background-color var(--ui), scale var(--ui);
}
.anruf svg { width: 18px; height: 18px; }
.anruf:active { scale: .96; }
/* ---------- Der Menü-Button ---------- */
.menueknopf {
--a: 4px; /* halber Linienabstand, zwei Linien */
--lw: 1.75em; /* Linienlänge */
display: inline-flex; align-items: center; gap: .8em;
min-height: 44px; min-width: 44px; padding: 0 2px 0 10px;
border: 0; background: none; color: var(--tinte); cursor: pointer;
font: 600 var(--knopf-gr)/1 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .06em; text-transform: uppercase;
border-radius: 8px; transition: color var(--icon) var(--kurve), scale var(--ui);
-webkit-tap-highlight-color: transparent;
}
.menueknopf:active, .menueknopf.gedrueckt { scale: .96; }
.menueknopf:focus-visible { outline: 2px solid currentColor; outline-offset: 4px; }
/* Label: zwei Wörter untereinander in einer Maske, eine Zeile hoch */
.wort { display: block; height: 1.25em; overflow: hidden; }
.rolle { display: grid; grid-template-rows: 1.25em 1.25em; justify-items: end; align-items: center; transition: translate var(--roll) var(--kurve); }
.rolle span { display: inline-flex; align-items: center; gap: .5em; white-space: nowrap; }
.rolle kbd { display: none; font: 500 .62em/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .06em; padding: .3em .45em .25em; border: 1px solid currentColor; border-radius: 4px; opacity: .7; }
.offen .rolle { translate: 0 -1.25em; }
/* Linien */
.striche { position: relative; display: block; width: var(--lw); height: 1.4em; flex: none; }
.striche i {
position: absolute; left: 50%; top: 50%; width: var(--lw); height: 2px; margin: -1px 0 0 calc(var(--lw) / -2);
background: currentColor; border-radius: 1px;
transition: translate var(--icon) var(--kurve), rotate var(--icon) var(--kurve), scale var(--icon) var(--kurve), opacity var(--icon) var(--kurve);
}
.striche i:nth-child(1) { translate: 0 calc(var(--a) * -1); }
.striche i:nth-child(2) { scale: 0 1; opacity: 0; }
.striche i:nth-child(3) { translate: 0 var(--a); transition-delay: 0s, var(--versatz), 0s, 0s; }
@media (hover: hover) { .menueknopf:hover { --a: 5.5px; } }
.menueknopf.schwebt { --a: 5.5px; }
.offen .striche i:nth-child(1) { translate: 0 0; rotate: 45deg; }
.offen .striche i:nth-child(3) { translate: 0 0; rotate: -45deg; }
/* Variante: drei Linien, Mittellinie zieht sich zusammen, Esc-Hinweis */
[data-variante="drei"] .menueknopf { --a: 7px; }
[data-variante="drei"] .menueknopf.schwebt { --a: 8.5px; }
@media (hover: hover) { [data-variante="drei"] .menueknopf:hover { --a: 8.5px; } }
[data-variante="drei"] .striche i:nth-child(2) { scale: 1 1; opacity: 1; }
[data-variante="drei"].offen .striche i:nth-child(2) { scale: 0 1; opacity: 0; }
[data-variante="drei"] .rolle kbd { display: inline-block; }
/* Variante: Kreis, der zur Menüfläche wächst */
[data-variante="kreis"] .menueknopf { --lw: 1.2em; --a: 3.5px; gap: 1em; }
[data-variante="kreis"] .menueknopf.schwebt { --a: 5px; }
@media (hover: hover) { [data-variante="kreis"] .menueknopf:hover { --a: 5px; } }
[data-variante="kreis"] .striche { width: 52px; height: 52px; color: var(--grund); }
[data-variante="kreis"] .striche::before { content: ""; position: absolute; inset: 0; border-radius: 50%; background: var(--tinte); transition: scale var(--ui); }
[data-variante="kreis"].offen .menueknopf { color: var(--grund); }
/* ---------- Menüfläche ---------- */
.schleier { position: fixed; inset: 0; z-index: 10; background: rgba(26, 26, 26, .38); opacity: 0; pointer-events: none; transition: opacity var(--zu) var(--zu-kurve); }
.offen .schleier { opacity: 1; pointer-events: auto; transition: opacity var(--auf) var(--kurve); }
.menue {
--p-grund: var(--salbei); --p-text: var(--tinte); --p-leise: rgba(26, 26, 26, .66); --p-linie: rgba(26, 26, 26, .2);
position: fixed; top: 0; right: 0; z-index: 20; width: min(560px, 100vw); height: 100svh;
background: var(--p-grund); color: var(--p-text);
padding: 112px var(--rand) 28px clamp(24px, 3.4vw, 48px);
display: flex; flex-direction: column; overflow-y: hidden; /* zu: inert und ohne Scrollbereich, offen: scrollt */
clip-path: inset(0 0 0 100%);
transition: clip-path var(--zu) var(--zu-kurve);
}
.offen .menue { overflow-y: auto; clip-path: inset(0 0 0 0); transition: clip-path var(--auf) var(--kurve); }
[data-variante="kreis"] .menue { --p-grund: var(--tinte); --p-text: var(--grund); --p-leise: rgba(239, 237, 234, .62); --p-linie: rgba(239, 237, 234, .18); clip-path: circle(26px at var(--kx, 90%) var(--ky, 50px)); }
[data-variante="kreis"].offen .menue { clip-path: circle(var(--kr, 150%) at var(--kx, 90%) var(--ky, 50px)); }
.menue-kopf { display: flex; justify-content: space-between; color: var(--p-leise); padding-bottom: 14px; border-bottom: 1px solid var(--p-linie); }
.menue-liste { list-style: none; }
.menue-liste a {
display: grid; grid-template-columns: 2.6em 1fr; column-gap: 8px; row-gap: 4px;
padding: 16px 0 17px; border-bottom: 1px solid var(--p-linie);
}
.menue-liste .nr { grid-row: span 2; padding-top: .55em; color: var(--p-leise); }
.menue-liste .titel { font: 700 clamp(26px, 2.6vw, 34px)/1.05 'Special Gothic', sans-serif; letter-spacing: -.01em; }
.menue-liste .text { font-size: 15px; line-height: 1.45; color: var(--p-leise); }
.menue-liste a:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
@media (hover: hover) { .menue-liste a .titel { transition: translate var(--ui); } .menue-liste a:hover .titel { translate: 6px 0; } }
.menue-fuss { margin-top: auto; padding-top: 28px; display: grid; grid-template-columns: 1fr 1fr; gap: 20px 24px; font-size: 15px; line-height: 1.5; }
.menue-fuss .gewerk { color: var(--p-leise); margin-bottom: 6px; }
.menue-cta { grid-column: 1 / -1; display: flex; gap: 10px; flex-wrap: wrap; }
.knopf {
display: inline-flex; align-items: center; justify-content: center; min-height: 48px; padding: 0 22px; border-radius: 999px;
font: 700 15px/1 'Special Gothic', sans-serif; letter-spacing: .01em; border: 1.5px solid currentColor;
transition: scale var(--ui), background-color var(--ui);
}
.knopf.signal { background: var(--signal); border-color: var(--signal); color: var(--tinte); }
.knopf:active { scale: .96; }
/* Einträge: 8 px Versatz plus Deckkraft, gestaffelt */
.menue [data-stufe] { opacity: 0; translate: 0 8px; transition: opacity 150ms linear, translate 0s 150ms; }
.offen .menue [data-stufe] { opacity: 1; translate: 0 0; transition: opacity var(--icon) var(--kurve), translate var(--icon) var(--kurve); transition-delay: calc(140ms + var(--i, 0) * 40ms); }
/* ---------- Seite dahinter ---------- */
.held {
display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); gap: clamp(28px, 4vw, 64px); align-items: center;
padding: clamp(12px, 3vw, 40px) var(--rand) clamp(28px, 4vw, 48px);
}
.held-text .gewerk { color: var(--leise); margin-bottom: 18px; }
h1 { font: 700 clamp(2.25rem, 1.5rem + 3vw, 3.5rem)/1.05 'Special Gothic', sans-serif; font-stretch: 100%; letter-spacing: -.01em; text-wrap: balance; max-width: 13em; }
.held-text p.lead { margin-top: 20px; font-size: 1.0625rem; line-height: 1.55; max-width: 34em; text-wrap: pretty; }
.held-cta { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 28px; }
.zahlen { display: grid; grid-template-columns: repeat(4, auto); justify-content: start; gap: 8px 36px; list-style: none; margin-top: clamp(28px, 4vw, 48px); padding-top: 18px; border-top: 1px solid var(--linie); }
.zahlen b { display: block; font: 700 28px/1 'Special Gothic', sans-serif; font-variant-numeric: tabular-nums; }
.zahlen span { display: block; margin-top: 6px; color: var(--leise); }
.held-bild { background: var(--zement); border-radius: 6px; padding: clamp(14px, 2vw, 26px); }
.held-bild svg { display: block; width: 100%; height: auto; }
.held-bild figcaption { display: flex; justify-content: space-between; gap: 12px; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--linie); color: var(--leise); }
/* ---------- Steuerung der Demo ---------- */
.steuer { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px 24px; padding: 14px var(--rand) 18px; border-top: 1px solid var(--linie); }
.werte { max-width: 70ch; }
.werte b { font-weight: 500; color: var(--tinte); }
.varianten { display: flex; gap: 4px; flex-wrap: wrap; }
.varianten button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 999px; padding: 7px 12px 6px; cursor: pointer; transition: color var(--ui), border-color var(--ui), background-color var(--ui); }
.varianten button[aria-pressed="true"] { color: var(--grund); background: var(--tinte); border-color: var(--tinte); }
.varianten button:focus-visible { outline: 2px solid var(--tinte); outline-offset: 2px; }
/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 40; width: 20px; height: 20px; pointer-events: none; display: none; transition: transform 700ms var(--kurve); }
.vorfuehrung .zeiger { display: block; }
/* Vorschau 960 × 720: nur die Bühne, Knopf größer */
.vorschau body { overflow: hidden; height: 100svh; }
.vorschau .steuer { display: none; }
:root.vorschau { --knopf-gr: 22px; }
.vorschau .kopf { min-height: 104px; }
.vorschau .menue { padding-top: 132px; }
.vorschau .menue-liste a { padding: 12px 0 13px; }
.vorschau .menue-fuss { padding-top: 20px; }
.vorschau .leiste { display: none; }
/* Untere Leiste (schmal): Anrufen und Anfrage immer erreichbar */
.leiste { display: none; }
/* ---------- Schmal und Touch ---------- */
@media (max-width: 1100px) { .kopf-info { display: none; } }
@media (max-width: 860px) {
.held { grid-template-columns: 1fr; }
.zahlen { grid-template-columns: repeat(2, auto); }
}
@media (max-width: 560px) {
.kopf { gap: 10px; min-height: 72px; padding-top: 12px; padding-bottom: 12px; }
.marke small { display: none; }
.kopf .anruf { display: none; }
.leiste { position: fixed; left: 0; right: 0; bottom: 0; z-index: 8; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 10px 16px calc(10px + env(safe-area-inset-bottom)); background: var(--grund); border-top: 1px solid var(--linie); }
.leiste .knopf { min-height: 48px; }
body { padding-bottom: 70px; }
.menue { padding-top: 92px; }
.menue-fuss { grid-template-columns: 1fr; }
.held-bild figcaption span:last-child { display: none; }
.werte { font-size: 10px; }
}
@media (hover: none) {
.menueknopf { padding-left: 14px; }
}
/* ---------- Reduzierte Bewegung: kein Roll, keine Drehung, nur kurzes Einblenden der Fläche ---------- */
.reduziert .rolle, .reduziert .striche i, .reduziert .menueknopf, .reduziert .menue [data-stufe], .reduziert .zeiger { transition: none !important; }
.reduziert .menue, .reduziert[data-variante] .menue { clip-path: none; opacity: 0; visibility: hidden; transition: opacity 150ms linear, visibility 0s 150ms; }
.reduziert.offen .menue { opacity: 1; visibility: visible; transition: opacity 150ms linear; }
.reduziert .menue [data-stufe] { opacity: 1; translate: none; }
.reduziert .schleier { transition: opacity 150ms linear; }
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<header class="kopf">
<a class="marke" href="#start" aria-label="Tischlerei Tönjes, Startseite">
<svg viewBox="0 0 30 30" aria-hidden="true"><rect width="30" height="30" rx="4" fill="#1a1a1a"/><path d="M8 7h14l-3.5 7.5L22 23H8l3.5-8.5Z" fill="#c3b4a5"/></svg>
<span><b>TÖNJES</b><small class="gewerk">Tischlerei · Oldenburg</small></span>
</a>
<p class="kopf-info gewerk">Meisterbetrieb seit 1987 · Werkstatt Donnerschwee</p>
<a class="anruf" href="#anrufen" aria-label="Anrufen: 0441 998 76 0">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6.6 3.5 9 3l1.6 4.3-2 1.5a12 12 0 0 0 6.6 6.6l1.5-2L21 15l-.5 2.4A2.4 2.4 0 0 1 18 19.5 15.5 15.5 0 0 1 4.5 6 2.4 2.4 0 0 1 6.6 3.5Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>
<span>0441 998 76 0</span>
</a>
<button class="menueknopf" type="button" aria-expanded="false" aria-controls="menue" data-knopf>
<span class="wort"><span class="rolle"><span>Menü</span><span aria-hidden="true">Schließen <kbd>Esc</kbd></span></span></span>
<span class="striche" aria-hidden="true"><i></i><i></i><i></i></span>
</button>
</header>
<div class="schleier" data-schleier aria-hidden="true"></div>
<nav class="menue" id="menue" aria-label="Hauptmenü" data-menue inert>
<div class="menue-kopf gewerk" data-stufe style="--i:0"><span>Seiten</span><span>05</span></div>
<ol class="menue-liste">
<li data-stufe style="--i:1"><a href="#leistungen"><span class="nr gewerk">01</span><span class="titel">Leistungen</span><span class="text">Einbauschränke, Küchen, Treppen, Holzfenster und Haustüren.</span></a></li>
<li data-stufe style="--i:2"><a href="#referenzen"><span class="nr gewerk">02</span><span class="titel">Referenzen</span><span class="text">Über 600 Projekte zwischen Ammerland und Wesermarsch.</span></a></li>
<li data-stufe style="--i:3"><a href="#ablauf"><span class="nr gewerk">03</span><span class="titel">So läuft ein Auftrag</span><span class="text">Aufmaß vor Ort, Zeichnung, Festpreis, Fertigung, Einbau.</span></a></li>
<li data-stufe style="--i:4"><a href="#werkstatt"><span class="nr gewerk">04</span><span class="titel">Werkstatt & Team</span><span class="text">14 Leute, zwei Meister, drei Azubis. Wir bilden aus.</span></a></li>
<li data-stufe style="--i:5"><a href="#kontakt"><span class="nr gewerk">05</span><span class="titel">Kontakt</span><span class="text">Anfrage mit Foto und Maßen, Antwort binnen zwei Werktagen.</span></a></li>
</ol>
<div class="menue-fuss">
<div data-stufe style="--i:6"><p class="gewerk">Werkstatt</p><p>Donnerschweer Straße 212<br>26123 Oldenburg</p></div>
<div data-stufe style="--i:6"><p class="gewerk">Erreichbar</p><p>Mo–Fr 7–16 Uhr<br>Notdienst Türen: 24 h</p></div>
<div class="menue-cta" data-stufe style="--i:7"><a class="knopf signal" href="#anrufen">Anrufen</a><a class="knopf" href="#anfrage">Anfrage stellen</a></div>
</div>
</nav>
<main class="held" id="start">
<div class="held-text">
<p class="gewerk">Tischlerei · Innenausbau · Oldenburg und Ammerland</p>
<h1>Möbel, die in Ihre Wände passen.</h1>
<p class="lead">Wir messen auf, bauen in unserer Werkstatt in Donnerschwee und montieren selbst. Ein Ansprechpartner vom ersten Termin bis zur letzten Fuge.</p>
<div class="held-cta"><a class="knopf signal" href="#anfrage">Anfrage stellen</a><a class="knopf" href="#anrufen">0441 998 76 0</a></div>
<ul class="zahlen">
<li><b>1987</b><span class="gewerk">gegründet</span></li>
<li><b>14</b><span class="gewerk">Leute</span></li>
<li><b>600+</b><span class="gewerk">Projekte</span></li>
<li><b>40 km</b><span class="gewerk">Einzugsgebiet</span></li>
</ul>
</div>
<figure class="held-bild">
<svg viewBox="0 0 600 470" role="img" aria-label="Werkzeichnung: Einbauschrank unter einer Dachschräge, Ansicht">
<!-- Raum: Wand mit Dachschräge -->
<path d="M50 30H300L560 220V420H50Z" fill="#e6e2dc"/>
<path d="M50 30H300L560 220V420" fill="none" stroke="#1a1a1a" stroke-width="1.6"/>
<path d="M30 420H580" stroke="#1a1a1a" stroke-width="2.4"/>
<!-- Hochschrank links, zwei Türen -->
<g stroke="#1a1a1a" stroke-width="1.2">
<rect x="70" y="50" width="70" height="355" fill="#bdc1a9"/>
<rect x="142" y="50" width="70" height="355" fill="#bdc1a9"/>
<path d="M132 210v40M150 210v40" stroke-width="2.2" stroke-linecap="round"/>
<!-- offenes Regal in Eiche -->
<rect x="214" y="50" width="86" height="355" fill="#c3b4a5"/>
<path d="M214 135h86M214 220h86M214 305h86" />
</g>
<!-- Inhalt Regal -->
<g fill="#1a1a1a" opacity=".78">
<rect x="222" y="98" width="7" height="37"/><rect x="231" y="104" width="6" height="31"/><rect x="239" y="94" width="8" height="41"/>
<rect x="270" y="112" width="22" height="23" rx="2" fill="#efedea" stroke="#1a1a1a"/>
<rect x="222" y="196" width="34" height="24" fill="#efedea" stroke="#1a1a1a"/>
<rect x="262" y="186" width="7" height="34"/><rect x="271" y="190" width="6" height="30"/>
<circle cx="282" cy="292" r="12" fill="none" stroke="#1a1a1a"/>
<rect x="222" y="268" width="40" height="37" fill="#efedea" stroke="#1a1a1a"/>
</g>
<!-- Schrank unter der Schräge: drei Türen, Oberkante folgt der Schräge -->
<g stroke="#1a1a1a" stroke-width="1.2" fill="#bdc1a9">
<path d="M302 52L380 109V318H302Z"/>
<path d="M382 110L460 167V318H382Z"/>
<path d="M462 168L540 225V318H462Z"/>
<rect x="302" y="320" width="118" height="85"/>
<rect x="422" y="320" width="118" height="85"/>
<path d="M370 250v34M392 250v34M450 260v24M472 260v24M341 362h40M461 362h40" stroke-width="2.2" stroke-linecap="round"/>
</g>
<!-- Sockel -->
<rect x="70" y="405" width="470" height="15" fill="#1a1a1a" opacity=".86"/>
<!-- Bemaßung -->
<g stroke="#1a1a1a" stroke-width="1" fill="none">
<path d="M70 446H540M70 439v14M540 439v14M300 442v8"/>
<path d="M28 50V405M21 50h14M21 405h14"/>
<path d="M410 128a40 40 0 0 1 22 -8" opacity=".7"/>
</g>
<g font-family="'Special Gothic', 'Arial Narrow', sans-serif" font-size="13" font-weight="500" fill="#1a1a1a" letter-spacing=".5" style="font-stretch: 75%">
<text x="185" y="466" text-anchor="middle">2.300</text>
<text x="420" y="466" text-anchor="middle">2.380</text>
<text x="16" y="230" text-anchor="middle" transform="rotate(-90 16 230)">2.360</text>
<text x="440" y="116">36°</text>
</g>
</svg>
<figcaption class="gewerk"><span>Einbauschrank Dachgeschoss, Bloherfelde</span><span>Ansicht M 1:25 · Eiche, Fronten Salbei lackiert</span></figcaption>
</figure>
</main>
<div class="steuer">
<p class="mono werte" data-werte></p>
<div class="varianten mono" role="group" aria-label="Variante">
<button type="button" data-v="zwei" aria-pressed="true">Zwei Linien</button>
<button type="button" data-v="drei" aria-pressed="false">Drei Linien + Esc</button>
<button type="button" data-v="kreis" aria-pressed="false">Kreis wird Fläche</button>
</div>
</div>
<div class="leiste"><a class="knopf signal" href="#anrufen">Anrufen</a><a class="knopf" href="#anfrage">Anfrage</a></div>
<svg class="zeiger" data-zeiger viewBox="0 0 18 18" aria-hidden="true"><path d="M2 1.5 15.5 9.2l-6 1.3-2.9 5.6Z" fill="#1a1a1a" stroke="#efedea" stroke-width="1.2" stroke-linejoin="round"/></svg>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const knopf = document.querySelector('[data-knopf]');
const menue = document.querySelector('[data-menue]');
const schleier = document.querySelector('[data-schleier]');
const werte = document.querySelector('[data-werte]');
const zeiger = document.querySelector('[data-zeiger]');
const varianten = [...document.querySelectorAll('[data-v]')];
const WERTE = {
zwei: 'Kurve <b>cubic-bezier(.65,.05,0,1)</b> · Linien <b>400 ms</b>, ±45°, untere <b>+50 ms</b> · Label-Roll <b>450 ms</b>, 100 % Zeilenhöhe · Fläche auf <b>500 ms</b>, zu <b>280 ms</b> cubic-bezier(.4,0,1,1)',
drei: 'Kurve <b>cubic-bezier(.65,.05,0,1)</b> · Mittellinie scaleX 1→0 <b>400 ms</b> · äußere ±45°, untere <b>+50 ms</b> · Label-Roll <b>450 ms</b> mit Esc-Hinweis · Fläche auf <b>500 ms</b>, zu <b>280 ms</b>',
kreis: 'Kurve <b>cubic-bezier(.65,.05,0,1)</b> · Kreis <b>52 px</b> wächst per clip-path: circle() in <b>500 ms</b>, zu <b>280 ms</b> · Linien <b>400 ms</b>, ±45° · Label-Roll <b>450 ms</b>'
};
function variante(v) {
if (html.classList.contains('offen')) umschalten(false);
html.dataset.variante = v;
varianten.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
werte.innerHTML = WERTE[v];
kreisMessen();
}
varianten.forEach(b => b.addEventListener('click', () => variante(b.dataset.v)));
// Kreis-Variante: Mittelpunkt des Knopfes relativ zur Menüfläche, Radius bis zur fernsten Ecke
function kreisMessen() {
const k = knopf.querySelector('.striche').getBoundingClientRect(), m = menue.getBoundingClientRect();
const x = k.left + k.width / 2 - m.left, y = k.top + k.height / 2 - m.top;
const r = Math.hypot(Math.max(x, m.width - x), Math.max(y, m.height - y));
menue.style.setProperty('--kx', x + 'px');
menue.style.setProperty('--ky', y + 'px');
menue.style.setProperty('--kr', Math.ceil(r + 2) + 'px');
}
addEventListener('resize', kreisMessen);
// Eine Quelle der Wahrheit: aria-expanded am Knopf, gespiegelt als .offen am <html>
function umschalten(auf) {
auf = auf ?? knopf.getAttribute('aria-expanded') !== 'true';
if (auf) kreisMessen();
knopf.setAttribute('aria-expanded', String(auf));
html.classList.toggle('offen', auf);
menue.inert = !auf;
}
knopf.addEventListener('click', () => { stoppen(); umschalten(); });
schleier.addEventListener('click', () => umschalten(false));
addEventListener('keydown', e => {
if (e.key === 'Escape' && knopf.getAttribute('aria-expanded') === 'true') { umschalten(false); knopf.focus(); }
});
document.addEventListener('click', e => { const a = e.target.closest('a[href^="#"]'); if (a) e.preventDefault(); });
// ---------- Vorführung: gedachter Zeiger öffnet und schließt, in der Vorschau durch alle Varianten ----------
let lauf = 0, angehalten = false;
const warten = ms => new Promise(r => setTimeout(r, ms));
function zeigerAuf(x, y, sofort) {
zeiger.style.transition = sofort ? 'none' : '';
zeiger.style.transform = `translate(${x - 3}px, ${y - 2}px)`;
}
function knopfMitte() { const r = knopf.querySelector('.striche').getBoundingClientRect(); return [r.left + r.width * .35, r.top + r.height * .62]; }
async function klick(id) {
knopf.classList.add('gedrueckt'); await warten(150); if (id !== lauf) return;
knopf.classList.remove('gedrueckt'); umschalten();
}
async function runde(id) {
const r = reduziert();
const reihe = html.classList.contains('vorschau') ? ['zwei', 'drei', 'kreis'] : [html.dataset.variante];
let n = reihe.indexOf(html.dataset.variante); if (n < 0) n = 0;
while (id === lauf) {
if (reihe.length > 1) variante(reihe[n % reihe.length]);
n++;
zeigerAuf(innerWidth * .62, innerHeight * .78, true);
await warten(500); if (id !== lauf) return;
zeigerAuf(...knopfMitte());
await warten(750); if (id !== lauf) return;
knopf.classList.add('schwebt');
await warten(r ? 200 : 550); if (id !== lauf) return;
await klick(id); if (id !== lauf) return;
await warten(700); if (id !== lauf) return;
const erster = menue.querySelector('.menue-liste li:nth-child(2) .titel').getBoundingClientRect();
knopf.classList.remove('schwebt');
zeigerAuf(erster.left + 60, erster.top + 20);
await warten(1500); if (id !== lauf) return;
zeigerAuf(...knopfMitte());
await warten(750); if (id !== lauf) return;
knopf.classList.add('schwebt');
await warten(350); if (id !== lauf) return;
await klick(id); if (id !== lauf) return;
await warten(500); if (id !== lauf) return;
knopf.classList.remove('schwebt');
zeigerAuf(innerWidth * .62, innerHeight * .78);
await warten(900);
}
}
function stoppen() {
if (!html.classList.contains('vorfuehrung')) return;
lauf++; html.classList.remove('vorfuehrung'); knopf.classList.remove('schwebt', 'gedrueckt');
}
function vorfuehren() {
lauf++; angehalten = false;
umschalten(false); knopf.classList.remove('schwebt', 'gedrueckt');
html.classList.add('vorfuehrung');
runde(lauf);
}
function pause() { if (html.classList.contains('vorfuehrung')) { lauf++; angehalten = true; } }
function weiter() { if (angehalten) vorfuehren(); }
function neustart() {
lauf++; angehalten = false;
html.classList.remove('vorfuehrung'); knopf.classList.remove('schwebt', 'gedrueckt');
umschalten(false);
}
variante('zwei');
})();
</script>
</body>
</html>