Design · Bausteine-Dossier

Kategorie

Formulare

Barrierefreie, DSGVO-saubere Formular-Bausteine: sichtbare <label>, deutliche Fokus-Ringe, eigene Steuerelemente und Spamschutz ohne Captcha. Akzentfarbe über --akzent in einer Zeile anpassbar.

Felder: Fokus-Ring + Custom-Select-Pfeil + Autofill-Fix

Pol 2 · Klarheit Anlass: Kontaktformular

Input, Select und Textarea mit weichem box-shadow-Fokus-Ring in der Akzentfarbe, eigenem Dropdown-Pfeil als inline data:-SVG und Autofill-Override gegen den weißen Browser-Hintergrund. Sichtbare Labels (BFSG). Vorbild: NDS, Silver Mountain, Bernstein.

<form class="fx-fields" aria-label="Beispielfelder">
  <div class="field">
    <label for="fx-name">Name</label>
    <input type="text" id="fx-name" placeholder="Vor- und Nachname">
  </div>
  <div class="field">
    <label for="fx-leistung">Leistung</label>
    <select id="fx-leistung">
      <option>Bitte wählen</option>
      <option>Küchenmontage</option>
      <option>Beratung vor Ort</option>
    </select>
  </div>
  <div class="field">
    <label for="fx-nachricht">Nachricht</label>
    <textarea id="fx-nachricht" placeholder="Worum geht es?"></textarea>
  </div>
</form>

