Passwortstärke-Visualisierung
Passwort festlegen im Mandantenportal einer Kanzlei: Leiste, Segmente oder eine Zeichnung zeigen die Stärke, die Regeln haken sich einzeln ab, am Ende eine ruhige Bestätigung.
Während der Eingabe zeigt eine Leiste, eine Segmentreihe oder ein Bild die Stärke. Länge und Farbe wachsen mit. Die Regeln wie Länge, Ziffer oder Sonderzeichen haken sich einzeln ab. Sind alle erfüllt, folgt eine dezente Bestätigung.
Anforderungen werden spielerisch erfüllbar. Das senkt den Frust bei Registrierungen.
Wann einsetzen
Registrierungen, Einladungen in ein Kundenportal, Dialoge zum Ändern des Passworts.
Wann nicht
Beim Login. Dort braucht es keine Stärkeanzeige.
Varianten
- Durchgehende Leiste mit Farbübergang
- Segmentleiste mit vier Stufen
- Regelliste per :has(:user-invalid) (Chrome)
- Bild wird mit steigender Stärke schärfer (Codrops PasswordStrengthVisualization, nach Colibro)
Umsetzung
Einen Score von 0 bis 4 berechnen, etwa mit zxcvbn oder aus Länge und Zeichenvorrat, und als --s setzen. Leiste: transform: scaleX(calc(var(--s) / 4)) mit einer Transition von 200 bis 300 ms, Farbe per color-mix zwischen Rot und Grün. Regeln als li[data-ok] mit einem Häkchen, das per scale einblendet. Die Stärke als Text danebenschreiben.
Robust umsetzen
Telefon. Identisch.
Ohne Bewegung. Ohne Transition.
Barrierefrei. Die Stärke zusätzlich als Text zeigen (schwach, stark) und gebündelt per aria-live="polite" ansagen. Nie nur Farbe. Regeln mit erfüllt oder nicht erfüllt für Screenreader auszeichnen und über aria-describedby mit dem Feld verbinden.
Code
<!DOCTYPE html>
<!--
Passwortstärke-Visualisierung
Fundus · Mikroanimationen · https://cecodesign.de/lab/fundus/mikroanimationen/passwort-staerke/
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>Passwortstärke-Visualisierung · 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: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-style: normal; 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 kanzlei-beratung-souveraen */
--grund: #f9f8f8; --flaeche: #efebeb; --linie: #d2cdc7; --marke: #1e1e50; --text: #212529; --meta: #7d6c5e; --akzent: #00bee6;
--schwach: #c8423f; --stark: #2f7d55;
--kurve: cubic-bezier(0.4, 0, 0.22, 1); /* eine Ausklang-Kurve für alles */
--kurz: 125ms; --mittel: 250ms;
--s: 0; /* Stärke 0–4, setzt das Skript */
--farbe: color-mix(in oklab, var(--schwach), var(--stark) calc(var(--s) * 25%));
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Schibsted Grotesk', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; overflow-x: hidden; }
button, input { font: inherit; color: inherit; }
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.serif { font-family: 'Newsreader', Georgia, serif; font-weight: 400; letter-spacing: -.01em; }
/* Kopf */
.kopf { display: flex; align-items: center; gap: 24px; height: 72px; padding: 0 clamp(16px, 4vw, 48px); border-bottom: 1px solid var(--linie); }
.marke { margin-right: auto; color: var(--marke); text-decoration: none; }
.marke b { display: block; font: 400 1.375rem/1 'Newsreader', Georgia, serif; letter-spacing: -.01em; }
.marke small { display: block; margin-top: 4px; font-size: .75rem; font-weight: 500; color: var(--meta); letter-spacing: .02em; }
.kopf-rechts { display: flex; align-items: center; gap: 22px; font-size: .875rem; font-weight: 500; }
.kopf-rechts a { color: var(--marke); text-decoration: none; font-variant-numeric: tabular-nums; background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat; transition: background-size var(--kurz) var(--kurve); }
.kopf-rechts a:hover { background-size: 100% 1px; }
.portal { display: inline-flex; align-items: center; gap: 8px; padding: 0 12px; height: 32px; border: 1px solid var(--linie); border-radius: 2px; color: var(--marke); }
.portal::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--akzent); }
/* Bühne */
.buehne { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); min-height: calc(100svh - 72px); }
.tafel { position: relative; display: flex; flex-direction: column; gap: 28px; padding: clamp(28px, 4vw, 56px); background: var(--marke); color: var(--grund); overflow: hidden; }
.tafel h2 { font-size: clamp(1.625rem, 1.2rem + 1.6vw, 2.5rem); line-height: 1.1; max-width: 16ch; text-wrap: balance; }
.tafel .mono { color: rgba(249, 248, 248, .6); }
.punkte { list-style: none; padding: 0; display: grid; gap: 0; border-top: 1px solid rgba(249, 248, 248, .18); max-width: 30rem; }
.punkte li { display: grid; grid-template-columns: 2.6rem 1fr; padding: 13px 0; border-bottom: 1px solid rgba(249, 248, 248, .18); font-size: .9375rem; line-height: 1.5; }
.punkte li span:first-child { font-variant-numeric: tabular-nums; color: rgba(249, 248, 248, .55); }
.haus { position: relative; margin-top: auto; max-width: 480px; }
.haus svg { display: block; width: 100%; height: auto; transition: filter var(--mittel) var(--kurve), opacity var(--mittel) var(--kurve); }
.haus figcaption { margin-top: 10px; }
.var-bild .haus svg { filter: blur(calc((4 - var(--s)) * 3.5px)); opacity: calc(.35 + var(--s) * .1625); }
.formular { display: flex; justify-content: center; padding: clamp(28px, 3.4vw, 48px) clamp(16px, 4vw, 56px); }
.form { width: 100%; max-width: 30rem; }
.schritt { display: flex; gap: 8px; align-items: center; }
.schritt i { width: 22px; height: 2px; background: var(--linie); }
.schritt i.an { background: var(--marke); }
.form h1 { font-size: clamp(2rem, 1.3rem + 2.2vw, 3.25rem); line-height: 1.1; color: var(--marke); margin: 16px 0 10px; text-wrap: balance; }
.form .einleitung { font-size: 1.0625rem; line-height: 1.6; max-width: 46ch; text-wrap: pretty; }
.feld { display: grid; grid-template-columns: minmax(0, 1fr); gap: 7px; margin-top: 18px; }
.feld label, .feld .label { font-size: .875rem; font-weight: 500; }
.eingabe { position: relative; display: flex; align-items: center; height: 48px; background: #fff; border: 1px solid var(--linie); border-radius: 2px; transition: border-color var(--kurz) var(--kurve), box-shadow var(--kurz) var(--kurve); }
.eingabe:focus-within { border-color: var(--marke); box-shadow: 0 0 0 3px color-mix(in srgb, var(--akzent) 35%, transparent); }
.eingabe input { flex: 1; min-width: 0; height: 100%; padding: 0 14px; border: 0; background: none; font-size: 1rem; outline: none; font-variant-ligatures: none; }
.eingabe input[readonly] { color: #5b5f64; }
.feld-fest .eingabe { background: var(--flaeche); border-color: transparent; }
.zeigen { display: inline-flex; align-items: center; gap: 6px; height: 34px; margin-right: 7px; padding: 0 10px; border: 0; border-radius: 2px; background: none; font-size: .8125rem; font-weight: 500; color: var(--marke); cursor: pointer; transition: background-color var(--kurz) var(--kurve); }
.zeigen:hover { background: var(--flaeche); }
.zeigen:focus-visible { outline: 2px solid var(--akzent); outline-offset: 1px; }
.zeigen svg { width: 18px; height: 18px; }
.zeigen .zu { display: none; }
.zeigen[aria-pressed='true'] .auf { display: none; }
.zeigen[aria-pressed='true'] .zu { display: block; }
.erfuellt .eingabe-pw { border-color: var(--stark); }
/* Stärkeanzeige */
.staerke { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px 14px; margin-top: 10px; }
.leiste { position: relative; height: 4px; background: var(--flaeche); border-radius: 2px; overflow: hidden; }
.leiste i { position: absolute; inset: 0; background: var(--farbe); transform-origin: left; transform: scaleX(calc(var(--s) / 4)); transition: transform var(--mittel) var(--kurve), background-color var(--mittel) var(--kurve); }
.leer .leiste i { transform: scaleX(0); }
.segmente { display: none; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.segmente i { height: 4px; border-radius: 1px; background: var(--flaeche); overflow: hidden; position: relative; }
.segmente i::after { content: ''; position: absolute; inset: 0; background: var(--farbe); transform: scaleX(0); transform-origin: left; transition: transform var(--mittel) var(--kurve), background-color var(--mittel) var(--kurve); }
.segmente i.an::after { transform: scaleX(1); }
.var-segmente .segmente { display: grid; }
.var-segmente .leiste, .var-bild .leiste, .var-bild .segmente { display: none; }
.var-bild .staerke { grid-template-columns: auto 1fr; }
.var-bild .staerke-text { grid-column: 1; grid-row: 1; }
.var-bild .bildhinweis { grid-column: 2; grid-row: 1; text-align: right; }
.urteil { font-size: .8125rem; font-weight: 500; color: color-mix(in oklab, var(--farbe), #000 22%); white-space: nowrap; transition: color var(--mittel) var(--kurve); }
.leer .urteil { color: var(--meta); }
.var-bild .bildhinweis { display: block; }
.bildhinweis { display: none; font-size: .8125rem; color: var(--meta); }
/* Regeln */
.regeln { list-style: none; padding: 0; margin-top: 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 9px 18px; }
.regeln li { display: flex; align-items: center; gap: 9px; font-size: .875rem; line-height: 1.3; }
.haken { position: relative; flex: none; width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid var(--linie); transition: border-color var(--kurz) var(--kurve), background-color var(--kurz) var(--kurve); }
.haken svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: scale(0); transition: transform var(--mittel) var(--kurve); }
.regeln li[data-ok] .haken { border-color: var(--marke); background: var(--marke); }
.regeln li[data-ok] .haken svg { transform: scale(1); }
.regeln li.warnung { color: var(--schwach); }
.regeln li.warnung .haken { border-color: var(--schwach); }
.bestaetigung { display: flex; align-items: center; gap: 8px; margin-top: 14px; font-size: .875rem; font-weight: 500; color: var(--stark); opacity: 0; translate: 0 4px; transition: opacity var(--mittel) var(--kurve), translate var(--mittel) var(--kurve); }
.erfuellt .bestaetigung { opacity: 1; translate: 0 0; }
.bestaetigung svg { width: 16px; height: 16px; }
.wiederholen-ok { font-size: .8125rem; color: var(--stark); min-height: 1.2em; }
.senden { width: 100%; height: 50px; margin-top: 24px; border: 0; border-radius: 2px; background: var(--marke); color: var(--grund); font-weight: 500; font-size: 1rem; cursor: pointer; transition: background-color var(--kurz) var(--kurve), opacity var(--kurz) var(--kurve); }
.senden:hover { background: #2a2a6a; }
.senden[disabled] { opacity: .35; cursor: default; }
.senden:focus-visible { outline: 2px solid var(--akzent); outline-offset: 3px; }
.tipp { margin-top: 16px; font-size: .8125rem; line-height: 1.5; color: var(--meta); }
.tipp b { font-weight: 500; color: var(--text); font-variant-ligatures: none; }
/* Demo-Bedienung */
.pult { padding: 16px clamp(16px, 4vw, 48px) 32px; border-top: 1px solid var(--linie); }
.pult-innen { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 24px; }
.modi { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.modi .mono { margin-right: 6px; }
.modi button { font: 500 11px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); background: none; border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; cursor: pointer; transition: color var(--kurz) var(--kurve), background-color var(--kurz) var(--kurve), border-color var(--kurz) var(--kurve); }
.modi button:hover { color: var(--marke); border-color: #b6aea5; }
.modi button[aria-pressed='true'] { background: var(--marke); color: var(--grund); border-color: var(--marke); }
.werte { display: flex; flex-wrap: wrap; gap: 4px 16px; }
.werte b { color: var(--marke); font-weight: 500; }
/* Vorschau 960 × 720 */
.vorschau body { overflow: hidden; }
.vorschau .pult, .vorschau .kopf-rechts a, .vorschau .tipp, .vorschau .feld-fest { display: none; }
.vorschau .buehne { min-height: 0; height: calc(100svh - 72px); }
.vorschau .formular { padding-top: 32px; }
.vorschau .form h1 { margin-top: 14px; }
.vorschau .haus { max-width: 290px; margin-top: 4px; }
.vorschau .tafel { gap: 22px; }
/* Reduziert: ohne Transition */
.reduziert .leiste i, .reduziert .segmente i::after, .reduziert .haken, .reduziert .haken svg, .reduziert .bestaetigung, .reduziert .haus svg, .reduziert .urteil { transition: none; }
.reduziert .bestaetigung { translate: none; }
@media (max-width: 900px) {
.buehne { grid-template-columns: minmax(0, 1fr); min-height: 0; }
.tafel { order: 2; }
.haus { max-width: 320px; }
}
@media (max-width: 560px) {
.kopf { height: 64px; }
.marke b { font-size: 1.1875rem; }
.portal { display: none; }
.regeln { grid-template-columns: 1fr; }
.kopf-rechts a { display: none; }
}
@media (hover: none) { .zeigen { height: 40px; } .zeigen:hover { background: none; } }
@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"><b>Vahrenholt Liebrecht</b><small>Rechtsanwälte · Steuerberater · Frankfurt am Main</small></a>
<div class="kopf-rechts">
<a href="#hilfe">Hilfe: 069 247 538-0</a>
<span class="portal">Mandantenportal</span>
</div>
</header>
<main class="buehne" id="start">
<aside class="tafel" aria-label="Über das Mandantenportal">
<p class="mono">Mandantenportal</p>
<h2 class="serif">Ihre Unterlagen. Sicher an einem Ort.</h2>
<ol class="punkte">
<li><span>01</span><span>Belege, Bescheide und Verträge verschlüsselt hochladen</span></li>
<li><span>02</span><span>Fristen und Termine Ihrer Mandate im Blick</span></li>
<li><span>03</span><span>Direkter Draht zu Dr. Clara Ruhland und ihrem Team</span></li>
</ol>
<figure class="haus">
<svg viewBox="0 0 420 250" role="img" aria-label="Strichzeichnung der Kanzlei, einer Gründerzeitvilla an der Bockenheimer Landstraße">
<g fill="none" stroke="#f9f8f8" stroke-width="1.4" stroke-linejoin="round" stroke-linecap="round">
<path d="M20 248h380" opacity=".6"/>
<path d="M70 248V74h280v174"/>
<path d="M62 74h296l-12-12H74Z"/>
<path d="M150 62 210 22l60 40"/><path d="M196 52a14 14 0 0 1 28 0v10h-28Z"/>
<path d="M70 128h280M70 186h280" opacity=".55"/>
<path d="M66 124h288v4H66ZM66 182h288v4H66Z" opacity=".75"/>
<g data-fenster></g>
<path d="M188 248v-44a22 22 0 0 1 44 0v44"/><path d="M210 182v66" opacity=".5"/>
<path d="M176 248h68M180 242h60" opacity=".7"/>
<path d="M98 248v-12h40v12M282 248v-12h40v12" opacity=".5"/>
<path d="M40 248c0-30 6-52 14-60 8 8 14 30 14 60M370 248c0-26 5-44 12-52 7 8 12 26 12 52" opacity=".45"/>
</g>
</svg>
<figcaption class="mono">Bockenheimer Landstraße 61 · Westend</figcaption>
</figure>
</aside>
<section class="formular">
<form class="form" data-form novalidate>
<div class="schritt"><i class="an"></i><i class="an"></i><i></i><span class="mono" style="margin-left:6px">Schritt 2 von 3 · Passwort</span></div>
<h1 class="serif">Legen Sie Ihr Passwort fest.</h1>
<p class="einleitung">Dr. Clara Ruhland hat Sie zum Mandantenportal eingeladen. Ihr Passwort schützt Bescheide, Verträge und Lohnunterlagen.</p>
<div class="feld feld-fest">
<span class="label">E-Mail aus der Einladung</span>
<div class="eingabe"><input type="email" value="j.sittig@sittig-holzbau.de" readonly aria-label="E-Mail aus der Einladung"></div>
</div>
<div class="feld" data-feld>
<label for="pw">Neues Passwort</label>
<div class="eingabe eingabe-pw">
<input id="pw" type="password" autocomplete="new-password" spellcheck="false" aria-describedby="pw-urteil pw-regeln" data-pw>
<button class="zeigen" type="button" aria-pressed="false" aria-controls="pw" data-zeigen>
<svg class="auf" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12Z" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="12" cy="12" r="3.2" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
<svg class="zu" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12Z" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="m4 4 16 16" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<span data-zeigen-text>Anzeigen</span>
</button>
</div>
<div class="staerke leer" data-staerke>
<div class="leiste" aria-hidden="true"><i></i></div>
<div class="segmente" aria-hidden="true"><i></i><i></i><i></i><i></i></div>
<span class="bildhinweis">Die Zeichnung links wird mit jedem Punkt schärfer.</span>
<span class="urteil staerke-text" id="pw-urteil" data-urteil>Noch kein Passwort</span>
</div>
<ul class="regeln" id="pw-regeln" aria-label="Anforderungen">
<li data-regel="laenge"><span class="haken"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="m5 9.3 2.6 2.5L13 6.4" fill="none" stroke="#f9f8f8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span>Mindestens 12 Zeichen</span><span class="sr" data-sr>, nicht erfüllt</span></li>
<li data-regel="gross"><span class="haken"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="m5 9.3 2.6 2.5L13 6.4" fill="none" stroke="#f9f8f8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span>Groß- und Kleinbuchstaben</span><span class="sr" data-sr>, nicht erfüllt</span></li>
<li data-regel="ziffer"><span class="haken"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="m5 9.3 2.6 2.5L13 6.4" fill="none" stroke="#f9f8f8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span>Mindestens eine Ziffer</span><span class="sr" data-sr>, nicht erfüllt</span></li>
<li data-regel="zeichen"><span class="haken"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="m5 9.3 2.6 2.5L13 6.4" fill="none" stroke="#f9f8f8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span>Ein Sonderzeichen</span><span class="sr" data-sr>, nicht erfüllt</span></li>
<li data-regel="wort"><span class="haken"><svg viewBox="0 0 18 18" aria-hidden="true"><path d="m5 9.3 2.6 2.5L13 6.4" fill="none" stroke="#f9f8f8" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span data-wort-text>Kein gängiges Wort</span><span class="sr" data-sr>, nicht erfüllt</span></li>
</ul>
<p class="bestaetigung" data-bestaetigung aria-hidden="true"><svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="7.2" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="m4.8 8.2 2.2 2.1 4.2-4.4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>Alle Anforderungen erfüllt</p>
</div>
<div class="feld">
<label for="pw2">Passwort wiederholen</label>
<div class="eingabe"><input id="pw2" type="password" autocomplete="new-password" spellcheck="false" data-pw2></div>
<span class="wiederholen-ok" aria-live="polite" data-gleich></span>
</div>
<button class="senden" type="submit" disabled data-senden>Zugang einrichten</button>
<p class="tipp">Tipp: Vier Wörter mit Zeichen dazwischen sind stark und gut zu merken, etwa <b>Mainufer-Kaffee-um-7!</b></p>
</form>
</section>
</main>
<div class="pult">
<div class="pult-innen">
<div class="modi" role="group" aria-label="Variante">
<span class="mono">Variante</span>
<button type="button" data-variante="leiste" aria-pressed="true">Leiste</button>
<button type="button" data-variante="segmente" aria-pressed="false">Segmente</button>
<button type="button" data-variante="bild" aria-pressed="false">Bild schärft sich</button>
</div>
<p class="werte mono" data-werte></p>
</div>
</div>
<p class="sr" aria-live="polite" data-ansage></p>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
/* Fenster der Villa: zwei Geschosse mit Bögen, Erdgeschoss rechteckig */
(function fenster() {
const g = $('[data-fenster]'), ns = 'http://www.w3.org/2000/svg';
const xs = [92, 136, 254, 298];
let d = '';
xs.forEach(x => {
d += `M${x} 116V94a15 15 0 0 1 30 0v22ZM${x + 15} 79v37M${x} 100h30`;
d += `M${x} 176v-30h30v30ZM${x + 15} 146v30M${x - 3} 176h36`;
d += `M${x} 240v-36h30v36ZM${x + 15} 204v36`;
});
d += 'M190 116V96a20 20 0 0 1 40 0v20ZM210 76v40M190 176v-30h40v30ZM186 176h48';
const p = document.createElementNS(ns, 'path'); p.setAttribute('d', d); g.appendChild(p);
})();
/* ---------- Bewertung: Score 0–4 aus Zeichenvorrat und Länge, Abzug für gängige Wörter und Muster ---------- */
const GAENGIG = ['passwort', 'password', 'frankfurt', 'mandant', 'kanzlei', 'vahrenholt', 'liebrecht', 'sittig', 'qwertz', 'qwerty', 'hallo', 'sommer', 'winter', 'eintracht', 'geheim', '123456', 'abcdef'];
const STUFEN = ['Sehr schwach', 'Schwach', 'Mittel', 'Gut', 'Stark'];
function regeln(pw) {
const klein = pw.toLowerCase();
return {
laenge: pw.length >= 12,
gross: /[a-zäöüß]/.test(pw) && /[A-ZÄÖÜ]/.test(pw),
ziffer: /\d/.test(pw),
zeichen: /[^A-Za-z0-9äöüÄÖÜß\s]/.test(pw),
wort: pw.length > 0 && !GAENGIG.some(w => klein.includes(w))
};
}
function bewerte(pw) {
if (!pw) return { s: 0, leer: true, r: regeln(pw) };
let vorrat = 0;
if (/[a-zäöüß]/.test(pw)) vorrat += 26;
if (/[A-ZÄÖÜ]/.test(pw)) vorrat += 26;
if (/\d/.test(pw)) vorrat += 10;
if (/[^A-Za-z0-9äöüÄÖÜß]/.test(pw)) vorrat += 33;
let bits = pw.length * Math.log2(Math.max(vorrat, 2));
const r = regeln(pw);
if (!r.wort) bits *= .5;
if (/(.)\1\1/.test(pw)) bits -= 10;
if (/0123|1234|2345|3456|abcd|bcde/i.test(pw)) bits -= 10;
let s = bits < 30 ? 0 : bits < 50 ? 1 : bits < 70 ? 2 : bits < 90 ? 3 : 4;
const alle = Object.values(r).every(Boolean);
if (!alle) s = Math.min(s, 3);
return { s, r, alle, bits: Math.round(bits) };
}
/* ---------- Anzeige ---------- */
const form = $('[data-form]'), pw = $('[data-pw]'), pw2 = $('[data-pw2]'), feld = $('[data-feld]');
const staerke = $('[data-staerke]'), urteil = $('[data-urteil]'), segmente = $$('.segmente i');
const bestaetigung = $('[data-bestaetigung]'), gleich = $('[data-gleich]'), senden = $('[data-senden]');
let ansageUhr = 0, zuletzt = '';
function zeige() {
const e = bewerte(pw.value);
// Leiste und Segmente lesen --s; ein leeres Feld zeigt nichts, „Sehr schwach“ einen schmalen Rest
html.style.setProperty('--s', e.leer ? 0 : Math.max(e.s, .4));
staerke.classList.toggle('leer', !!e.leer);
segmente.forEach((el, i) => el.classList.toggle('an', !e.leer && i < Math.max(e.s, 1)));
const text = e.leer ? 'Noch kein Passwort' : STUFEN[e.s];
urteil.textContent = text;
// Regeln einzeln abhaken
$$('[data-regel]').forEach(li => {
const ok = e.r[li.dataset.regel];
li.toggleAttribute('data-ok', ok);
li.querySelector('[data-sr]').textContent = ok ? ', erfüllt' : ', nicht erfüllt';
li.classList.toggle('warnung', li.dataset.regel === 'wort' && !e.leer && !ok);
});
$('[data-wort-text]').textContent = !e.leer && !e.r.wort ? 'Enthält ein gängiges Wort' : 'Kein gängiges Wort';
const erfuellt = !e.leer && e.alle && e.s === 4;
feld.classList.toggle('erfuellt', erfuellt);
bestaetigung.setAttribute('aria-hidden', String(!erfuellt));
const passt = erfuellt && pw2.value === pw.value;
gleich.textContent = pw2.value ? (pw2.value === pw.value ? 'Stimmt überein.' : '') : '';
senden.disabled = !passt;
// Ansage gebündelt, damit Screenreader nicht jede Taste vorlesen
clearTimeout(ansageUhr);
ansageUhr = setTimeout(() => {
const a = e.leer ? '' : `Passwortstärke: ${text}${erfuellt ? '. Alle Anforderungen erfüllt' : ''}`;
if (a !== zuletzt) { zuletzt = a; $('[data-ansage]').textContent = a; }
}, 600);
zeigeWerte(e);
}
pw.addEventListener('input', zeige);
pw2.addEventListener('input', zeige);
const zeigen = $('[data-zeigen]');
function sichtbar(an) {
pw.type = an ? 'text' : 'password'; pw2.type = pw.type;
zeigen.setAttribute('aria-pressed', String(an));
$('[data-zeigen-text]').textContent = an ? 'Verbergen' : 'Anzeigen';
}
zeigen.addEventListener('click', () => sichtbar(pw.type === 'password'));
form.addEventListener('submit', e => { e.preventDefault(); senden.textContent = 'Zugang eingerichtet'; });
$$('a[href^="#"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));
/* ---------- Varianten und Werte ---------- */
let variante = 'leiste';
const werte = $('[data-werte]');
function zeigeWerte(e = bewerte(pw.value)) {
const s = e.leer ? '–' : e.s;
const art = { leiste: 'Leiste <b>scaleX(--s / 4)</b>', segmente: '<b>4 Segmente</b>, je scaleX 0 → 1', bild: 'Zeichnung <b>blur((4 − s) × 3,5 px)</b>, Deckkraft <b>0,35 → 1</b>' }[variante];
werte.innerHTML = reduziert()
? `<span>Reduziert: <b>ohne Transition</b>, Zustände springen</span><span>Score <b>${s}</b> / 4</span>`
: `<span>${art}</span><span>Farbe <b>color-mix(oklab)</b> #c8423f → #2f7d55</span><span><b>250 ms</b> cubic-bezier(0.4, 0, 0.22, 1)</span><span>Häkchen scale <b>0 → 1</b>, 250 ms</span><span>Score <b>${s}</b> / 4${e.bits != null ? ` · ${e.bits} bit` : ''}</span>`;
}
function setzeVariante(v) {
variante = v;
html.classList.remove('var-leiste', 'var-segmente', 'var-bild'); html.classList.add('var-' + v);
$$('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
zeigeWerte();
}
$$('[data-variante]').forEach(b => b.addEventListener('click', () => setzeVariante(b.dataset.variante)));
new MutationObserver(() => zeigeWerte()).observe(html, { attributes: true, attributeFilter: ['class'] });
/* ---------- Vorführung: simuliertes Tippen ---------- */
const REIHE = ['leiste', 'segmente', 'bild'];
let uhren = [], runde = 0, laeuft = false;
const spaeter = (fn, ms) => uhren.push(setTimeout(fn, ms));
function tippe(text, ab, takt) { [...text].forEach((z, i) => spaeter(() => { pw.value += z; zeige(); }, ab + i * takt)); return ab + text.length * takt; }
function loesche(ab, takt) { const n = pw.value.length || 24; for (let i = 0; i < n; i++) spaeter(() => { pw.value = pw.value.slice(0, -1); zeige(); }, ab + i * takt); return ab + n * takt; }
function leeren() { pw.value = ''; pw2.value = ''; senden.textContent = 'Zugang einrichten'; zeige(); }
function runde_spielen() {
leeren(); setzeVariante(REIHE[runde % 3]);
let t = 250;
t = tippe('Mainufer', t, 85) + 450;
t = tippe('-Kaffee', t, 85) + 900;
t = tippe('-um-7!', t, 95) + 250;
spaeter(() => { pw2.value = pw.value; zeige(); }, t);
t += 2000;
// Gegenprobe: gängiges Wort
const n = 'Mainufer-Kaffee-um-7!'.length;
spaeter(() => { pw2.value = ''; zeige(); }, t);
t = (function () { for (let i = 0; i < n; i++) spaeter(() => { pw.value = pw.value.slice(0, -1); zeige(); }, t + i * 22); return t + n * 22; })() + 300;
t = tippe('Frankfurt2026', t, 75) + 1600;
spaeter(() => { runde++; runde_spielen(); }, t);
}
function pause() { uhren.forEach(clearTimeout); uhren = []; laeuft = false; }
function vorfuehren() { pause(); html.classList.add('vorfuehrung'); laeuft = true; sichtbar(true); runde_spielen(); }
function weiter() { if (!laeuft && html.classList.contains('vorfuehrung')) { laeuft = true; runde_spielen(); } }
function neustart() {
pause(); html.classList.remove('vorfuehrung'); runde = 0; leeren(); setzeVariante(variante);
sichtbar(true);
let t = tippe('Mainufer-Kaffee-um-7!', 300, 70);
spaeter(() => sichtbar(false), t + 1200);
}
setzeVariante('leiste'); zeige();
})();
</script>
</body>
</html>