Absenden wird Bestätigung
Die Kurzbewerbung eines Elektrobetriebs wird nach dem Absenden selbst zur Bestätigung: der rote Knopf schrumpft zum Haken, die weiße Fläche färbt sich tannengrün, Fehler lassen alle Eingaben stehen.
Kurzbewerbung, Kontakt- oder Terminanfrage mit zwei bis fünf Feldern in einer eigenen Fläche (arbeitgeber, handwerk, praxis, eventlocation).
Ein Formular durchläuft nach dem Klick auf Absenden vier Zustände: bereit, wird gesendet, gesendet oder fehlgeschlagen. Beim Senden bleibt der Knopf gleich breit, die Felder werden schreibgeschützt und behalten ihre Werte. Bei Erfolg verwandelt sich die Formularfläche in die Bestätigung: gleiche Fläche, neuer Inhalt, mit Eingangsnummer, nächsten Schritten mit Zeitangabe und einer Zusammenfassung der Angaben. Der Fokus springt auf die Überschrift der Bestätigung. Bei einem Fehler bleibt alles stehen, eine Meldung sagt, was passiert ist und was jetzt hilft, und der Knopf heißt „Erneut senden“.
Der Moment nach dem Absenden entscheidet, ob jemand dem Betrieb vertraut. Eine Fläche, die sich in die Antwort verwandelt, zeigt ohne Worte: Das Formular ist angekommen, hier ist das Ergebnis. Ein Seitenwechsel oder ein verschwindendes Formular lässt dagegen offen, ob etwas passiert ist. Wer nach einem Fehler seine Eingaben noch hat, versucht es noch einmal statt abzubrechen.
Wann einsetzen
Überall, wo ein Formular abgeschickt wird: Kontakt, Rückruf, Kurzbewerbung, Terminanfrage, Newsletter, Gutschein-Anfrage. Pflicht in allen Rezepten mit Anfrage oder Bewerbung.
Wann nicht
Wenn nach dem Absenden ein echter Folgeschritt kommt (Bezahlung, Konto anlegen): dann führt der Erfolg direkt dorthin. Nicht als Ersatz für eine Danke-Seite, wenn der Betrieb eine eigene URL für Messung oder Lesezeichen braucht; dann dieselbe Bestätigung auf der Danke-Seite. Keine Antwortzeit versprechen, die der Betrieb nicht hält.
Varianten
- Karte: Formularfläche wird zur Bestätigungskarte mit Eingangsnummer, nächsten Schritten und Angaben (Kurzbewerbung, Anfrage)
- Zeile: einzeiliges Rückruf- oder Newsletter-Formular, Knopf und Feld werden zur Bestätigungszeile
- In beiden: Fehler mit erhaltenen Eingaben und Telefon als Ausweg, Offline-Hinweis, Hinweis bei langsamer Antwort, „Angaben korrigieren“ führt zurück
Umsetzung
Ein echtes <form> mit method und action, damit es ohne JS auf eine Danke-Seite führt. Mit JS: submit abfangen, gegen Doppelklick sperren, Felder readOnly statt disabled (Werte bleiben lesbar und gehen nicht verloren), Knopf mit fester Breite und aria-disabled, Zustand am Formular als data-zustand (bereit, sendet, gesendet, fehler). fetch mit AbortController und Zeitgrenze. Bei Erfolg: document.startViewTransition(() => Bestätigung einsetzen) mit view-transition-name auf der Fläche, damit Größe und Lage gleiten; ohne View Transitions eine FLIP-Höhenanimation. Danach Fokus auf die Überschrift der Bestätigung (tabindex=-1) und eine kurze Ansage in einer aria-live=polite-Region, die schon beim Laden im DOM steht. Bei Fehler: Meldung mit role=alert über dem Knopf, Eingaben bleiben, Knopf heißt „Erneut senden“, Telefon als Ausweg. Offline (navigator.onLine false) gar nicht erst senden, sondern sagen, dass keine Verbindung besteht.
Robust umsetzen
Telefon. Die Bestätigung steht an derselben Stelle wie das Formular, die Seite springt nicht. Nach dem Morph scrollt die Überschrift in den sichtbaren Bereich, falls die Tastatur sie verdeckt hat. Knöpfe mindestens 48 px hoch, Telefonnummer als tel:-Link.
Ohne Bewegung. Kein Gleiten und kein Haken-Strich: die Bestätigung erscheint mit kurzer Überblendung, der Haken steht sofort.
Barrierefrei. Zustand nie nur über Farbe oder Spinner: Der Knopf sagt „Wird gesendet …“. Eine aria-live=polite-Region sagt „Bewerbung gesendet“, Fehler kommen mit role=alert. Fokus nach Erfolg auf die Überschrift der Bestätigung, nach Fehler bleibt er am Knopf. readOnly statt disabled, damit Screenreader die Werte weiter vorlesen. Kontrast der Meldungen mindestens 4,5 : 1.
Code
<!DOCTYPE html>
<!--
Absenden wird Bestätigung
Fundus · Mikroanimationen · https://cecodesign.de/lab/fundus/mikroanimationen/absenden-zu-bestaetigung/
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>Absenden wird Bestätigung · 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: 'Funnel Display'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/funnel-display:vf@latest/latin-wght-normal.woff2') format('woff'); font-weight: 300 800; font-display: swap; }
@font-face { font-family: 'Funnel Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/funnel-sans:vf@latest/latin-wght-normal.woff2') format('woff'); font-weight: 300 800; font-display: swap; }
/* Welt: Rezept arbeitgeber-karriere · Elektro Quast, Stadthagen (erfunden) */
:root {
--kreide: #F6F3EC; --tinte: #161616; --leise: #5A5650; --zettel: #EFE6D2; --tanne: #1D4A3A; --mohn: #C8382A;
--weiss: #FFFDF8; --linie: rgba(90, 86, 80, .28); --kreide-leise: rgba(246, 243, 236, .78);
--ein: cubic-bezier(0.16, 1, 0.3, 1); /* öffnen, Morph (token-toggle-asymmetrisch) */
--aus: cubic-bezier(0.7, 0, 0.84, 0); /* schließen */
--feder: cubic-bezier(0.34, 1.56, 0.64, 1); /* nur der Haken (token-federnd-verspielt) */
--t-morph: 280ms; --t-aus: 190ms; --t-haken: 400ms; --t-ui: 150ms;
--display: 'Funnel Display', system-ui, sans-serif; --text: 'Funnel Sans', system-ui, sans-serif;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--kreide); color: var(--tinte); font-family: var(--text); -webkit-text-size-adjust: 100%; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; }
:focus-visible { outline: 2px solid var(--tinte); outline-offset: 3px; }
.dunkel :focus-visible { outline-color: var(--kreide); }
button, input { font: inherit; color: inherit; }
.versal { font: 600 .8125rem/1.2 var(--text); letter-spacing: .08em; text-transform: uppercase; color: var(--leise); }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
/* Kopf */
.kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; flex-wrap: wrap; padding: 22px clamp(20px, 4vw, 56px); border-bottom: 1px solid var(--linie); }
.marke { font: 700 1.25rem/1 var(--display); letter-spacing: -.02em; }
.marke span { font-weight: 400; color: var(--leise); }
.kopf nav { display: flex; gap: 22px; font-size: .9375rem; }
.kopf nav a { color: inherit; text-underline-offset: 4px; }
/* Bühne */
.buehne { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: clamp(28px, 5vw, 80px); align-items: start; padding: clamp(32px, 5vw, 72px) clamp(20px, 4vw, 56px) 96px; max-width: 1320px; margin: 0 auto; }
.stelle { container-type: inline-size; }
.stelle h1 { font: 700 clamp(2.25rem, 11.2cqi, 4.75rem)/.96 var(--display); hyphens: auto; letter-spacing: -.03em; margin: 14px 0 22px; text-wrap: balance; }
.stelle > p { font-size: 1.125rem; line-height: 1.6; max-width: 34ch; }
.fakten { margin: 28px 0 0; border-top: 1px solid var(--linie); max-width: 30rem; }
.fakten div { display: grid; grid-template-columns: 7.5rem minmax(0, 1fr); gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--linie); font-size: 1rem; }
.fakten dt { color: var(--leise); }
.fakten dd { font-weight: 600; font-variant-numeric: tabular-nums; }
.abzug { position: relative; margin: 40px 0 0 8%; width: min(15rem, 62%); transform: rotate(-2.5deg); background: var(--weiss); padding: 10px 10px 12px; box-shadow: 0 1px 2px rgba(22, 22, 22, .12), 0 10px 24px -12px rgba(22, 22, 22, .3); }
.abzug img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; }
.abzug figcaption { font: italic 500 .875rem/1.35 var(--text); color: var(--leise); margin-top: 8px; }
/* Fläche: wird vom Formular zur Bestätigung */
.flaeche { view-transition-name: flaeche; background: var(--weiss); border: 1px solid var(--linie); border-radius: 2px; padding: clamp(22px, 3.2vw, 44px); position: relative; }
.flaeche.dunkel { background: var(--tanne); color: var(--kreide); border-color: var(--tanne); }
.flaeche h2 { font: 600 clamp(1.75rem, 1.3rem + 2vw, 3rem)/1.02 var(--display); letter-spacing: -.02em; text-wrap: balance; }
.flaeche h2:focus, .flaeche h2:focus-visible { outline: none; } /* nur per Skript fokussiert, nicht bedienbar */
.einleitung { font-size: 1.0625rem; line-height: 1.55; color: var(--leise); margin: 10px 0 26px; max-width: 46ch; }
.feld { margin-bottom: 20px; }
.feld label, .feld legend { display: block; font-weight: 600; font-size: 1rem; margin-bottom: 8px; }
.feld .frei { font-weight: 400; color: var(--leise); }
.feld input[type=text], .feld input[type=tel] { width: 100%; height: 52px; padding: 0 14px; background: var(--kreide); border: 1px solid rgba(90, 86, 80, .55); border-radius: 6px; font-size: 1.0625rem; transition: border-color var(--t-ui) linear, background-color var(--t-ui) linear; }
.feld input:hover { border-color: var(--tinte); }
.feld input:focus-visible { outline: 2px solid var(--tinte); outline-offset: 1px; }
.feld input[aria-invalid=true] { border-color: var(--mohn); border-width: 2px; }
.feld input:read-only { background: var(--zettel); border-style: dashed; }
.hilfe { font-size: .875rem; color: var(--leise); margin-top: 6px; }
.fehler-feld { color: #A82C20; font-size: .9375rem; font-weight: 600; margin-top: 6px; }
.fehler-feld:empty { display: none; }
fieldset { border: 0; padding: 0; min-width: 0; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chips label { margin: 0; font-weight: 500; }
.chips input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.chips span { display: inline-flex; align-items: center; height: 44px; padding: 0 16px; border: 1px solid rgba(90, 86, 80, .55); border-radius: 6px; cursor: pointer; transition: background-color var(--t-ui) linear, color var(--t-ui) linear, border-color var(--t-ui) linear; }
.chips input:checked + span { background: var(--tinte); color: var(--kreide); border-color: var(--tinte); }
.chips input:focus-visible + span { outline: 2px solid var(--tinte); outline-offset: 2px; }
.chips input:disabled + span, .gesperrt .chips span { cursor: default; opacity: .75; }
.zeile2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
/* Senden */
.senden { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 22px; margin-top: 28px; }
.knopf { view-transition-name: knopf; position: relative; display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-width: 15.5rem; height: 56px; padding: 0 24px; border: 0; border-radius: 6px; background: var(--mohn); color: #fff; font-weight: 700; font-size: 1.0625rem; cursor: pointer; overflow: hidden; transition: background-color var(--t-ui) linear; }
.knopf:hover { background: #B23125; }
.knopf:active { transform: translateY(1px); }
.knopf[aria-disabled=true] { cursor: progress; }
.knopf .pfeil { transition: transform var(--t-morph) var(--ein); }
.knopf:hover .pfeil { transform: translateX(4px); }
.knopf .lade { position: absolute; left: 0; bottom: 0; height: 3px; width: 100%; background: rgba(255, 255, 255, .85); transform: scaleX(0); transform-origin: left; }
[data-zustand=sendet] .knopf .lade { animation: laden 1.6s var(--ein) infinite; }
@keyframes laden { 0% { transform: scaleX(0); transform-origin: left; } 50% { transform: scaleX(1); transform-origin: left; } 50.01% { transform-origin: right; } 100% { transform: scaleX(0); transform-origin: right; } }
.datenschutz { font-size: .875rem; color: var(--leise); max-width: 34ch; line-height: 1.45; }
.datenschutz a { color: inherit; }
.langsam { flex-basis: 100%; font-size: .9375rem; color: var(--leise); display: none; }
[data-langsam] .langsam { display: block; animation: rein var(--t-morph) var(--ein) both; }
/* Fehlermeldung */
.meldung { display: none; grid-template-columns: auto minmax(0, 1fr); gap: 4px 14px; margin-top: 24px; padding: 16px 18px; background: var(--zettel); border-left: 4px solid var(--mohn); border-radius: 2px; font-size: 1rem; line-height: 1.5; }
.meldung.an { display: grid; animation: rein var(--t-morph) var(--ein) both; }
.meldung svg { grid-row: span 2; margin-top: 2px; }
.meldung strong { font-weight: 700; }
.meldung a { color: inherit; font-weight: 600; }
@keyframes rein { from { opacity: 0; transform: translateY(-8px); } }
.wackeln { animation: wackeln 380ms var(--ein); }
@keyframes wackeln { 20% { transform: translateX(-6px); } 45% { transform: translateX(5px); } 70% { transform: translateX(-3px); } }
/* Bestätigung */
.bestaetigung[hidden], form[hidden] { display: none; }
.b-kopf { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 18px; align-items: start; }
.haken { view-transition-name: knopf; width: 64px; height: 64px; border-radius: 50%; background: var(--mohn); box-shadow: 0 0 0 2px var(--kreide); display: grid; place-items: center; }
.haken path { stroke-dasharray: 30; stroke-dashoffset: 30; }
.zeigt .haken path { animation: haken var(--t-haken) var(--ein) 180ms forwards; }
.zeigt .haken { animation: hakenring var(--t-haken) var(--feder) 120ms both; }
@keyframes haken { to { stroke-dashoffset: 0; } }
@keyframes hakenring { from { transform: scale(.72); } }
.dunkel .versal { color: var(--kreide-leise); }
.b-text { font-size: 1.125rem; line-height: 1.55; color: var(--kreide-leise); margin-top: 10px; max-width: 44ch; }
.schritte { list-style: none; padding: 0; margin: 30px 0 0; border-top: 1px solid rgba(246, 243, 236, .22); counter-reset: s; }
.schritte li { display: grid; grid-template-columns: 2.25rem minmax(0, 1fr); gap: 4px 12px; padding: 14px 0; border-bottom: 1px solid rgba(246, 243, 236, .22); counter-increment: s; }
.schritte li::before { content: counter(s); font: 600 1.25rem/1.3 var(--display); }
.schritte b { font-weight: 700; display: block; }
.schritte span { color: var(--kreide-leise); font-size: 1rem; }
.zeigt .schritte li { animation: rein var(--t-morph) var(--ein) both; }
.zeigt .schritte li:nth-child(1) { animation-delay: 260ms; } .zeigt .schritte li:nth-child(2) { animation-delay: 300ms; } .zeigt .schritte li:nth-child(3) { animation-delay: 340ms; }
.angaben { margin-top: 22px; font-size: 1rem; }
.angaben summary { cursor: pointer; font-weight: 600; padding: 8px 0; width: fit-content; }
.angaben dl { display: grid; grid-template-columns: 9rem minmax(0, 1fr); gap: 6px 12px; padding: 8px 0 4px; }
.angaben dt { color: var(--kreide-leise); }
.b-wege { display: flex; flex-wrap: wrap; gap: 12px 22px; align-items: center; margin-top: 26px; }
.b-wege a, .b-wege button { color: var(--kreide); font-weight: 600; text-underline-offset: 4px; }
.b-wege .neben { background: none; border: 2px solid var(--kreide); border-radius: 6px; height: 48px; padding: 0 18px; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; }
.b-wege .neben:hover { background: rgba(246, 243, 236, .1); }
/* View Transition: Fläche gleitet, Inhalte blenden */
::view-transition-group(flaeche) { animation-duration: var(--t-morph); animation-timing-function: var(--ein); }
::view-transition-group(knopf) { animation-duration: var(--t-morph); animation-timing-function: var(--ein); }
::view-transition-old(flaeche) { animation: 120ms linear both vt-aus; }
::view-transition-new(flaeche) { animation: 200ms linear 80ms both vt-ein; }
::view-transition-old(knopf), ::view-transition-new(knopf) { height: 100%; width: 100%; object-fit: cover; }
::view-transition-old(knopf) { animation: 160ms linear both vt-aus; }
::view-transition-new(knopf) { animation: 160ms linear both vt-ein; }
.zurueck::view-transition-group(flaeche), .zurueck::view-transition-group(knopf) { animation-duration: var(--t-aus); animation-timing-function: var(--aus); }
@keyframes vt-aus { to { opacity: 0; } }
@keyframes vt-ein { from { opacity: 0; } }
/* Variante Zeile: Rückruf in einer Zeile */
.v-zeile { display: none; }
[data-variante=zeile] .buehne { display: none; }
[data-variante=zeile] .v-zeile { display: block; }
.v-zeile { background: var(--tanne); color: var(--kreide); padding: clamp(40px, 7vw, 104px) clamp(20px, 4vw, 56px); min-height: calc(100svh - 70px); }
.v-zeile .innen { max-width: 1180px; margin: 0 auto; display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: clamp(28px, 5vw, 80px); align-items: center; }
.collage { position: relative; min-height: 26rem; }
.collage .abzug { position: absolute; right: 4%; top: 0; margin: 0; width: min(24rem, 92%); transform: rotate(2.5deg); }
.collage .abzug img { aspect-ratio: 3 / 2; }
.zettel-notiz { position: absolute; left: 0; bottom: 0; width: min(17rem, 80%); background: var(--zettel); color: var(--tinte); padding: 18px 20px 16px; transform: rotate(-3deg); box-shadow: 0 1px 2px rgba(0, 0, 0, .18), 0 12px 26px -14px rgba(0, 0, 0, .45); font: italic 500 1.0625rem/1.45 var(--text); }
.zettel-notiz b { display: block; font-style: normal; font-weight: 600; font-size: .875rem; margin-top: 10px; color: var(--leise); }
.v-zeile h1 { font: 700 clamp(2.25rem, 1.4rem + 3.6vw, 4.25rem)/.98 var(--display); letter-spacing: -.03em; max-width: 14ch; margin: 14px 0 18px; text-wrap: balance; }
.v-zeile .versal { color: var(--kreide-leise); }
.v-zeile .lead { font-size: 1.125rem; line-height: 1.6; color: var(--kreide-leise); max-width: 44ch; }
.rueckruf { view-transition-name: zeile; margin-top: 36px; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0; max-width: 44rem; background: var(--kreide); border-radius: 6px; padding: 6px; }
.rueckruf label.nur-sr + input, .rueckruf input { height: 56px; border: 0; background: transparent; color: var(--tinte); font-size: 1.125rem; padding: 0 16px; min-width: 0; border-radius: 4px; }
.rueckruf input:focus-visible { outline: 2px solid var(--tinte); outline-offset: -2px; }
.rueckruf input:read-only { color: var(--leise); }
.rueckruf .knopf { view-transition-name: zknopf; min-width: 13rem; }
.zeile-unten { display: flex; flex-wrap: wrap; gap: 8px 22px; margin-top: 14px; font-size: .9375rem; color: var(--kreide-leise); }
.zeile-unten a { color: var(--kreide); }
.v-zeile .meldung { max-width: 44rem; color: var(--tinte); }
.v-zeile .fehler-feld { color: #FFB4A8; margin-top: 10px; }
.notiert { view-transition-name: zeile; margin-top: 36px; max-width: 44rem; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 16px; min-height: 68px; padding: 6px 6px 6px 8px; border-radius: 6px; background: var(--kreide); color: var(--tinte); }
.notiert[hidden] { display: none; }
.notiert .haken { view-transition-name: zknopf; width: 56px; height: 56px; box-shadow: none; border-radius: 4px; }
.notiert p { font-size: 1.0625rem; line-height: 1.4; }
.notiert p b { font-variant-numeric: tabular-nums; }
.notiert p:focus, .notiert p:focus-visible { outline: none; }
.notiert button { height: 48px; padding: 0 16px; background: none; border: 1px solid rgba(90, 86, 80, .55); border-radius: 6px; cursor: pointer; font-weight: 600; }
::view-transition-group(zeile), ::view-transition-group(zknopf) { animation-duration: var(--t-morph); animation-timing-function: var(--ein); }
::view-transition-old(zeile) { animation: 120ms linear both vt-aus; }
::view-transition-new(zeile) { animation: 200ms linear 80ms both vt-ein; }
::view-transition-old(zknopf), ::view-transition-new(zknopf) { height: 100%; width: 100%; object-fit: cover; }
.zurueck::view-transition-group(zeile), .zurueck::view-transition-group(zknopf) { animation-duration: var(--t-aus); animation-timing-function: var(--aus); }
/* Pult */
.pult { position: fixed; left: 16px; bottom: 16px; z-index: 50; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; max-width: calc(100vw - 32px); padding: 10px 12px; background: rgba(22, 22, 22, .9); color: #f3f2ee; border-radius: 8px; font: 500 11px/1 'Funnel Sans', system-ui, sans-serif; letter-spacing: .1em; text-transform: uppercase; }
.pult .gruppe { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.pult .gruppe > span { color: rgba(243, 242, 238, .72); margin-right: 4px; }
.pult button { background: transparent; color: inherit; border: 1px solid rgba(243, 242, 238, .28); border-radius: 4px; height: 28px; padding: 0 9px; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; }
.pult button[aria-pressed=true] { background: #f3f2ee; color: #161616; border-color: #f3f2ee; }
.pult button:focus-visible { outline: 2px solid #f3f2ee; outline-offset: 2px; }
.zeiger { position: fixed; left: 0; top: 0; z-index: 60; width: 22px; height: 22px; pointer-events: none; opacity: 0; transition: opacity 200ms linear; }
.vorfuehrung .zeiger { opacity: 1; }
/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .kopf nav { display: none; }
@media (max-width: 900px) {
.buehne { grid-template-columns: minmax(0, 1fr); }
.abzug { display: none; }
.v-zeile .innen { grid-template-columns: minmax(0, 1fr); }
.collage { display: none; }
.stelle > p { max-width: none; }
}
@media (max-width: 520px) {
.kopf nav { display: none; }
.zeile2 { grid-template-columns: minmax(0, 1fr); gap: 0; }
.knopf { width: 100%; min-width: 0; }
.rueckruf { grid-template-columns: minmax(0, 1fr); gap: 6px; }
.notiert { grid-template-columns: auto minmax(0, 1fr); }
.notiert button { grid-column: 1 / -1; }
.fakten div { grid-template-columns: 6.5rem minmax(0, 1fr); }
.b-kopf { grid-template-columns: minmax(0, 1fr); }
.angaben dl { grid-template-columns: minmax(0, 1fr); gap: 0 0; } .angaben dd { margin-bottom: 8px; }
}
:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { animation-duration: .01ms !important; animation-delay: 0ms !important; transition-duration: .01ms !important; }
:root.reduziert .flaeche, :root.reduziert .notiert { view-transition-name: none; }
:root.reduziert .bestaetigung.zeigt, :root.reduziert .notiert.zeigt { animation: blende 150ms linear both !important; }
@keyframes blende { from { opacity: 0; } }
@media (hover: none) { .knopf:hover { background: var(--mohn); } .knopf:hover .pfeil { transform: none; } }
@media (forced-colors: active) {
button, [role=button], input, .chips span { border: 1px solid ButtonText !important; }
.chips input:checked + span { outline: 3px solid Highlight; }
.meldung { border: 1px solid CanvasText; }
}
</style>
</head>
<body>
<header class="kopf">
<p class="marke">Elektro Quast <span>· Stadthagen · Karriere</span></p>
<nav aria-label="Karriere"><a href="#stelle">Offene Stellen</a><a href="#stelle">Ein Tag bei uns</a><a href="tel:+4957219876">05721 98 76-0</a></nav>
</header>
<main>
<section class="buehne" id="stelle" aria-labelledby="stelle-titel">
<div class="stelle">
<p class="versal">Stelle 1 von 3 · Vollzeit</p>
<h1 id="stelle-titel">Elektroniker:in für Gebäudetechnik</h1>
<p>Du verkabelst Neubauten im Schaumburger Land und baust Wärmepumpen an. Bewirb dich in zwei Minuten, ohne Anschreiben.</p>
<dl class="fakten">
<div><dt>Lohn</dt><dd>19,50 bis 25 € die Stunde</dd></div>
<div><dt>Arbeitszeit</dt><dd>38 Stunden, freitags um 13 Uhr Schluss</dd></div>
<div><dt>Urlaub</dt><dd>30 Tage, Heiligabend frei</dd></div>
</dl>
<figure class="abzug">
<img src="bilder/handwerk-heizraum-hoch.webp" alt="Heizraum mit Speicher und gedämmten Rohren unter der Decke" width="800" height="1000">
<figcaption>Heizraum in Bückeburg, Oktober 2026</figcaption>
</figure>
</div>
<div class="flaeche" id="flaeche" data-zustand="bereit">
<form id="bewerbung" action="danke.html" method="post" novalidate>
<p class="versal">Kurzbewerbung</p>
<h2>Drei Angaben, dann melden wir uns.</h2>
<p class="einleitung">Kein Anschreiben, kein Lebenslauf nötig. Jana aus der Werkstattleitung ruft dich an.</p>
<div class="zeile2">
<div class="feld">
<label for="b-name">Dein Name</label>
<input type="text" id="b-name" name="name" autocomplete="name" enterkeyhint="next" required aria-describedby="b-name-f">
<p class="fehler-feld" id="b-name-f"></p>
</div>
<div class="feld">
<label for="b-tel">Telefon</label>
<input type="tel" id="b-tel" name="telefon" autocomplete="tel" inputmode="tel" enterkeyhint="send" required aria-describedby="b-tel-h b-tel-f">
<p class="hilfe" id="b-tel-h">Mobil oder Festnetz, z. B. 0171 2345678</p>
<p class="fehler-feld" id="b-tel-f"></p>
</div>
</div>
<fieldset class="feld">
<legend>Wann können wir dich anrufen? <span class="frei">(freiwillig)</span></legend>
<div class="chips">
<label><input type="radio" name="zeit" value="Vormittags"><span>Vormittags</span></label>
<label><input type="radio" name="zeit" value="Mittags"><span>Mittags</span></label>
<label><input type="radio" name="zeit" value="Nach 17 Uhr"><span>Nach 17 Uhr</span></label>
</div>
</fieldset>
<div class="meldung" id="b-meldung" role="alert" tabindex="-1">
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="9" fill="none" stroke="#C8382A" stroke-width="2"/><path d="M10 5.5v5.5M10 13.6v.9" stroke="#C8382A" stroke-width="2" stroke-linecap="round"/></svg>
<strong data-titel></strong>
<span data-text></span>
</div>
<div class="senden">
<button class="knopf" type="submit" id="b-knopf"><span data-label>Bewerbung abschicken</span><svg class="pfeil" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><path d="M2 9h13M10 4l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2"/></svg><i class="lade" aria-hidden="true"></i></button>
<p class="datenschutz">Wir nutzen deine Angaben nur für diese Bewerbung. <a href="#datenschutz">Datenschutz</a></p>
<p class="langsam">Das dauert länger als sonst. Bitte nicht neu laden, deine Angaben gehen nicht verloren.</p>
</div>
</form>
<div class="bestaetigung" id="b-bestaetigung" hidden>
<div class="b-kopf">
<div class="haken" aria-hidden="true"><svg width="30" height="30" viewBox="0 0 30 30"><path d="M7 15.5l5.5 5.5L23.5 9.5" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<div>
<p class="versal">Eingang <span data-nummer>B-1104-27</span> · <span data-uhr>14:32</span> Uhr</p>
<h2 tabindex="-1" id="b-danke">Danke, <span data-vorname>Mara</span>. Deine Bewerbung ist da.</h2>
<p class="b-text">Du musst nichts weiter tun. Die Nummer brauchst du nur, wenn du nachfragst.</p>
</div>
</div>
<ol class="schritte">
<li><div><b>Bis Freitag ruft Jana Quast dich an</b><span>Werkstattleitung, von 05721 98 76-12, <span data-zeitwunsch>wann es dir passt</span></span></div></li>
<li><div><b>Ihr verabredet einen Probetag</b><span>Bezahlt, sechs Stunden, auf einer echten Baustelle</span></div></li>
<li><div><b>Danach entscheidest du</b><span>Zusage oder Absage innerhalb einer Woche</span></div></li>
</ol>
<details class="angaben">
<summary>Deine Angaben ansehen</summary>
<dl><dt>Name</dt><dd data-a="name"></dd><dt>Telefon</dt><dd data-a="telefon"></dd><dt>Erreichbar</dt><dd data-a="zeit"></dd></dl>
</details>
<div class="b-wege">
<button type="button" class="neben" data-aendern>Angaben korrigieren</button>
<a href="#stelle">Weitere Stellen ansehen</a>
</div>
</div>
</div>
</section>
<section class="v-zeile" aria-labelledby="z-titel">
<div class="innen">
<div>
<p class="versal">Lieber erst reden?</p>
<h1 id="z-titel">Wir rufen dich zurück. Heute noch.</h1>
<p class="lead">Nummer eintragen, abschicken, fertig. Jana erzählt dir, wie ein Tag bei uns aussieht, und was wir zahlen.</p>
<form class="rueckruf" id="rueckruf" action="danke.html" method="post" novalidate data-zustand="bereit">
<label class="nur-sr" for="z-tel">Deine Telefonnummer</label>
<input type="tel" id="z-tel" name="telefon" autocomplete="tel" inputmode="tel" enterkeyhint="send" placeholder="z. B. 0171 2345678" required aria-describedby="z-tel-f">
<button class="knopf" type="submit" id="z-knopf"><span data-label>Rückruf anfordern</span><svg class="pfeil" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><path d="M2 9h13M10 4l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2"/></svg><i class="lade" aria-hidden="true"></i></button>
</form>
<div class="notiert" id="z-notiert" hidden>
<div class="haken" aria-hidden="true"><svg width="26" height="26" viewBox="0 0 30 30"><path d="M7 15.5l5.5 5.5L23.5 9.5" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<p tabindex="-1" id="z-text">Notiert. Jana ruft dich heute bis 17 Uhr an: <b data-nr></b></p>
<button type="button" data-z-aendern>Nummer ändern</button>
</div>
<p class="fehler-feld" id="z-tel-f"></p>
<div class="meldung" id="z-meldung" role="alert" tabindex="-1">
<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="9" fill="none" stroke="#C8382A" stroke-width="2"/><path d="M10 5.5v5.5M10 13.6v.9" stroke="#C8382A" stroke-width="2" stroke-linecap="round"/></svg>
<strong data-titel></strong><span data-text></span>
</div>
<p class="zeile-unten"><span>Mo bis Fr, 7 bis 17 Uhr</span><a href="tel:+4957219876">Oder gleich anrufen: 05721 98 76-0</a></p>
</div>
<div class="collage" aria-hidden="true">
<figure class="abzug"><img src="bilder/handwerk-waermepumpe.webp" alt="" width="1200" height="800"><figcaption>Wärmepumpe in Obernkirchen, September 2026</figcaption></figure>
<p class="zettel-notiz">Rückruf heißt: zehn Minuten Telefon. Kein Vorstellungsgespräch, keine Unterlagen. <b>Jana Quast, Werkstattleitung</b></p>
</div>
</div>
</section>
</main>
<p class="nur-sr" aria-live="polite" id="ansage"></p>
<div class="pult">
<div class="gruppe" role="group" aria-label="Variante"><span>Variante</span>
<button type="button" data-v="karte" aria-pressed="true">Karte</button>
<button type="button" data-v="zeile" aria-pressed="false">Zeile</button>
</div>
<div class="gruppe" role="group" aria-label="Antwort des Servers"><span>Server</span>
<button type="button" data-antwort="erfolg" aria-pressed="true">Erfolg</button>
<button type="button" data-antwort="fehler" aria-pressed="false">Fehler</button>
<button type="button" data-antwort="langsam" aria-pressed="false">Langsam</button>
<button type="button" data-antwort="offline" aria-pressed="false">Offline</button>
</div>
<button type="button" data-reset>Zurücksetzen</button>
</div>
<svg class="zeiger" viewBox="0 0 22 22" aria-hidden="true"><path d="M3 2l15 8.5-6.6 1.6L8.4 19z" fill="#161616" stroke="#F6F3EC" stroke-width="1.5" stroke-linejoin="round"/></svg>
<script>
(() => {
const h = document.documentElement, $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
const reduziert = () => h.classList.contains('reduziert');
const VARIANTEN = ['karte', 'zeile'];
const q = new URLSearchParams(location.search);
let variante = VARIANTEN.includes(q.get('v')) ? q.get('v') : 'karte';
let antwort = 'erfolg', zaehler = 27;
const ansage = $('#ansage');
const sage = t => { ansage.textContent = ''; setTimeout(() => { ansage.textContent = t; }, 60); };
/* Morph über View Transitions, sonst sofort (reduziert: kurze Überblendung per CSS) */
function morph(fn, zurueck) {
if (!document.startViewTransition || reduziert()) { fn(); return Promise.resolve(); }
h.classList.toggle('zurueck', !!zurueck);
const vt = document.startViewTransition(fn);
return vt.finished.catch(() => {}).finally(() => h.classList.remove('zurueck'));
}
/* Gedachter Server: Erfolg, Fehler, langsam; Abbruch nach 15 s */
let serverT = null;
function server(modus) {
return new Promise((ok, nein) => {
const dauer = modus === 'langsam' ? 5600 : 1400;
const abbruch = setTimeout(() => nein(new Error('zeit')), 15000);
serverT = setTimeout(() => { clearTimeout(abbruch); modus === 'fehler' ? nein(new Error('500')) : ok({ nummer: 'B-1104-' + (zaehler++) }); }, dauer);
});
}
const uhr = () => new Intl.DateTimeFormat('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Berlin' }).format(new Date());
function meldung(el, titel, text) {
el.querySelector('[data-titel]').textContent = titel;
el.querySelector('[data-text]').innerHTML = text;
el.classList.remove('an'); void el.offsetWidth; el.classList.add('an');
}
const ohneMeldung = el => el.classList.remove('an');
function wackeln(el) { if (reduziert()) return; el.classList.remove('wackeln'); void el.offsetWidth; el.classList.add('wackeln'); }
function feldFehler(input, text) {
const f = document.getElementById(input.id + '-f');
input.setAttribute('aria-invalid', text ? 'true' : 'false');
if (f) f.textContent = text || '';
return !text;
}
const ziffern = v => (v.match(/\d/g) || []).length;
/* ---------- Variante Karte ---------- */
const fl = $('#flaeche'), form = $('#bewerbung'), best = $('#b-bestaetigung'), knopf = $('#b-knopf'), bm = $('#b-meldung');
const bName = $('#b-name'), bTel = $('#b-tel');
let langsamT = null;
function karteZustand(z) {
fl.dataset.zustand = z;
const sendet = z === 'sendet';
[bName, bTel].forEach(i => { i.readOnly = sendet; });
$$('#bewerbung input[type=radio]').forEach(i => { i.disabled = sendet && !i.checked; });
form.classList.toggle('gesperrt', sendet);
knopf.setAttribute('aria-disabled', sendet ? 'true' : 'false');
knopf.querySelector('[data-label]').textContent = sendet ? 'Wird gesendet …' : z === 'fehler' ? 'Erneut senden' : 'Bewerbung abschicken';
if (!sendet) { clearTimeout(langsamT); delete fl.dataset.langsam; }
}
function pruefeKarte() {
const a = feldFehler(bName, bName.value.trim().length < 2 ? 'Bitte gib deinen Namen ein.' : '');
const b = feldFehler(bTel, ziffern(bTel.value) < 6 ? 'Bitte gib eine Telefonnummer ein, z. B. 0171 2345678.' : '');
if (!a) bName.focus(); else if (!b) bTel.focus();
return a && b;
}
async function sendeKarte(modus = antwort) {
if (fl.dataset.zustand === 'sendet' || fl.dataset.zustand === 'gesendet') return;
ohneMeldung(bm);
if (!pruefeKarte()) return;
if (modus === 'offline' || !navigator.onLine) {
karteZustand('fehler'); wackeln(knopf);
meldung(bm, 'Du bist gerade offline.', 'Sobald du wieder Netz hast, schick die Bewerbung ab. Deine Angaben bleiben hier stehen.');
return;
}
karteZustand('sendet'); sage('Bewerbung wird gesendet.');
langsamT = setTimeout(() => { fl.dataset.langsam = ''; }, 4000);
try {
const r = await server(modus);
const name = bName.value.trim(), zeit = (form.querySelector('[name=zeit]:checked') || {}).value;
best.querySelector('[data-nummer]').textContent = r.nummer;
best.querySelector('[data-uhr]').textContent = uhr();
best.querySelector('[data-vorname]').textContent = name.split(/\s+/)[0];
best.querySelector('[data-zeitwunsch]').textContent = zeit ? ({ Vormittags: 'vormittags', Mittags: 'mittags', 'Nach 17 Uhr': 'nach 17 Uhr' })[zeit] : 'wann es dir passt';
best.querySelector('[data-a=name]').textContent = name;
best.querySelector('[data-a=telefon]').textContent = bTel.value.trim();
best.querySelector('[data-a=zeit]').textContent = zeit || 'keine Angabe';
await morph(() => {
form.hidden = true; best.hidden = false;
fl.classList.add('dunkel'); best.classList.add('zeigt');
karteZustand('gesendet');
});
$('#b-danke').focus({ preventScroll: true });
const r2 = $('#b-danke').getBoundingClientRect();
if (r2.top < 0 || r2.bottom > innerHeight) $('#b-danke').scrollIntoView({ block: 'center', behavior: reduziert() ? 'auto' : 'smooth' });
sage('Bewerbung gesendet. Eingang ' + r.nummer + '.');
} catch (e) {
karteZustand('fehler'); wackeln(knopf);
meldung(bm, 'Das hat nicht geklappt.', 'Der Server hat nicht geantwortet. Deine Angaben sind noch da. Versuch es noch einmal oder ruf Jana an: <a href="tel:+495721987612">05721 98 76-12</a>.');
}
}
form.addEventListener('submit', e => { e.preventDefault(); sendeKarte(); });
[bName, bTel].forEach(i => i.addEventListener('input', () => { if (i.getAttribute('aria-invalid') === 'true') feldFehler(i, ''); }));
async function karteZurueck(fokus = true) {
await morph(() => {
best.hidden = true; best.classList.remove('zeigt'); form.hidden = false;
fl.classList.remove('dunkel'); karteZustand('bereit');
}, true);
if (fokus) bName.focus();
}
best.querySelector('[data-aendern]').addEventListener('click', () => karteZurueck());
/* ---------- Variante Zeile ---------- */
const zf = $('#rueckruf'), zTel = $('#z-tel'), zk = $('#z-knopf'), zn = $('#z-notiert'), zm = $('#z-meldung');
function zeileZustand(z) {
zf.dataset.zustand = z;
zTel.readOnly = z === 'sendet';
zk.setAttribute('aria-disabled', z === 'sendet' ? 'true' : 'false');
zk.querySelector('[data-label]').textContent = z === 'sendet' ? 'Wird gesendet …' : z === 'fehler' ? 'Erneut senden' : 'Rückruf anfordern';
}
async function sendeZeile(modus = antwort) {
if (zf.dataset.zustand === 'sendet' || zf.dataset.zustand === 'gesendet') return;
ohneMeldung(zm);
if (!feldFehler(zTel, ziffern(zTel.value) < 6 ? 'Bitte gib eine Telefonnummer ein, z. B. 0171 2345678.' : '')) { zTel.focus(); wackeln(zf); return; }
if (modus === 'offline' || !navigator.onLine) {
zeileZustand('fehler'); wackeln(zf);
meldung(zm, 'Du bist gerade offline.', 'Die Nummer bleibt stehen. Schick sie ab, sobald du wieder Netz hast.');
return;
}
zeileZustand('sendet'); sage('Rückrufwunsch wird gesendet.');
try {
await server(modus);
zn.querySelector('[data-nr]').textContent = zTel.value.trim();
await morph(() => { zf.hidden = true; zn.hidden = false; zn.classList.add('zeigt'); zeileZustand('gesendet'); });
$('#z-text').focus({ preventScroll: true });
sage('Notiert. Jana ruft dich heute bis 17 Uhr an.');
} catch (e) {
zeileZustand('fehler'); wackeln(zf);
meldung(zm, 'Das hat nicht geklappt.', 'Deine Nummer ist noch da. Versuch es noch einmal oder ruf direkt an: <a href="tel:+4957219876">05721 98 76-0</a>.');
}
}
zf.addEventListener('submit', e => { e.preventDefault(); sendeZeile(); });
zTel.addEventListener('input', () => { if (zTel.getAttribute('aria-invalid') === 'true') feldFehler(zTel, ''); });
async function zeileZurueck(fokus = true) {
await morph(() => { zn.hidden = true; zn.classList.remove('zeigt'); zf.hidden = false; zeileZustand('bereit'); }, true);
if (fokus) zTel.focus();
}
zn.querySelector('[data-z-aendern]').addEventListener('click', () => zeileZurueck());
/* ---------- Pult ---------- */
function setzeVariante(v, schreiben) {
variante = v; document.body.dataset.variante = v;
$$('[data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v ? 'true' : 'false'));
if (schreiben) { const p = new URLSearchParams(location.search); p.set('v', v); history.replaceState(null, '', location.pathname + '?' + p + location.hash); }
}
$$('[data-v]').forEach(b => b.addEventListener('click', () => { pause(); setzeVariante(b.dataset.v, true); neustart(); }));
$$('[data-antwort]').forEach(b => b.addEventListener('click', () => {
antwort = b.dataset.antwort;
$$('[data-antwort]').forEach(x => x.setAttribute('aria-pressed', x === b ? 'true' : 'false'));
}));
$('[data-reset]').addEventListener('click', () => { pause(); neustart(); });
/* ---------- Ausgangszustand ---------- */
function neustart() {
clearTimeout(serverT); clearTimeout(langsamT);
form.reset(); form.hidden = false; best.hidden = true; best.classList.remove('zeigt'); fl.classList.remove('dunkel');
[bName, bTel].forEach(i => feldFehler(i, '')); ohneMeldung(bm); karteZustand('bereit');
zf.reset(); zf.hidden = false; zn.hidden = true; zn.classList.remove('zeigt'); feldFehler(zTel, ''); ohneMeldung(zm); zeileZustand('bereit');
}
/* ---------- Vorführung: gedachter Zeiger füllt aus, schickt ab, scheitert jede zweite Runde einmal ---------- */
const zeiger = $('.zeiger');
let lauf = 0, zx = innerWidth * .5, zy = innerHeight * .9;
const warte = (ms, id) => new Promise((ok, nein) => setTimeout(() => (id === lauf ? ok() : nein('stopp')), ms));
function zeigeAuf(el, id, fx = .62, fy = .55) {
const r = el.getBoundingClientRect(), x = r.left + r.width * fx, y = r.top + r.height * fy;
const dauer = reduziert() ? 0 : 560;
zeiger.animate([{ transform: `translate(${zx}px, ${zy}px)` }, { transform: `translate(${x}px, ${y}px)` }], { duration: dauer, easing: 'cubic-bezier(0.16, 1, 0.3, 1)', fill: 'forwards' });
zx = x; zy = y;
return warte(dauer + 120, id);
}
async function tippe(input, text, id) {
input.focus({ preventScroll: true });
for (const c of text) { input.value += c; await warte(reduziert() ? 0 : 42, id); }
}
async function runde(id, n) {
neustart();
const fehlerRunde = n % 2 === 1;
await warte(500, id);
if (variante === 'karte') {
await zeigeAuf(bName, id); await tippe(bName, 'Mara Lindholm', id);
await zeigeAuf(bTel, id); await tippe(bTel, '0171 2345678', id);
const chip = form.querySelector('[value="Nach 17 Uhr"]');
await zeigeAuf(chip.nextElementSibling, id); chip.checked = true;
await zeigeAuf(knopf, id);
if (fehlerRunde) { sendeKarte('fehler'); await warte(2900, id); await zeigeAuf(knopf, id, .5); }
sendeKarte('erfolg'); await warte(4800, id);
} else {
await zeigeAuf(zTel, id, .3); await tippe(zTel, '0171 2345678', id);
await zeigeAuf(zk, id);
if (fehlerRunde) { sendeZeile('fehler'); await warte(2900, id); await zeigeAuf(zk, id, .5); }
sendeZeile('erfolg'); await warte(4200, id);
}
runde(id, n + 1).catch(() => {});
}
function vorfuehren() {
const id = ++lauf;
h.classList.add('vorfuehrung');
runde(id, 0).catch(() => {});
}
function pause() { lauf++; clearTimeout(serverT); h.classList.remove('vorfuehrung'); }
function weiter() { vorfuehren(); }
setzeVariante(variante, false);
neustart();
})();
</script>
</body>
</html>