<style>
.fx-fields{display:flex;flex-direction:column;gap:14px;max-width:380px;text-align:left}
.fx-fields .field{display:flex;flex-direction:column;gap:6px}
.fx-fields label{font-size:.8rem;font-weight:600;color:#a1a1aa;letter-spacing:.02em}
.fx-fields input,.fx-fields select,.fx-fields textarea{width:100%;padding:11px 14px;
  background:#16161a;border:1px solid #26262c;border-radius:10px;color:#e9e9ee;font:inherit;font-size:.92rem;
  outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;
  transition:border-color .2s ease,box-shadow .2s ease,background .2s ease}
.fx-fields input::placeholder,.fx-fields textarea::placeholder{color:#7a7a82}
.fx-fields input:focus,.fx-fields select:focus,.fx-fields textarea:focus{
  border-color:var(--akzent, #7c87ff);background:#17171b;
  box-shadow:0 0 0 3px color-mix(in srgb,var(--akzent, #7c87ff) 22%,transparent)}
.fx-fields textarea{resize:vertical;min-height:84px;line-height:1.55}
.fx-fields select{cursor:pointer;padding-right:38px;
  background-image:url("data:image/svg+xml,%3Csvg width='12' height='7' viewBox='0 0 12 7' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%237c87ff' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 14px center}
.fx-fields select option{background:#16161a;color:#e9e9ee}
/* Autofill-Override (Chrome/Edge/Safari) — sonst weißer Browser-Hintergrund */
.fx-fields input:-webkit-autofill,.fx-fields input:-webkit-autofill:hover,
.fx-fields input:-webkit-autofill:focus,.fx-fields textarea:-webkit-autofill{
  -webkit-box-shadow:0 0 0 1000px #16161a inset!important;-webkit-text-fill-color:#e9e9ee!important;
  caret-color:#e9e9ee!important;border:1px solid #26262c!important;transition:background-color 9999s ease 0s}
@media (prefers-reduced-motion:reduce){.fx-fields input,.fx-fields select,.fx-fields textarea{transition:none}}
</style>

Custom Consent-Checkbox (DSGVO)

Pol 2 · Klarheit Anlass: Einwilligung Kontaktformular

Eigene Checkbox per appearance:none — Browser-Default ist auf dunklem Grund kaum sichtbar. Box mit Akzent-Border, CSS-Häkchen bei :checked, daneben DSGVO-Hinweis mit Datenschutz-Link. Eigener Fokus-Ring für Tastatur. Vorbild: Silver Mountain, Bernstein.

<label class="fx-consent">
  <input type="checkbox" name="consent">
  <span>Ich habe die <a href="#">Datenschutzerklärung</a> gelesen und willige in die
    Verarbeitung meiner Angaben zur Bearbeitung der Anfrage ein. Die Einwilligung ist
    jederzeit widerrufbar.</span>
</label>

<style>
.fx-consent{display:flex;gap:12px;align-items:flex-start;max-width:420px;
  font-size:.83rem;color:#a1a1aa;line-height:1.55;cursor:pointer}
.fx-consent>span{flex:1 1 auto;min-width:0}
.fx-consent input[type="checkbox"]{-webkit-appearance:none;appearance:none;flex:0 0 auto;
  width:20px;height:20px;margin:1px 0 0;background:#16161a;border:1.5px solid var(--akzent, #7c87ff);
  border-radius:4px;cursor:pointer;position:relative;transition:background .2s ease,border-color .2s ease}
.fx-consent input[type="checkbox"]:hover{background:color-mix(in srgb,var(--akzent, #7c87ff) 14%,transparent)}
.fx-consent input[type="checkbox"]:focus-visible{outline:2px solid var(--akzent, #7c87ff);outline-offset:2px}
.fx-consent input[type="checkbox"]:checked{background:var(--akzent, #7c87ff);border-color:var(--akzent, #7c87ff)}
.fx-consent input[type="checkbox"]:checked::after{content:"";position:absolute;left:6px;top:2px;
  width:5px;height:10px;border:solid #0f0f12;border-width:0 2px 2px 0;transform:rotate(45deg)}
.fx-consent a{color:var(--akzent, #7c87ff);text-decoration:underline;text-underline-offset:3px}
@media (prefers-reduced-motion:reduce){.fx-consent input[type="checkbox"]{transition:none}}
</style>

Spamschutz: Honeypot + Render-Zeit

Pol 2 · Klarheit Anlass: Formular gegen Bots

Captcha-frei: ein off-screen Honeypot-Feld (für Menschen unsichtbar, Bots füllen es aus) plus ein verstecktes Timestamp-Feld. Das JS setzt die Render-Zeit und lehnt zu schnelle Sendungen still ab. Im echten Projekt prüft der Server dieselben Werte erneut. Vorbild: Nördlicht, NDS. Live-Demo unten zeigt die Reaktion statt zu versenden.

<form class="fx-spam" aria-label="Beispiel mit Spamschutz">
  <!-- Honeypot: NICHT ausfüllen — für Menschen unsichtbar -->
  <div class="fx-hp" aria-hidden="true">
    <label for="fx-website">Website (bitte leer lassen)</label>
    <input type="text" id="fx-website" name="website" tabindex="-1" autocomplete="off">
  </div>
  <input type="hidden" name="rendered_at" value="">
  <div class="field">
    <label for="fx-email">E-Mail</label>
    <input type="email" id="fx-email" placeholder="name@beispiel.de">
  </div>
  <button type="submit">Anfrage senden</button>
  <p class="fx-spam-status" role="status" aria-live="polite"></p>
</form>

<style>
.fx-spam{display:flex;flex-direction:column;gap:12px;max-width:380px;text-align:left}
.fx-spam .field{display:flex;flex-direction:column;gap:6px}
.fx-spam label{font-size:.8rem;font-weight:600;color:#a1a1aa}
.fx-spam input{width:100%;padding:11px 14px;background:#16161a;border:1px solid #26262c;border-radius:10px;
  color:#e9e9ee;font:inherit;font-size:.92rem;outline:none;transition:border-color .2s ease,box-shadow .2s ease}
.fx-spam input:focus{border-color:var(--akzent, #7c87ff);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--akzent, #7c87ff) 22%,transparent)}
/* Honeypot: für Menschen unsichtbar, Bots füllen es aus */
.fx-hp{position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden}
.fx-spam button{margin-top:2px;padding:11px 16px;border:1px solid var(--akzent, #7c87ff);border-radius:10px;
  background:var(--akzent, #7c87ff);color:#0f0f12;font:inherit;font-weight:600;font-size:.9rem;cursor:pointer;
  transition:filter .2s ease}
.fx-spam button:hover,.fx-spam button:focus-visible{filter:brightness(1.08)}
.fx-spam-status{font-size:.82rem;color:#a1a1aa;min-height:1.2em}
@media (prefers-reduced-motion:reduce){.fx-spam input,.fx-spam button{transition:none}}
</style>

<script>
document.querySelectorAll('.fx-spam').forEach(function(form){
  var renderedAt=Date.now();
  var ts=form.querySelector('[name="rendered_at"]'); if(ts)ts.value=String(renderedAt);
  var status=form.querySelector('.fx-spam-status');
  form.addEventListener('submit',function(ev){
    ev.preventDefault();
    var trap=form.querySelector('[name="website"]');
    if(trap&&trap.value){status.textContent='Abgelehnt (Honeypot ausgefüllt).';return;}
    if(Date.now()-renderedAt<1500){status.textContent='Zu schnell — vermutlich ein Bot. Abgelehnt.';return;}
    // Hier würde der echte Versand an form.action erfolgen (Server prüft erneut)
    status.textContent='Geprüft — bereit zum Versand.';
  });
});
</script>

Formular-Status: Erfolg / Fehler inline

Pol 2 · Klarheit Anlass: Rückmeldung nach Versand

Farbcodierte Banner direkt im Formular: eine Fehlerbox bei fehlgeschlagenem Versand, eine Erfolgsbox die das Formular per .is-visible ersetzt. role="alert" bzw. role="status" für Screenreader. Vorbild: Hauszeit, NDS. Die Knöpfe schalten die Zustände nur zur Demo um.

Anfrage gesendet

Vielen Dank — wir melden uns innerhalb von 24 Stunden.

<div class="fx-form-error" role="alert">
  Versand fehlgeschlagen. Bitte versuchen Sie es erneut oder rufen Sie uns an:
  <a href="#">030 123 456</a>.
</div>

<div class="fx-form-success" role="status">
  <div class="icon" aria-hidden="true">✓</div>
  <h4>Anfrage gesendet</h4>
  <p>Vielen Dank — wir melden uns innerhalb von 24 Stunden.</p>
</div>
<!-- bei Erfolg: form.style.display='none'; success.classList.add('is-visible') -->

<style>
.fx-form-error{background:color-mix(in srgb,#dc2626 14%,#16161a);
  border:1px solid color-mix(in srgb,#dc2626 45%,transparent);
  color:#fca5a5;padding:11px 15px;border-radius:10px;font-size:.85rem;line-height:1.5}
.fx-form-error a{color:#fca5a5;font-weight:600}
.fx-form-success{display:none;text-align:center;padding:26px 18px;
  background:color-mix(in srgb,var(--akzent, #7c87ff) 10%,#16161a);
  border:1px solid color-mix(in srgb,var(--akzent, #7c87ff) 40%,transparent);border-radius:12px}
.fx-form-success.is-visible{display:block}
.fx-form-success .icon{font-size:2rem;color:var(--akzent, #7c87ff);line-height:1}
.fx-form-success h4{margin:.4rem 0 .2rem;color:#f4f4f5;font-size:1rem}
.fx-form-success p{margin:0;color:#a1a1aa;font-size:.85rem}
</style>

Hinweis-/Warn-Callout-Box

Pol 2 · Klarheit Anlass: Hinweis im Formular / FAQ

Getönte Box mit linker Akzentkante für wichtige Hinweise (z. B. Datenschutz, Bearbeitungsdauer). role="note" macht sie für Screenreader als Anmerkung kenntlich. Vorbild: Nördlicht.

Hinweis: Bitte senden Sie keine sensiblen Daten (z. B. Passwörter oder Zahlungsangaben) über dieses Formular. Für vertrauliche Anliegen vereinbaren wir gern einen Rückruf.
<div class="fx-callout" role="note">
  <span class="mark" aria-hidden="true">!</span>
  <span><strong>Hinweis:</strong> Bitte senden Sie keine sensiblen Daten (z. B. Passwörter
    oder Zahlungsangaben) über dieses Formular. Für vertrauliche Anliegen vereinbaren wir
    gern einen Rückruf.</span>
</div>

<style>
.fx-callout{max-width:480px;display:flex;gap:12px;align-items:flex-start;
  background:color-mix(in srgb,var(--akzent, #7c87ff) 8%,#16161a);
  border:1px solid color-mix(in srgb,var(--akzent, #7c87ff) 22%,transparent);
  border-left:4px solid var(--akzent, #7c87ff);border-radius:10px;padding:14px 16px;
  font-size:.84rem;line-height:1.55;color:#c9c9d2}
.fx-callout .mark{flex:0 0 auto;color:var(--akzent, #7c87ff);font-weight:700}
.fx-callout strong{color:#f4f4f5}
</style>

Mehrstufiger Wizard mit Fortschrittsbalken

Pol 1 · Craft Anlass: geführte Anfrage / Konfigurator

Mehrschritt-Formular: Schritte per .is-active, animierter Progress-Fill, Radio-Optionen als ganze klickbare Karten via :has(input:checked). Kleines JS für Schrittwechsel. Vorbild: NDS. Kompakt auf drei Schritte reduziert.

Schritt 1 von 333 %

Leistung

Worum geht es?

Zeitraum

Wann soll es losgehen?

Fertig

Angaben prüfen

Alles erfasst. Im echten Projekt folgen hier Kontaktfelder und der Senden-Knopf.
<form class="fx-wizard" aria-label="Beispiel-Wizard">
  <div class="fx-wiz-progress">
    <div class="lbl"><span class="step-now">Schritt 1 von 3</span><span class="pct">33 %</span></div>
    <div class="fx-wiz-track"><span class="fx-wiz-fill"></span></div>
  </div>

  <div class="fx-wiz-step is-active" data-step="1">
    <p class="eb">Leistung</p>
    <h4>Worum geht es?</h4>
    <div class="fx-wiz-options">
      <label class="fx-wiz-option"><input type="radio" name="w-leistung"><span class="t">Küchenmontage</span></label>
      <label class="fx-wiz-option"><input type="radio" name="w-leistung"><span class="t">Möbelaufbau</span></label>
      <label class="fx-wiz-option"><input type="radio" name="w-leistung"><span class="t">Beratung vor Ort</span></label>
    </div>
  </div>

  <div class="fx-wiz-step" data-step="2"> … weitere Optionen … </div>
  <div class="fx-wiz-step" data-step="3"> … Abschluss … </div>

  <div class="fx-wiz-nav">
    <button type="button" class="back" hidden>Zurück</button>
    <button type="button" class="next">Weiter</button>
  </div>
</form>

<style>
.fx-wizard{max-width:460px;background:#16161a;border:1px solid #26262c;border-radius:16px;padding:24px 22px;text-align:left}
.fx-wiz-progress{margin-bottom:20px}
.fx-wiz-progress .lbl{display:flex;justify-content:space-between;font-size:.78rem;color:#a1a1aa;font-weight:600;margin-bottom:8px}
.fx-wiz-track{width:100%;height:6px;background:#1c1c22;border-radius:99px;overflow:hidden}
.fx-wiz-fill{height:100%;width:33%;background:var(--akzent, #7c87ff);border-radius:99px;transition:width .35s cubic-bezier(.4,0,.2,1)}
.fx-wiz-step{display:none}
.fx-wiz-step.is-active{display:block}
.fx-wiz-step .eb{font-size:.74rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--akzent, #7c87ff);margin-bottom:8px}
.fx-wiz-step h4{margin:0 0 16px;font-size:1.05rem;color:#f4f4f5;line-height:1.3}
.fx-wiz-options{display:flex;flex-direction:column;gap:10px}
.fx-wiz-option{display:flex;align-items:center;gap:12px;padding:13px 15px;background:#1c1c22;
  border:1px solid #26262c;border-radius:10px;cursor:pointer;transition:border-color .2s ease,background .2s ease}
.fx-wiz-option:hover{border-color:color-mix(in srgb,var(--akzent, #7c87ff) 60%,transparent)}
.fx-wiz-option input[type="radio"]{accent-color:var(--akzent, #7c87ff);width:18px;height:18px;flex:0 0 auto}
.fx-wiz-option input[type="radio"]:focus-visible{outline:2px solid var(--akzent, #7c87ff);outline-offset:2px}
.fx-wiz-option .t{color:#e9e9ee;font-size:.92rem;font-weight:500}
/* ganze Karte als Auswahl-Indikator via :has() */
.fx-wiz-option:has(input:checked){border-color:var(--akzent, #7c87ff);background:color-mix(in srgb,var(--akzent, #7c87ff) 10%,#1c1c22)}
.fx-wiz-option:has(input:checked) .t{color:var(--akzent, #7c87ff);font-weight:700}
.fx-wiz-nav{display:flex;justify-content:space-between;gap:12px;margin-top:22px}
.fx-wiz-nav button{padding:10px 20px;border-radius:10px;font:inherit;font-size:.88rem;font-weight:600;cursor:pointer;transition:filter .2s ease,border-color .2s ease}
.fx-wiz-nav .back{background:#1c1c22;border:1px solid #26262c;color:#e9e9ee}
.fx-wiz-nav .next{background:var(--akzent, #7c87ff);border:1px solid var(--akzent, #7c87ff);color:#0f0f12;margin-left:auto}
@media (prefers-reduced-motion:reduce){.fx-wiz-fill,.fx-wiz-option,.fx-wiz-nav button{transition:none}}
</style>

<script>
document.querySelectorAll('.fx-wizard').forEach(function(wiz){
  var steps=wiz.querySelectorAll('.fx-wiz-step');
  var fill=wiz.querySelector('.fx-wiz-fill');
  var lbl=wiz.querySelector('.step-now'), pct=wiz.querySelector('.pct');
  var back=wiz.querySelector('.back'), next=wiz.querySelector('.next');
  var i=0;
  function render(){
    steps.forEach(function(s,n){s.classList.toggle('is-active',n===i);});
    var p=Math.round((i+1)/steps.length*100);
    fill.style.width=p+'%'; pct.textContent=p+' %';
    lbl.textContent='Schritt '+(i+1)+' von '+steps.length;
    back.hidden=i===0;
    next.textContent=i===steps.length-1?'Absenden':'Weiter';
  }
  next.addEventListener('click',function(){if(i<steps.length-1){i++;render();}});
  back.addEventListener('click',function(){if(i>0){i--;render();}});
  render();
});
</script>

Fortschrittsbalken (determiniert + indeterminate)

Pol 2 · Klarheit Anlass: Upload / Ladezustand, Wizard-Fortschritt

Zwei Varianten: ein determinierter Balken, dessen Füllung sich weich auf einen Zielwert schiebt (hier 68 %), und ein indeterminater Balken mit endlos laufendem Streifen für unbekannte Dauer. role="progressbar" mit aria-valuenow/min/max. Kleines JS setzt beim Laden die Zielbreite. Vorbild: animate-ui „Progress".

Upload läuft68 %
Wird verarbeitet…
<div class="fx-prog">
  <div class="row">
    <div class="lbl"><span>Upload läuft</span><span class="val">68 %</span></div>
    <div class="prog-track">
      <span class="prog-fill" role="progressbar" aria-valuenow="68" aria-valuemin="0"
        aria-valuemax="100" aria-label="Upload-Fortschritt" data-value="68"></span>
    </div>
  </div>
  <div class="row">
    <div class="lbl"><span>Wird verarbeitet…</span></div>
    <div class="prog-track is-indeterminate">
      <span class="prog-fill" role="progressbar" aria-label="Wird verarbeitet" aria-valuetext="Läuft"></span>
    </div>
  </div>
</div>

<style>
.fx-prog{max-width:400px;display:flex;flex-direction:column;gap:22px;text-align:left}
.fx-prog .row .lbl{display:flex;justify-content:space-between;font-size:.8rem;font-weight:600;
  color:#a1a1aa;margin-bottom:8px}
.fx-prog .row .lbl .val{color:var(--akzent, #7c87ff)}
.prog-track{width:100%;height:8px;background:#1c1c22;border-radius:99px;overflow:hidden}
.prog-fill{height:100%;width:0;background:var(--akzent, #7c87ff);border-radius:99px;
  transition:width .6s cubic-bezier(.4,0,.2,1)}
/* indeterminate: endlos laufender Streifen */
.prog-track.is-indeterminate .prog-fill{width:40%;
  background:linear-gradient(90deg,transparent,var(--akzent, #7c87ff),transparent);
  animation:prog-slide 1.4s ease-in-out infinite}
@keyframes prog-slide{0%{transform:translateX(-100%)}100%{transform:translateX(350%)}}
@media (prefers-reduced-motion:reduce){
  .prog-fill{transition:none}
  .prog-track.is-indeterminate .prog-fill{animation:none;width:40%}
}
</style>

<script>
// determinierten Balken auf data-value schieben (Animation ab Laden)
document.querySelectorAll('.prog-fill[data-value]').forEach(function(el){
  requestAnimationFrame(function(){ el.style.width = el.dataset.value + '%'; });
});
</script>

Toggle-Switch

Pol 2 · Klarheit Anlass: Einstellungen, Consent-Optionen

Echter <input type="checkbox">, visuell versteckt per sr-only-Technik (nicht display:none — bleibt fokussierbar). Der Knopf gleitet, die Bahn färbt sich bei :checked in der Akzentfarbe. Label klickbar, per Leertaste schaltbar, eigener Fokus-Ring am Schalter. Reines CSS ohne JS. Vorbild: animate-ui „Switch".

<label class="fx-switch">
  <input type="checkbox" checked>
  <span class="track"><span class="knob"></span></span>
  <span>Newsletter abonnieren</span>
</label>

<style>
.fx-switch{display:inline-flex;align-items:center;gap:12px;cursor:pointer;
  font-size:.9rem;color:#e9e9ee;-webkit-user-select:none;user-select:none}
/* echtes Input visuell versteckt (nicht display:none — bleibt fokussierbar) */
.fx-switch input{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}
.fx-switch .track{position:relative;flex:0 0 auto;width:46px;height:26px;background:#1c1c22;
  border:1px solid #26262c;border-radius:99px;transition:background .25s ease,border-color .25s ease}
.fx-switch .knob{position:absolute;top:2px;left:2px;width:20px;height:20px;background:#a1a1aa;
  border-radius:50%;transition:transform .25s cubic-bezier(.4,0,.2,1),background .25s ease}
.fx-switch:hover .track{border-color:#3a3a45}
.fx-switch input:checked+.track{background:var(--akzent, #7c87ff);border-color:var(--akzent, #7c87ff)}
.fx-switch input:checked+.track .knob{transform:translateX(20px);background:#0f0f12}
.fx-switch input:focus-visible+.track{outline:2px solid var(--akzent, #7c87ff);outline-offset:3px}
@media (prefers-reduced-motion:reduce){.fx-switch .track,.fx-switch .knob{transition:none}}
</style>

Animierte Checkbox (Häkchen wird gezeichnet)

Pol 2 · Klarheit Anlass: Formular-Zustimmung, Auswahllisten

Echte Checkbox (sr-only versteckt), beim Anhaken zeichnet sich das Häkchen per stroke-dasharray/stroke-dashoffset als Inline-SVG, die Box füllt sich in der Akzentfarbe. Label klickbar, eigener Fokus-Ring. Bei reduced-motion erscheint das Häkchen sofort ohne Zeichnen. Vorbild: animate-ui „Checkbox".

<label class="fx-check">
  <input type="checkbox" checked>
  <span class="box"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7"/></svg></span>
  <span>Ich stimme den Nutzungsbedingungen zu und möchte fortfahren.</span>
</label>

<style>
.fx-check{display:inline-flex;align-items:flex-start;gap:12px;cursor:pointer;max-width:420px;
  font-size:.88rem;line-height:1.55;color:#e9e9ee;-webkit-user-select:none;user-select:none}
.fx-check input{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
  clip:rect(0 0 0 0);white-space:nowrap;border:0}
.fx-check .box{position:relative;flex:0 0 auto;width:22px;height:22px;margin-top:1px;background:#16161a;
  border:1.5px solid #26262c;border-radius:6px;transition:background .2s ease,border-color .2s ease}
.fx-check .box svg{position:absolute;inset:0;width:100%;height:100%}
.fx-check .box svg path{stroke:#0f0f12;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round;
  fill:none;stroke-dasharray:22;stroke-dashoffset:22;transition:stroke-dashoffset .3s ease .05s}
.fx-check:hover .box{border-color:color-mix(in srgb,var(--akzent, #7c87ff) 60%,#26262c)}
.fx-check input:checked+.box{background:var(--akzent, #7c87ff);border-color:var(--akzent, #7c87ff)}
.fx-check input:checked+.box svg path{stroke-dashoffset:0}
.fx-check input:focus-visible+.box{outline:2px solid var(--akzent, #7c87ff);outline-offset:3px}
@media (prefers-reduced-motion:reduce){.fx-check .box,.fx-check .box svg path{transition:none}}
</style>

Floating-Label-Feld

Pol 1 · Craft Anlass: schlankes Kontaktformular

Das Label sitzt zunächst als Platzhalter im Feld und wandert bei Fokus oder Eingabe nach oben — reines CSS über :not(:placeholder-shown), kein JS. Das echte <label> bleibt sichtbar (BFSG), das placeholder=" " ist nur der Schalter. Vorbild: Material-Textfeld.

<div class="fx-float">
  <input type="email" id="fx-float-mail" placeholder=" ">
  <label for="fx-float-mail">E-Mail-Adresse</label>
</div>

<style>
.fx-float{position:relative;max-width:380px;text-align:left}
.fx-float input{width:100%;padding:20px 14px 8px;background:#16161a;border:1px solid #26262c;
  border-radius:10px;color:#e9e9ee;font:inherit;font-size:.95rem;outline:none;
  transition:border-color .2s ease,box-shadow .2s ease}
.fx-float label{position:absolute;left:15px;top:15px;color:#7a7a82;font-size:.95rem;pointer-events:none;
  transform-origin:left top;transition:transform .18s ease,color .18s ease}
/* Label hoch, sobald Feld fokussiert ODER befüllt ist (:not(:placeholder-shown)) */
.fx-float input:focus+label,
.fx-float input:not(:placeholder-shown)+label{transform:translateY(-9px) scale(.74);color:var(--akzent, #7c87ff)}
.fx-float input:focus{border-color:var(--akzent, #7c87ff);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--akzent, #7c87ff) 22%,transparent)}
@media (prefers-reduced-motion:reduce){.fx-float input,.fx-float label{transition:none}}
</style>

Passwort-Stärke-Anzeige + Sichtbarkeit

Pol 1 · Craft Anlass: Registrierung / Passwort setzen

Vier Segment-Balken färben sich live nach geschätzter Stärke (Länge, Groß-/Kleinbuchstaben, Ziffern, Sonderzeichen), begleitet von einer Textstufe. Der Augen-Knopf schaltet die Sichtbarkeit per aria-pressed um. Rein clientseitige Orientierung, keine Sicherheitsprüfung. Vorbild: gängige Sign-up-Felder.

Noch keine Eingabe.

<div class="fx-pw" data-strength="0">
  <label for="fx-pw-in">Passwort wählen</label>
  <div class="wrap-in">
    <input type="password" id="fx-pw-in" placeholder="mindestens 8 Zeichen" autocomplete="new-password">
    <button type="button" class="toggle" aria-pressed="false" aria-label="Passwort anzeigen">
      <svg class="eye-on" viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"/><circle cx="12" cy="12" r="3"/></svg>
      <svg class="eye-off" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 3l18 18"/><path d="M10.6 5.2A10.9 10.9 0 0 1 12 5c6.5 0 10 7 10 7a17.7 17.7 0 0 1-3.4 4.3M6.6 6.6A17.7 17.7 0 0 0 2 12s3.5 7 10 7a10.8 10.8 0 0 0 3.9-.7"/></svg>
    </button>
  </div>
  <div class="bars" aria-hidden="true"><span></span><span></span><span></span><span></span></div>
  <p class="hint" role="status" aria-live="polite">Noch keine Eingabe.</p>
</div>

<style>
.fx-pw{max-width:380px;display:flex;flex-direction:column;gap:8px;text-align:left}
.fx-pw label{font-size:.8rem;font-weight:600;color:#a1a1aa}
.fx-pw .wrap-in{position:relative}
.fx-pw input{width:100%;padding:11px 44px 11px 14px;background:#16161a;border:1px solid #26262c;
  border-radius:10px;color:#e9e9ee;font:inherit;font-size:.92rem;outline:none;
  transition:border-color .2s ease,box-shadow .2s ease}
.fx-pw input:focus{border-color:var(--akzent, #7c87ff);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--akzent, #7c87ff) 22%,transparent)}
.fx-pw .toggle{position:absolute;right:6px;top:50%;transform:translateY(-50%);display:flex;
  align-items:center;justify-content:center;width:32px;height:32px;background:none;border:0;
  color:#a1a1aa;cursor:pointer;border-radius:8px;transition:color .2s ease,background .2s ease}
.fx-pw .toggle:hover,.fx-pw .toggle:focus-visible{color:#e9e9ee;background:#1c1c22;outline:none}
.fx-pw .toggle:focus-visible{outline:2px solid var(--akzent, #7c87ff);outline-offset:2px}
.fx-pw .toggle svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round}
.fx-pw .toggle .eye-off{display:none}
.fx-pw .toggle[aria-pressed="true"] .eye-on{display:none}
.fx-pw .toggle[aria-pressed="true"] .eye-off{display:block}
.fx-pw .bars{display:flex;gap:6px}
.fx-pw .bars span{flex:1;height:5px;border-radius:99px;background:#26262c;transition:background .3s ease}
.fx-pw[data-strength="1"] .bars span:nth-child(-n+1),
.fx-pw[data-strength="2"] .bars span:nth-child(-n+2),
.fx-pw[data-strength="3"] .bars span:nth-child(-n+3),
.fx-pw[data-strength="4"] .bars span:nth-child(-n+4){background:var(--pw-col,#a1a1aa)}
.fx-pw[data-strength="1"]{--pw-col:#dc2626}
.fx-pw[data-strength="2"]{--pw-col:#d97706}
.fx-pw[data-strength="3"]{--pw-col:#ca8a04}
.fx-pw[data-strength="4"]{--pw-col:var(--akzent, #7c87ff)}
.fx-pw .hint{font-size:.78rem;color:#a1a1aa;min-height:1.1em}
.fx-pw .hint b{color:var(--pw-col,#e9e9ee)}
@media (prefers-reduced-motion:reduce){.fx-pw input,.fx-pw .toggle,.fx-pw .bars span{transition:none}}
</style>

<script>
document.querySelectorAll('.fx-pw').forEach(function(box){
  var input=box.querySelector('input');
  var toggle=box.querySelector('.toggle');
  var hint=box.querySelector('.hint');
  var labels=['','Sehr schwach','Schwach','Solide','Stark'];
  input.addEventListener('input',function(){
    var v=input.value, s=0;
    if(v.length>=8)s++;
    if(/[a-z]/.test(v)&&/[A-Z]/.test(v))s++;
    if(/\d/.test(v))s++;
    if(/[^A-Za-z0-9]/.test(v))s++;
    if(v.length===0)s=0;
    box.dataset.strength=String(s);
    hint.innerHTML=v.length===0?'Noch keine Eingabe.':'Stärke: <b>'+labels[s]+'</b>';
  });
  toggle.addEventListener('click',function(){
    var show=toggle.getAttribute('aria-pressed')==='true';
    toggle.setAttribute('aria-pressed',String(!show));
    input.type=show?'password':'text';
    toggle.setAttribute('aria-label',show?'Passwort anzeigen':'Passwort verbergen');
  });
});
</script>

Datei-Drop-Zone

Pol 1 · Craft Anlass: Anhang / Foto-Upload

Gestrichelte Fläche zum Klicken oder Hineinziehen von Dateien; ein echtes <input type="file"> bleibt per sr-only-Technik fokussierbar (BFSG). Beim Überfahren hebt sich der Rand hervor, gewählte Dateien erscheinen als Liste mit Namen und Größe. Kein Versand — nur Auswahl-Anzeige.

    <div class="fx-drop">
      <label>
        <input type="file" multiple aria-label="Dateien auswählen">
        <span class="zone">
          <svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 16V4m0 0L8 8m4-4l4 4"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>
          <span class="t">Dateien hierher ziehen oder <u>durchsuchen</u></span>
          <span class="sub">PDF, JPG oder PNG · bis 10 MB</span>
        </span>
      </label>
      <ul class="files" aria-live="polite"></ul>
    </div>
    
    <style>
    .fx-drop{max-width:400px;text-align:left}
    .fx-drop .zone{display:flex;flex-direction:column;align-items:center;gap:10px;padding:28px 20px;
      background:#16161a;border:1.5px dashed #3a3a45;border-radius:14px;text-align:center;cursor:pointer;
      transition:border-color .2s ease,background .2s ease}
    .fx-drop .zone:hover,.fx-drop .zone.is-over,.fx-drop input:focus-visible+.zone{
      border-color:var(--akzent, #7c87ff);background:color-mix(in srgb,var(--akzent, #7c87ff) 8%,#16161a)}
    .fx-drop input{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
      clip:rect(0 0 0 0);white-space:nowrap;border:0}
    .fx-drop .ic{width:34px;height:34px;fill:none;stroke:var(--akzent, #7c87ff);stroke-width:1.6;
      stroke-linecap:round;stroke-linejoin:round}
    .fx-drop .t{color:#e9e9ee;font-size:.92rem;font-weight:600}
    .fx-drop .t u{color:var(--akzent, #7c87ff);text-underline-offset:3px}
    .fx-drop .sub{color:#a1a1aa;font-size:.78rem}
    .fx-drop .files{list-style:none;margin:12px 0 0;padding:0;display:flex;flex-direction:column;gap:8px}
    .fx-drop .files li{display:flex;align-items:center;gap:10px;padding:9px 12px;background:#1c1c22;
      border:1px solid #26262c;border-radius:10px;font-size:.82rem;color:#e9e9ee}
    .fx-drop .files .dot{flex:0 0 auto;width:8px;height:8px;border-radius:50%;background:var(--akzent, #7c87ff)}
    .fx-drop .files .nm{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    .fx-drop .files .sz{flex:0 0 auto;color:#a1a1aa}
    @media (prefers-reduced-motion:reduce){.fx-drop .zone{transition:none}}
    </style>
    
    <script>
    document.querySelectorAll('.fx-drop').forEach(function(root){
      var input=root.querySelector('input[type="file"]');
      var zone=root.querySelector('.zone');
      var list=root.querySelector('.files');
      function fmt(b){return b<1024?b+' B':b<1048576?(b/1024).toFixed(0)+' KB':(b/1048576).toFixed(1)+' MB';}
      function render(files){
        list.innerHTML='';
        Array.prototype.forEach.call(files,function(f){
          var li=document.createElement('li');
          var dot=document.createElement('span');dot.className='dot';
          var nm=document.createElement('span');nm.className='nm';nm.textContent=f.name;
          var sz=document.createElement('span');sz.className='sz';sz.textContent=fmt(f.size);
          li.appendChild(dot);li.appendChild(nm);li.appendChild(sz);list.appendChild(li);
        });
      }
      input.addEventListener('change',function(){render(input.files);});
      ['dragover','dragenter'].forEach(function(ev){
        zone.addEventListener(ev,function(e){e.preventDefault();zone.classList.add('is-over');});
      });
      ['dragleave','dragend','drop'].forEach(function(ev){
        zone.addEventListener(ev,function(e){e.preventDefault();zone.classList.remove('is-over');});
      });
      zone.addEventListener('drop',function(e){
        if(e.dataTransfer&&e.dataTransfer.files){try{input.files=e.dataTransfer.files;}catch(x){}render(e.dataTransfer.files);}
      });
    });
    </script>

    OTP-Code-Felder

    Pol 1 · Craft Anlass: 2-Faktor / Bestätigungscode

    Sechs Einzelfelder für einen numerischen Code: Eingabe springt automatisch weiter, Backspace zurück, Einfügen eines kompletten Codes verteilt die Ziffern. inputmode="numeric" öffnet mobil die Zahlentastatur, autocomplete="one-time-code" erlaubt Auto-Ausfüllen. Vorbild: SMS-/App-Code-Eingabe.

    <div class="fx-otp" role="group" aria-label="Bestätigungscode, 6 Ziffern">
      <input type="text" inputmode="numeric" maxlength="1" placeholder="·" autocomplete="one-time-code" aria-label="Ziffer 1">
      <input type="text" inputmode="numeric" maxlength="1" placeholder="·" aria-label="Ziffer 2">
      <input type="text" inputmode="numeric" maxlength="1" placeholder="·" aria-label="Ziffer 3">
      <input type="text" inputmode="numeric" maxlength="1" placeholder="·" aria-label="Ziffer 4">
      <input type="text" inputmode="numeric" maxlength="1" placeholder="·" aria-label="Ziffer 5">
      <input type="text" inputmode="numeric" maxlength="1" placeholder="·" aria-label="Ziffer 6">
    </div>
    
    <style>
    .fx-otp{display:flex;gap:10px;flex-wrap:wrap;justify-content:center}
    .fx-otp input{width:46px;height:56px;text-align:center;background:#16161a;border:1px solid #26262c;
      border-radius:10px;color:#f4f4f5;font:inherit;font-size:1.4rem;font-weight:700;outline:none;
      -webkit-appearance:none;-moz-appearance:textfield;appearance:none;caret-color:var(--akzent, #7c87ff);
      transition:border-color .2s ease,box-shadow .2s ease,background .2s ease}
    .fx-otp input::-webkit-outer-spin-button,.fx-otp input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
    .fx-otp input:focus{border-color:var(--akzent, #7c87ff);background:#17171b;
      box-shadow:0 0 0 3px color-mix(in srgb,var(--akzent, #7c87ff) 22%,transparent)}
    .fx-otp input:not(:placeholder-shown){border-color:color-mix(in srgb,var(--akzent, #7c87ff) 55%,#26262c)}
    @media (prefers-reduced-motion:reduce){.fx-otp input{transition:none}}
    </style>
    
    <script>
    document.querySelectorAll('.fx-otp').forEach(function(group){
      var boxes=Array.prototype.slice.call(group.querySelectorAll('input'));
      boxes.forEach(function(box,idx){
        box.addEventListener('input',function(){
          box.value=box.value.replace(/\D/g,'').slice(0,1);
          if(box.value&&idx<boxes.length-1)boxes[idx+1].focus();
        });
        box.addEventListener('keydown',function(e){
          if(e.key==='Backspace'&&!box.value&&idx>0){boxes[idx-1].focus();}
        });
        box.addEventListener('paste',function(e){
          e.preventDefault();
          var digits=(e.clipboardData||window.clipboardData).getData('text').replace(/\D/g,'').split('');
          boxes.forEach(function(b,i){b.value=digits[i]||'';});
          var next=Math.min(digits.length,boxes.length-1);
          boxes[next].focus();
        });
      });
    });
    </script>

    Range-Slider mit Live-Wert

    Pol 2 · Klarheit Anlass: Budget / Menge filtern

    Nativer <input type="range"> mit eigener Optik: die gefüllte Spur wächst per background-size, der aktuelle Wert steht live in einem <output>. Funktioniert per Tastatur out of the box, Firefox nutzt ::-moz-range-progress. Ein winziges JS spiegelt den Wert.

    2.500 €
    500 €5.000 €
    <div class="fx-range">
      <div class="top">
        <label for="fx-range-in">Budget</label>
        <output class="out" for="fx-range-in">2.500 €</output>
      </div>
      <input type="range" id="fx-range-in" min="500" max="5000" step="100" value="2500" style="--fill:44.4%">
      <div class="scale"><span>500 €</span><span>5.000 €</span></div>
    </div>
    
    <style>
    .fx-range{max-width:380px;display:flex;flex-direction:column;gap:12px;text-align:left}
    .fx-range .top{display:flex;justify-content:space-between;align-items:baseline}
    .fx-range .top label{font-size:.8rem;font-weight:600;color:#a1a1aa}
    .fx-range .out{font-size:1rem;font-weight:700;color:var(--akzent, #7c87ff);font-variant-numeric:tabular-nums}
    .fx-range input[type="range"]{-webkit-appearance:none;appearance:none;width:100%;height:6px;
      border-radius:99px;background:#1c1c22;outline:none;cursor:pointer;
      background-image:linear-gradient(var(--akzent, #7c87ff),var(--akzent, #7c87ff));
      background-repeat:no-repeat;background-size:var(--fill,50%) 100%}
    .fx-range input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
      width:20px;height:20px;border-radius:50%;background:#f4f4f5;border:3px solid var(--akzent, #7c87ff);
      box-shadow:0 2px 8px -2px rgba(0,0,0,.6);cursor:pointer;transition:transform .15s ease}
    .fx-range input[type="range"]::-moz-range-thumb{width:20px;height:20px;border-radius:50%;
      background:#f4f4f5;border:3px solid var(--akzent, #7c87ff);cursor:pointer}
    .fx-range input[type="range"]::-moz-range-track{height:6px;border-radius:99px;background:#1c1c22}
    .fx-range input[type="range"]::-moz-range-progress{height:6px;border-radius:99px;background:var(--akzent, #7c87ff)}
    .fx-range input[type="range"]:hover::-webkit-slider-thumb{transform:scale(1.12)}
    .fx-range input[type="range"]:focus-visible{box-shadow:0 0 0 3px color-mix(in srgb,var(--akzent, #7c87ff) 30%,transparent)}
    .fx-range .scale{display:flex;justify-content:space-between;font-size:.74rem;color:#7a7a82}
    @media (prefers-reduced-motion:reduce){.fx-range input[type="range"]::-webkit-slider-thumb{transition:none}}
    </style>
    
    <script>
    document.querySelectorAll('.fx-range').forEach(function(root){
      var slider=root.querySelector('input[type="range"]');
      var out=root.querySelector('.out');
      function upd(){
        var min=+slider.min,max=+slider.max,val=+slider.value;
        var pct=(val-min)/(max-min)*100;
        slider.style.setProperty('--fill',pct+'%');
        out.textContent=val.toLocaleString('de-DE')+' €';
      }
      slider.addEventListener('input',upd);
      upd();
    });
    </script>