Design · Bausteine-Dossier

Kategorie

Popups & Modale

Aufmerksamkeit ohne Aufdringlichkeit: getriggerte Overlays, Consent-Banner und Widgets — jeweils mit sauberer Tastatur-Bedienung, Body-Scroll-Lock und Schließen über ESC, X und Backdrop. In der Bühne öffnen die Demos per Knopf (nicht beim Laden).

Lead-/Förder-Popup mit Trigger & Dismiss-Memory

Pol 1 · Craft Anlass: Lead-Capture / Aktion

Öffnet bei 50 % Scroll-Tiefe oder nach einem Zeit-Fallback; nach dem Schließen 7 Tage Pause via localStorage. Backdrop-Blur, sanfter Eintritts-Transform, schließt über ESC, X und Backdrop, gibt den Fokus an den Auslöser zurück. Vorbild: NDS, Silver Mountain, Bernstein. (Demo öffnet hier per Knopf statt per Scroll/Zeit.)

<button type="button" data-open-lead>Popup öffnen</button>

<div class="fx-lead" id="lead" role="dialog" aria-modal="true" aria-labelledby="lead-t" hidden>
  <div class="fx-lead__card">
    <button type="button" class="fx-lead__close" data-lead-close aria-label="Schließen">×</button>
    <div class="fx-lead__icon" aria-hidden="true">
      <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 2"/></svg>
    </div>
    <span class="fx-lead__eyebrow">Förderung prüfen</span>
    <h2 id="lead-t" class="fx-lead__title">Bis zu <strong>4.000&nbsp;€</strong> Zuschuss — haben Sie Anspruch?</h2>
    <p class="fx-lead__text">In zwei Minuten prüfen, ob die Voraussetzungen erfüllt sind.</p>
    <a href="#" class="fx-lead__cta" data-lead-close>Anspruch jetzt prüfen</a>
    <button type="button" class="fx-lead__later" data-lead-close>Später, danke</button>
  </div>
</div>

<style>
.fx-lead{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;padding:20px;
  background:rgba(10,10,14,.55);-webkit-backdrop-filter:saturate(120%) blur(6px);backdrop-filter:saturate(120%) blur(6px);
  opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease}
.fx-lead.open{opacity:1;visibility:visible}
.fx-lead__card{position:relative;width:min(420px,100%);background:#17171b;border:1px solid #2b2b33;border-radius:20px;
  padding:38px 32px 28px;text-align:center;color:#e9e9ee;box-shadow:0 40px 90px -40px rgba(0,0,0,.7);
  transform:translateY(14px) scale(.97);transition:transform .3s cubic-bezier(.16,1,.3,1)}
.fx-lead.open .fx-lead__card{transform:none}
.fx-lead__close{position:absolute;top:14px;right:14px;width:38px;height:38px;border-radius:11px;background:transparent;
  border:1px solid #34343e;color:#a1a1aa;font-size:22px;line-height:1;cursor:pointer;transition:background .2s ease,color .2s ease}
.fx-lead__close:hover,.fx-lead__close:focus-visible{background:rgba(255,255,255,.06);color:#f4f4f5}
.fx-lead__icon{width:64px;height:64px;border-radius:50%;margin:0 auto 18px;display:grid;place-items:center;
  background:color-mix(in srgb,var(--akzent, #7c87ff) 16%,transparent);color:var(--akzent, #7c87ff)}
.fx-lead__eyebrow{display:block;font-size:.72rem;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
  color:var(--akzent, #7c87ff);margin-bottom:10px}
.fx-lead__title{font-size:1.4rem;line-height:1.25;margin:0 0 12px;font-weight:800}
.fx-lead__title strong{color:var(--akzent, #7c87ff)}
.fx-lead__text{font-size:.92rem;line-height:1.6;color:#a1a1aa;margin:0 0 22px}
.fx-lead__cta{display:block;width:100%;padding:.8em 1.1em;border-radius:99px;background:var(--akzent, #7c87ff);
  color:#fff;font-weight:700;text-decoration:none;margin-bottom:10px;transition:filter .2s ease}
.fx-lead__cta:hover,.fx-lead__cta:focus-visible{filter:brightness(1.1)}
.fx-lead__later{width:100%;background:transparent;border:1px solid #34343e;color:#a1a1aa;border-radius:99px;
  padding:.7em 1.1em;font:inherit;font-weight:600;cursor:pointer;transition:background .2s ease,color .2s ease}
.fx-lead__later:hover,.fx-lead__later:focus-visible{background:rgba(255,255,255,.05);color:#f4f4f5}
@media (prefers-reduced-motion:reduce){.fx-lead,.fx-lead__card{transition:none}}
</style>

<script>
(function(){
  var modal=document.getElementById('lead');
  if(!modal) return;
  var KEY='fx-lead-dismissed', TTL=7*24*60*60*1000;   // 7 Tage Pause
  var SCROLL=0.5, FALLBACK=20000;                       // 50% Tiefe / 20s Fallback
  var opened=false, lastFocus=null, timer=null;

  function dismissed(){
    try{var ts=parseInt(localStorage.getItem(KEY)||'0',10);
        return ts && (Date.now()-ts < TTL);}catch(e){return false;}
  }
  function remember(){try{localStorage.setItem(KEY,String(Date.now()));}catch(e){}}

  function open(){
    if(opened) return; opened=true;
    lastFocus=document.activeElement;
    modal.hidden=false;
    requestAnimationFrame(function(){modal.classList.add('open');});
    document.body.style.overflow='hidden';             // Body-Scroll-Lock
    window.removeEventListener('scroll',onScroll);
    if(timer) clearTimeout(timer);
    setTimeout(function(){modal.querySelector('.fx-lead__close').focus();},40);
  }
  function close(){
    modal.classList.remove('open');
    document.body.style.overflow='';                   // Scroll-Lock lösen
    remember();
    setTimeout(function(){modal.hidden=true;},300);
    if(lastFocus && lastFocus.focus) lastFocus.focus();
  }

  modal.addEventListener('click',function(e){
    if(e.target===modal || e.target.closest('[data-lead-close]')){e.preventDefault();close();}
  });
  document.addEventListener('keydown',function(e){
    if(e.key==='Escape' && !modal.hidden) close();
  });

  // Manueller Auslöser (z.B. Demo-Knopf)
  var trigger=document.querySelector('[data-open-lead]');
  if(trigger) trigger.addEventListener('click',open);

  // Auto-Trigger: 50% Scroll-Tiefe ODER Zeit-Fallback
  function depth(){var h=document.documentElement.scrollHeight-window.innerHeight;
    return h<=0?1:(window.scrollY||0)/h;}
  function onScroll(){if(!opened && depth()>=SCROLL) open();}
  if(!dismissed()){
    window.addEventListener('scroll',onScroll,{passive:true});
    timer=setTimeout(open,FALLBACK);
  }
})();
</script>

Cookie-Consent-Banner (slide-in)

Pol 2 · Klarheit Anlass: TDDDG-Einwilligung

Schiebt von unten ein; Buttons Akzeptieren / Ablehnen / Details; die Wahl (Kategorien necessary immer an, extern optional) wird mit TTL in localStorage gespeichert. Rechtlich Pflicht: externe Dienste (Maps, Fonts, Tracker) erst nach aktiver Einwilligung laden — § 25 TDDDG. Vorbild: Nördlicht.

<div class="fx-cookie" id="cookie" role="dialog" aria-labelledby="ck-t" aria-describedby="ck-d">
  <h4 id="ck-t">Datenschutz auf dieser Seite</h4>
  <p id="ck-d">Wir nutzen technisch notwendige Cookies. Optionale Inhalte laden wir nur nach Ihrer
    Einwilligung. Mehr in der <a href="#">Datenschutzerklärung</a>.</p>
  <div class="fx-cookie__cats">
    <label class="fx-cookie__cat"><input type="checkbox" checked disabled>
      <span>Notwendig <small>Immer aktiv</small></span></label>
    <label class="fx-cookie__cat"><input type="checkbox" data-cat="extern">
      <span>Externe Medien <small>Karten, eingebettete Inhalte</small></span></label>
  </div>
  <div class="fx-cookie__actions">
    <button type="button" class="fx-cookie__reject" data-cookie-reject>Nur notwendige</button>
    <button type="button" class="fx-cookie__accept" data-cookie-accept>Alle akzeptieren</button>
    <button type="button" class="fx-cookie__details" data-cookie-details>Details</button>
  </div>
</div>

<style>
.fx-cookie{position:fixed;left:16px;right:16px;bottom:16px;max-width:480px;margin:0 auto;z-index:55;
  background:#16161a;border:1px solid #2b2b33;border-radius:16px;box-shadow:0 16px 44px rgba(0,0,0,.4);
  padding:20px 22px;font-size:.82rem;line-height:1.55;color:#e9e9ee;
  opacity:0;visibility:hidden;transform:translateY(20px);transition:opacity .35s ease,transform .35s ease,visibility .35s ease}
.fx-cookie.show{opacity:1;visibility:visible;transform:translateY(0)}
.fx-cookie h4{font-size:.95rem;font-weight:800;color:#f4f4f5;margin:0 0 8px}
.fx-cookie p{margin:0 0 6px;color:#a1a1aa}
.fx-cookie a{color:var(--akzent, #7c87ff);text-decoration:underline}
.fx-cookie__cats{display:none;margin:10px 0 4px;border-top:1px solid #26262c;padding-top:10px}
.fx-cookie.details .fx-cookie__cats{display:block}
.fx-cookie__cat{display:flex;align-items:center;gap:8px;margin:6px 0;font-size:.8rem;color:#c9c9d2}
.fx-cookie__cat input{accent-color:var(--akzent, #7c87ff)}
.fx-cookie__cat span small{display:block;color:#71717a;font-size:.72rem}
.fx-cookie__actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}
.fx-cookie__actions button{flex:1;min-width:110px;padding:10px 14px;border-radius:99px;font-size:.74rem;font-weight:700;
  cursor:pointer;border:1px solid #34343e;letter-spacing:.02em;transition:background .25s ease,color .25s ease,transform .15s ease}
.fx-cookie__accept{background:var(--akzent, #7c87ff);color:#fff;border-color:var(--akzent, #7c87ff)}
.fx-cookie__accept:hover,.fx-cookie__accept:focus-visible{filter:brightness(1.1);transform:translateY(-1px)}
.fx-cookie__reject{background:#1c1c22;color:#e9e9ee}
.fx-cookie__reject:hover,.fx-cookie__reject:focus-visible{background:#23232b}
.fx-cookie__details{background:transparent;color:#a1a1aa;border-color:transparent;text-decoration:underline;
  flex:0 0 auto;min-width:auto;padding:10px 8px}
.fx-cookie__details:hover,.fx-cookie__details:focus-visible{color:#f4f4f5}
@media (prefers-reduced-motion:reduce){.fx-cookie,.fx-cookie__actions button{transition:none}}
</style>

<script>
(function(){
  var banner=document.getElementById('cookie');
  if(!banner) return;
  var KEY='fx-cookie-consent', TTL=365*24*60*60*1000;  // 12 Monate

  function get(){try{var p=JSON.parse(localStorage.getItem(KEY)||'null');
    if(!p) return null; if(Date.now()-p.ts>TTL){localStorage.removeItem(KEY);return null;}
    return p;}catch(e){return null;}}
  function save(extern){try{localStorage.setItem(KEY,JSON.stringify(
    {necessary:true,extern:extern,ts:Date.now()}));}catch(e){}}
  function show(){banner.classList.add('show');}
  function hide(){banner.classList.remove('show');}

  banner.querySelector('[data-cookie-accept]').addEventListener('click',function(){save(true);hide();});
  banner.querySelector('[data-cookie-reject]').addEventListener('click',function(){save(false);hide();});
  banner.querySelector('[data-cookie-details]').addEventListener('click',function(){banner.classList.toggle('details');});
  var dpLink=banner.querySelector('[data-cookie-close]');
  if(dpLink) dpLink.addEventListener('click',function(e){e.preventDefault();hide();});

  // Beim ersten Besuch zeigen; bei vorhandener Wahl: extern-Dienste nachladen wenn erlaubt
  var consent=get();
  if(consent===null) show();
  else if(consent.extern){ /* hier externe Dienste (Maps/Fonts) initialisieren */ }
})();
</script>

Floating Action Button(s)

Pol 2 · Klarheit Anlass: dauerhafter Kontakt-Shortcut

Fixierte runde Schaltflächen unten rechts (hier als Stack: Anruf + Nachricht). Heben sich beim Hover/Fokus leicht an, Schatten wächst. Icons inline als SVG — keine Icon-Fonts. Vorbild: Hauszeit, NDS, Nördlicht. (In der Bühne absolut statt am Viewport positioniert.)

<div class="fx-fab">
  <a href="#" aria-label="Per Nachricht schreiben">
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>
  </a>
  <a href="#" aria-label="Jetzt anrufen">
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13 1.05.37 2.07.7 3.06a2 2 0 0 1-.45 2.11L8.09 10.91a16 16 0 0 0 6 6l2.02-1.27a2 2 0 0 1 2.11-.45c.99.33 2.02.57 3.06.7A2 2 0 0 1 22 16.92z"/></svg>
  </a>
</div>

<style>
.fx-fab{position:fixed;right:18px;bottom:18px;display:flex;flex-direction:column;gap:12px;z-index:998}
.fx-fab a,.fx-fab button{width:56px;height:56px;border-radius:50%;display:grid;place-items:center;border:0;cursor:pointer;
  color:#fff;text-decoration:none;background:var(--akzent, #7c87ff);
  box-shadow:0 8px 24px rgba(0,0,0,.35),0 4px 12px rgba(0,0,0,.22);
  transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s ease,filter .25s ease}
.fx-fab a:hover,.fx-fab a:focus-visible,.fx-fab button:hover,.fx-fab button:focus-visible{
  transform:translateY(-3px) scale(1.06);box-shadow:0 14px 30px rgba(0,0,0,.45);filter:brightness(1.08)}
.fx-fab a:active,.fx-fab button:active{transform:translateY(0) scale(.97)}
.fx-fab svg{width:24px;height:24px}
@media (prefers-reduced-motion:reduce){.fx-fab a,.fx-fab button{transition:none}.fx-fab a:hover,.fx-fab button:hover{transform:none}}
</style>

Datengetriebenes Modal (ein Skelett, n Inhalte)

Pol 1 · Craft Anlass: Zielgruppen / Leistungs-Details

Ein einziges Modal-Skelett wird aus einem JS-Objekt befüllt — beliebig viele Auslöser, kein Markup pro Inhalt. Mit Fokus-Trap (Tab bleibt im Modal), Fokus-Rückgabe an den Auslöser, Body-Scroll-Lock, Schließen über ESC und Backdrop. Vorbild: NDS Zielgruppen-Modal.

<button type="button" data-dm="eigentuemer">Für Eigentümer</button>
<button type="button" data-dm="verwaltung">Für Verwaltungen</button>
<button type="button" data-dm="gewerbe">Für Gewerbe</button>

<div class="fx-dm" id="dm" role="dialog" aria-modal="true" aria-labelledby="dm-t" hidden>
  <div class="fx-dm__backdrop" data-dm-close></div>
  <div class="fx-dm__card">
    <button type="button" class="fx-dm__close" data-dm-close aria-label="Schließen">×</button>
    <h2 class="fx-dm__title" id="dm-t"></h2>
    <ul class="fx-dm__list"></ul>
    <div class="fx-dm__cta"><a href="#" data-dm-close>Kostenlose Beratung anfragen</a></div>
  </div>
</div>

<style>
.fx-dm{position:fixed;inset:0;z-index:60;display:none;align-items:center;justify-content:center;padding:20px}
.fx-dm:not([hidden]){display:flex}
.fx-dm__backdrop{position:absolute;inset:0;background:rgba(10,10,14,.6);
  -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);cursor:pointer;animation:fxDmBack .22s ease-out}
.fx-dm__card{position:relative;z-index:2;width:min(440px,100%);max-height:88vh;overflow-y:auto;
  background:#17171b;border:1px solid #2b2b33;border-radius:18px;padding:34px 30px 28px;color:#e9e9ee;
  box-shadow:0 24px 64px rgba(0,0,0,.45);animation:fxDmIn .3s cubic-bezier(.16,1,.3,1)}
@keyframes fxDmBack{from{opacity:0}to{opacity:1}}
@keyframes fxDmIn{from{opacity:0;transform:translateY(16px) scale(.96)}to{opacity:1;transform:none}}
.fx-dm__close{position:absolute;top:14px;right:14px;width:36px;height:36px;border:none;background:transparent;
  font-size:26px;line-height:1;cursor:pointer;color:#a1a1aa;border-radius:10px;transition:background .2s ease,color .2s ease}
.fx-dm__close:hover,.fx-dm__close:focus-visible{background:rgba(255,255,255,.06);color:#f4f4f5}
.fx-dm__title{font-size:1.25rem;font-weight:800;margin:0 0 16px;color:#f4f4f5}
.fx-dm__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}
.fx-dm__list li{display:flex;gap:10px;align-items:flex-start;font-size:.92rem;color:#c9c9d2}
.fx-dm__list svg{flex:0 0 auto;width:18px;height:18px;margin-top:2px;color:var(--akzent, #7c87ff)}
.fx-dm__cta a{display:block;text-align:center;margin-top:22px;padding:.8em 1.1em;border-radius:99px;
  background:var(--akzent, #7c87ff);color:#fff;font-weight:700;text-decoration:none}
@media (prefers-reduced-motion:reduce){.fx-dm__backdrop,.fx-dm__card{animation:none}}
</style>

<script>
(function(){
  var modal=document.getElementById('dm');
  if(!modal) return;
  // n Inhalte aus einem Objekt — ein Skelett befüllt alles
  var DATA={
    eigentuemer:{title:'Für Eigentümer',items:['Transparente Festpreise','Termingarantie','Schlüsselfertige Übergabe','Kostenlose Erstbesichtigung']},
    verwaltung:{title:'Für Verwaltungen',items:['Ein Ansprechpartner fürs Portfolio','Lückenlose Dokumentation','Rahmenverträge','Koordination aller Gewerke']},
    gewerbe:{title:'Für Gewerbe',items:['Kurze Stillstandzeiten','Arbeiten außerhalb der Öffnungszeiten','Budgettreue ohne Nachträge','Mehrere Objekte parallel']}
  };
  var titleEl=modal.querySelector('.fx-dm__title'),
      listEl=modal.querySelector('.fx-dm__list'),
      lastFocus=null;

  function check(){return '<svg viewBox="0 0 18 18" fill="none" aria-hidden="true">'+
    '<circle cx="9" cy="9" r="8" stroke="currentColor" stroke-width="1.2"/>'+
    '<path d="M5.5 9.5l2 2 5-5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';}

  function open(key){
    var d=DATA[key]; if(!d) return;
    titleEl.textContent=d.title;
    listEl.innerHTML=d.items.map(function(t){
      return '<li>'+check()+'<span>'+t+'</span></li>';}).join('');
    lastFocus=document.activeElement;
    modal.hidden=false;
    document.body.style.overflow='hidden';             // Body-Scroll-Lock
    setTimeout(function(){modal.querySelector('.fx-dm__close').focus();},30);
  }
  function close(){
    modal.hidden=true;
    document.body.style.overflow='';
    if(lastFocus && lastFocus.focus) lastFocus.focus();
  }

  document.querySelectorAll('[data-dm]').forEach(function(btn){
    btn.addEventListener('click',function(){open(btn.dataset.dm);});
  });
  modal.addEventListener('click',function(e){
    if(e.target.closest('[data-dm-close]')){e.preventDefault();close();}
  });
  document.addEventListener('keydown',function(e){
    if(modal.hidden) return;
    if(e.key==='Escape'){close();return;}
    if(e.key==='Tab'){                                  // Fokus-Trap
      var f=modal.querySelectorAll('button,a[href],input');
      if(!f.length) return;
      var first=f[0], last=f[f.length-1];
      if(e.shiftKey && document.activeElement===first){e.preventDefault();last.focus();}
      else if(!e.shiftKey && document.activeElement===last){e.preventDefault();first.focus();}
    }
  });
})();
</script>

Offline-Chatbot-Widget (Keyword-Matching)

Pol 1 · Craft Anlass: Self-Service / Erstkontakt

Glas-Bubble öffnet ein Glas-Panel mit Chatverlauf; Antworten kommen aus einem einfachen Keyword-Abgleich (kein Backend, kein KI-Dienst). Bewusst kompakt/vereinfacht: das echte Vorbild (NDS) hat dutzende Themen, Tippfehler-Toleranz und Eskalations-Logik — hier nur das Grundmuster, das sich beliebig erweitern lässt. Vorbild: NDS „Nordi".

<div class="fx-chat" id="chat">
  <div class="fx-chat__panel" role="dialog" aria-label="Assistent">
    <div class="fx-chat__head">
      <span class="fx-chat__avatar" aria-hidden="true">…</span>
      <div><b>Assistent</b><small>Antwortet sofort</small></div>
      <button type="button" class="fx-chat__x" data-chat-close aria-label="Schließen">×</button>
    </div>
    <div class="fx-chat__log" data-chat-log aria-live="polite"></div>
    <div class="fx-chat__quick">
      <button type="button" data-q="Was kostet eine Beratung?">Preis</button>
      <button type="button" data-q="Wie sind eure Öffnungszeiten?">Öffnungszeiten</button>
      <button type="button" data-q="Wie kann ich euch erreichen?">Kontakt</button>
    </div>
    <form class="fx-chat__form" data-chat-form>
      <input type="text" data-chat-input placeholder="Ihre Frage…" aria-label="Nachricht">
      <button type="submit" aria-label="Senden">…</button>
    </form>
  </div>
  <button type="button" class="fx-chat__fab" data-chat-toggle aria-label="Chat öffnen" aria-expanded="false">…</button>
</div>

<style>
.fx-chat{position:fixed;right:18px;bottom:18px;z-index:999}
.fx-chat__fab{width:56px;height:56px;border-radius:50%;display:grid;place-items:center;border:0;cursor:pointer;color:#fff;
  background:var(--akzent, #7c87ff);box-shadow:0 8px 24px rgba(0,0,0,.35);
  transition:transform .25s cubic-bezier(.4,0,.2,1),filter .25s ease}
.fx-chat__fab:hover,.fx-chat__fab:focus-visible{transform:scale(1.08);filter:brightness(1.08)}
.fx-chat__fab svg{width:24px;height:24px}
.fx-chat__panel{position:absolute;right:0;bottom:70px;width:300px;max-width:78vw;border-radius:18px;overflow:hidden;
  background:rgba(22,22,26,.82);border:1px solid rgba(255,255,255,.1);
  -webkit-backdrop-filter:saturate(150%) blur(16px);backdrop-filter:saturate(150%) blur(16px);
  box-shadow:0 24px 60px rgba(0,0,0,.5);
  opacity:0;visibility:hidden;transform:translateY(10px) scale(.97);transform-origin:bottom right;
  transition:opacity .25s ease,transform .25s cubic-bezier(.16,1,.3,1),visibility .25s ease}
.fx-chat.open .fx-chat__panel{opacity:1;visibility:visible;transform:none}
.fx-chat__head{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid rgba(255,255,255,.08)}
.fx-chat__avatar{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;
  background:color-mix(in srgb,var(--akzent, #7c87ff) 22%,transparent);color:var(--akzent, #7c87ff)}
.fx-chat__head b{display:block;font-size:.9rem;color:#f4f4f5}
.fx-chat__head small{color:#a1a1aa;font-size:.74rem}
.fx-chat__x{margin-left:auto;width:30px;height:30px;border:0;background:transparent;color:#a1a1aa;cursor:pointer;
  border-radius:8px;font-size:18px;line-height:1;transition:background .2s ease,color .2s ease}
.fx-chat__x:hover,.fx-chat__x:focus-visible{background:rgba(255,255,255,.08);color:#f4f4f5}
.fx-chat__log{height:200px;overflow-y:auto;padding:14px 16px;display:flex;flex-direction:column;gap:10px}
.fx-chat__msg{max-width:82%;padding:.55rem .75rem;border-radius:14px;font-size:.82rem;line-height:1.45}
.fx-chat__msg--bot{align-self:flex-start;background:rgba(255,255,255,.07);color:#e9e9ee;border-bottom-left-radius:4px}
.fx-chat__msg--user{align-self:flex-end;background:var(--akzent, #7c87ff);color:#fff;border-bottom-right-radius:4px}
.fx-chat__quick{display:flex;flex-wrap:wrap;gap:6px;padding:0 16px 10px}
.fx-chat__quick button{font:inherit;font-size:.72rem;color:#c9c9d2;background:rgba(255,255,255,.06);
  border:1px solid rgba(255,255,255,.1);border-radius:99px;padding:.3rem .6rem;cursor:pointer;transition:background .2s ease}
.fx-chat__quick button:hover,.fx-chat__quick button:focus-visible{background:rgba(255,255,255,.12)}
.fx-chat__form{display:flex;gap:8px;padding:10px 14px;border-top:1px solid rgba(255,255,255,.08)}
.fx-chat__form input{flex:1;background:#101014;border:1px solid #2b2b33;border-radius:99px;padding:.5rem .8rem;color:#e9e9ee;font:inherit;font-size:.82rem}
.fx-chat__form button{width:38px;height:38px;border-radius:50%;border:0;cursor:pointer;background:var(--akzent, #7c87ff);color:#fff;display:grid;place-items:center}
@media (prefers-reduced-motion:reduce){.fx-chat__panel,.fx-chat__fab{transition:none}.fx-chat__fab:hover{transform:none}}
</style>

<script>
(function(){
  var root=document.getElementById('chat');
  if(!root) return;
  var log=root.querySelector('[data-chat-log]'),
      input=root.querySelector('[data-chat-input]');

  // Wissensbasis: Keywords -> Antwort. Erster Treffer gewinnt.
  var TOPICS=[
    {keys:['preis','kosten','was kostet','teuer'],a:'Die Erstberatung ist kostenlos. Das konkrete Angebot erstellen wir nach einem kurzen Gespräch.'},
    {keys:['oeffnung','öffnung','zeiten','erreichbar','wann offen'],a:'Mo bis Fr von 8 bis 17 Uhr. Außerhalb gern über das Kontaktformular.'},
    {keys:['kontakt','erreichen','anrufen','telefon','mail'],a:'Sie erreichen uns telefonisch oder per E-Mail über die Kontaktseite — wir melden uns schnell zurück.'},
    {keys:['termin','beratung','besichtigung'],a:'Einen Termin vereinbaren wir gern telefonisch oder über das Formular. Wann passt es Ihnen?'}
  ];
  var FALLBACK='Das kläre ich am besten persönlich für Sie — nutzen Sie gern die Kontaktseite oder rufen Sie an.';

  function norm(s){return s.toLowerCase()
    .replace(/ä/g,'ae').replace(/ö/g,'oe').replace(/ü/g,'ue').replace(/ß/g,'ss');}
  function answer(q){
    var n=norm(q);
    for(var i=0;i<TOPICS.length;i++){
      for(var k=0;k<TOPICS[i].keys.length;k++){
        if(n.indexOf(norm(TOPICS[i].keys[k]))!==-1) return TOPICS[i].a;
      }
    }
    return FALLBACK;
  }
  function push(text,who){
    var el=document.createElement('div');
    el.className='fx-chat__msg fx-chat__msg--'+who;
    el.textContent=text;
    log.appendChild(el);
    log.scrollTop=log.scrollHeight;
  }
  function ask(q){
    if(!q.trim()) return;
    push(q,'user');
    setTimeout(function(){push(answer(q),'bot');},350);
  }

  function toggle(force){
    var open=force!==undefined?force:!root.classList.contains('open');
    root.classList.toggle('open',open);
    root.querySelector('[data-chat-toggle]').setAttribute('aria-expanded',String(open));
    if(open){
      if(!log.children.length) push('Hallo! Wie kann ich helfen? Wählen Sie unten ein Thema oder schreiben Sie mir.','bot');
      setTimeout(function(){input.focus();},80);
    }
  }
  root.querySelector('[data-chat-toggle]').addEventListener('click',function(){toggle();});
  root.querySelector('[data-chat-close]').addEventListener('click',function(){toggle(false);});
  root.querySelector('[data-chat-form]').addEventListener('submit',function(e){
    e.preventDefault(); ask(input.value); input.value='';
  });
  root.querySelectorAll('[data-q]').forEach(function(b){
    b.addEventListener('click',function(){ask(b.dataset.q);});
  });
  document.addEventListener('keydown',function(e){
    if(e.key==='Escape' && root.classList.contains('open')) toggle(false);
  });
})();
</script>

Toast-Benachrichtigung (Stapel, Auto-Dismiss)

Pol 2 · Klarheit Anlass: Feedback nach Aktion

Kurze Rückmeldung nach einer Aktion (Formular gesendet, gespeichert). Toasts stapeln sich unten rechts, laufen über eine Fortschrittsleiste nach 4 s selbst aus und lassen sich per X sofort schließen. aria-live="polite" für Screenreader; die Farbe zeigt Info / Erfolg / Warnung.

<button type="button" data-toast="ok">Erfolg</button>
<div class="fx-toast__stack" id="toasts" aria-live="polite" aria-atomic="false"></div>

<style>
.fx-toast__stack{position:fixed;right:20px;bottom:20px;z-index:9999;display:flex;flex-direction:column;gap:10px;
  width:min(320px,calc(100% - 40px));pointer-events:none}
.fx-toast{pointer-events:auto;position:relative;overflow:hidden;display:flex;gap:11px;align-items:flex-start;
  background:#17171b;border:1px solid #2b2b33;border-left:3px solid var(--akzent, #7c87ff);border-radius:12px;
  padding:13px 14px 15px;color:#e9e9ee;box-shadow:0 14px 34px -14px rgba(0,0,0,.7);
  animation:fxToastIn .32s cubic-bezier(.16,1,.3,1)}
.fx-toast.leaving{animation:fxToastOut .26s ease forwards}
@keyframes fxToastIn{from{opacity:0;transform:translateX(24px) scale(.96)}to{opacity:1;transform:none}}
@keyframes fxToastOut{to{opacity:0;transform:translateX(24px) scale(.96)}}
.fx-toast--ok{border-left-color:#3ecf8e}
.fx-toast--warn{border-left-color:#f0b64b}
.fx-toast__ic{flex:0 0 auto;width:20px;height:20px;margin-top:1px;color:var(--akzent, #7c87ff)}
.fx-toast--ok .fx-toast__ic{color:#3ecf8e}
.fx-toast--warn .fx-toast__ic{color:#f0b64b}
.fx-toast__body{flex:1;min-width:0}
.fx-toast__title{font-size:.85rem;font-weight:700;color:#f4f4f5;margin:0 0 2px}
.fx-toast__text{font-size:.78rem;line-height:1.45;color:#a1a1aa;margin:0;word-wrap:break-word}
.fx-toast__x{flex:0 0 auto;width:24px;height:24px;border:0;background:transparent;color:#71717a;cursor:pointer;
  border-radius:7px;font-size:16px;line-height:1;transition:background .2s ease,color .2s ease}
.fx-toast__x:hover,.fx-toast__x:focus-visible{background:rgba(255,255,255,.07);color:#f4f4f5}
.fx-toast__bar{position:absolute;left:0;bottom:0;height:2px;width:100%;transform-origin:left;
  background:var(--akzent, #7c87ff);animation:fxToastBar 4s linear forwards}
.fx-toast--ok .fx-toast__bar{background:#3ecf8e}
.fx-toast--warn .fx-toast__bar{background:#f0b64b}
@keyframes fxToastBar{from{transform:scaleX(1)}to{transform:scaleX(0)}}
@media (prefers-reduced-motion:reduce){.fx-toast,.fx-toast.leaving,.fx-toast__bar{animation:none}}
</style>

<script>
(function(){
  var stack=document.getElementById('toasts');
  if(!stack) return;
  var LIFE=4000;
  var ICONS={
    info:'<path d="M12 8h.01M11 12h1v4h1" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.7"/>',
    ok:'<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.7"/><path d="M8 12.5l2.5 2.5 5-5.5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>',
    warn:'<path d="M12 3l9 16H3z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M12 10v4M12 17h.01" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>'
  };
  var DEFAULTS={
    info:{t:'Hinweis',m:'Ihre Anfrage wird bearbeitet.'},
    ok:{t:'Erfolgreich gesendet',m:'Wir melden uns in Kürze bei Ihnen.'},
    warn:{t:'Bitte prüfen',m:'Einige Felder sind noch unvollständig.'}
  };

  function toast(type,title,text){
    var d=DEFAULTS[type]||DEFAULTS.info;
    var el=document.createElement('div');
    el.className='fx-toast'+(type==='ok'?' fx-toast--ok':type==='warn'?' fx-toast--warn':'');
    el.setAttribute('role','status');
    el.innerHTML=
      '<svg class="fx-toast__ic" viewBox="0 0 24 24" fill="none" aria-hidden="true">'+(ICONS[type]||ICONS.info)+'</svg>'+
      '<div class="fx-toast__body"><p class="fx-toast__title"></p><p class="fx-toast__text"></p></div>'+
      '<button type="button" class="fx-toast__x" aria-label="Schließen">×</button>'+
      '<span class="fx-toast__bar" aria-hidden="true"></span>';
    el.querySelector('.fx-toast__title').textContent=title||d.t;   // textContent = XSS-sicher
    el.querySelector('.fx-toast__text').textContent=text||d.m;
    stack.appendChild(el);

    var timer=setTimeout(remove,LIFE);
    function remove(){
      clearTimeout(timer);
      el.classList.add('leaving');
      el.addEventListener('animationend',function(){if(el.parentNode) el.remove();});
      setTimeout(function(){if(el.parentNode) el.remove();},400);  // Fallback
    }
    el.querySelector('.fx-toast__x').addEventListener('click',remove);
  }

  document.querySelectorAll('[data-toast]').forEach(function(b){
    b.addEventListener('click',function(){toast(b.dataset.toast);});
  });
})();
</script>

Bottom-Sheet / Drawer (von unten)

Pol 1 · Craft Anlass: Aktions-Auswahl mobil

Auswahl-Fläche, die von unten einfährt — auf dem Handy angenehmer als ein mittiges Modal, weil der Daumen sie erreicht. Mit Griff-Indikator, Fokus-Trap, Fokus-Rückgabe an den Auslöser, Body-Scroll-Lock und Schließen über ESC, Abbrechen und Scrim. (In der Bühne absolut zur .stage statt am Viewport.)

<button type="button" data-open-sheet>Optionen öffnen</button>

<div class="fx-sheet" id="sheet" role="dialog" aria-modal="true" aria-labelledby="sheet-t" hidden>
  <div class="fx-sheet__scrim" data-sheet-close></div>
  <div class="fx-sheet__panel">
    <div class="fx-sheet__grip" aria-hidden="true"></div>
    <h2 class="fx-sheet__title" id="sheet-t">Wie möchten Sie fortfahren?</h2>
    <p class="fx-sheet__lead">Wählen Sie eine Option.</p>
    <ul class="fx-sheet__opts">
      <li><button type="button" class="fx-sheet__opt" data-sheet-close>
        <svg viewBox="0 0 24 24" …>…</svg>
        <span><b>Rückruf anfordern</b><small>Wir melden uns telefonisch</small></span></button></li>
      <li><button type="button" class="fx-sheet__opt" data-sheet-close>
        <svg viewBox="0 0 24 24" …>…</svg>
        <span><b>Nachricht schreiben</b><small>Antwort per E-Mail</small></span></button></li>
    </ul>
    <button type="button" class="fx-sheet__cancel" data-sheet-close>Abbrechen</button>
  </div>
</div>

<style>
.fx-sheet{position:fixed;inset:0;z-index:60;display:flex;align-items:flex-end;justify-content:center;visibility:hidden}
.fx-sheet.open{visibility:visible}
.fx-sheet__scrim{position:absolute;inset:0;background:rgba(10,10,14,.55);
  -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);opacity:0;transition:opacity .3s ease;cursor:pointer}
.fx-sheet.open .fx-sheet__scrim{opacity:1}
.fx-sheet__panel{position:relative;z-index:2;width:min(440px,100%);max-height:82%;overflow-y:auto;
  background:#17171b;border:1px solid #2b2b33;border-bottom:0;border-radius:20px 20px 0 0;
  padding:8px 24px 24px;color:#e9e9ee;box-shadow:0 -20px 50px -24px rgba(0,0,0,.7);
  transform:translateY(100%);transition:transform .34s cubic-bezier(.16,1,.3,1)}
.fx-sheet.open .fx-sheet__panel{transform:none}
.fx-sheet__grip{width:40px;height:4px;border-radius:99px;background:#3a3a45;margin:8px auto 16px}
.fx-sheet__title{font-size:1.15rem;font-weight:800;color:#f4f4f5;margin:0 0 6px}
.fx-sheet__lead{font-size:.86rem;line-height:1.55;color:#a1a1aa;margin:0 0 18px}
.fx-sheet__opts{list-style:none;margin:0 0 20px;padding:0;display:flex;flex-direction:column;gap:8px}
.fx-sheet__opt{display:flex;align-items:center;gap:12px;width:100%;text-align:left;cursor:pointer;
  background:#1c1c22;border:1px solid #26262c;border-radius:12px;padding:12px 14px;color:#e9e9ee;font:inherit;
  transition:background .2s ease,border-color .2s ease,transform .15s ease}
.fx-sheet__opt:hover,.fx-sheet__opt:focus-visible{background:#22222a;border-color:#3a3a45;transform:translateY(-1px)}
.fx-sheet__opt svg{flex:0 0 auto;width:20px;height:20px;color:var(--akzent, #7c87ff)}
.fx-sheet__opt b{display:block;font-size:.88rem;font-weight:700}
.fx-sheet__opt small{display:block;font-size:.74rem;color:#a1a1aa}
.fx-sheet__cancel{width:100%;background:transparent;border:1px solid #34343e;color:#a1a1aa;border-radius:99px;
  padding:.7em 1.1em;font:inherit;font-weight:600;cursor:pointer;transition:background .2s ease,color .2s ease}
.fx-sheet__cancel:hover,.fx-sheet__cancel:focus-visible{background:rgba(255,255,255,.05);color:#f4f4f5}
@media (prefers-reduced-motion:reduce){
  .fx-sheet__scrim,.fx-sheet__panel,.fx-sheet__opt{transition:none}.fx-sheet__opt:hover{transform:none}}
</style>

<script>
(function(){
  var sheet=document.getElementById('sheet');
  if(!sheet) return;
  var panel=sheet.querySelector('.fx-sheet__panel'), lastFocus=null;

  function open(){
    lastFocus=document.activeElement;
    sheet.hidden=false;
    requestAnimationFrame(function(){sheet.classList.add('open');});
    document.body.style.overflow='hidden';               // Body-Scroll-Lock
    setTimeout(function(){var f=panel.querySelector('button'); if(f) f.focus();},60);
  }
  function close(){
    sheet.classList.remove('open');
    document.body.style.overflow='';
    setTimeout(function(){sheet.hidden=true;},340);
    if(lastFocus && lastFocus.focus) lastFocus.focus();
  }

  var trigger=document.querySelector('[data-open-sheet]');
  if(trigger) trigger.addEventListener('click',open);
  sheet.addEventListener('click',function(e){
    if(e.target.closest('[data-sheet-close]')){e.preventDefault();close();}
  });
  document.addEventListener('keydown',function(e){
    if(sheet.hidden) return;
    if(e.key==='Escape'){close();return;}
    if(e.key==='Tab'){                                    // Fokus-Trap
      var f=panel.querySelectorAll('button,a[href],input,select,textarea');
      if(!f.length) return;
      var first=f[0], last=f[f.length-1];
      if(e.shiftKey && document.activeElement===first){e.preventDefault();last.focus();}
      else if(!e.shiftKey && document.activeElement===last){e.preventDefault();first.focus();}
    }
  });
})();
</script>

Tooltip-Popover (rein CSS, :focus-within)

Pol 2 · Klarheit Anlass: Begriff / Detail erklären

Erklär-Popover an einem Begriff — erscheint bei :hover und, für Tastatur, bei :focus-within über dem fokussierbaren Auslöser. Ganz ohne JavaScript. Der Auslöser ist ein <button>, damit er per Tab erreichbar ist; der Text bleibt im Fluss und braucht keinen Scroll-Lock.

Festpreis-Garantie

Der genannte Preis gilt verbindlich. Nachträge nur bei ausdrücklich beauftragten Zusatzleistungen.

Kostenlose Besichtigung

Wir schätzen den Aufwand direkt vor Ort ein — unverbindlich und ohne Anfahrtskosten.

<span class="fx-tip">
  <button type="button" class="fx-tip__trigger">
    <svg viewBox="0 0 24 24" …>…</svg> Festpreis
  </button>
  <span class="fx-tip__pop" role="tooltip">
    <b>Festpreis-Garantie</b>
    <p>Der genannte Preis gilt verbindlich. Nachträge nur bei beauftragten Zusatzleistungen.</p>
  </span>
</span>

<style>
.fx-tip{position:relative;display:inline-flex}
.fx-tip__trigger{display:inline-flex;align-items:center;gap:6px;cursor:help;
  background:#1c1c22;border:1px solid #34343e;border-radius:99px;padding:.45rem .85rem;
  color:#e9e9ee;font:inherit;font-size:.85rem;font-weight:600;transition:border-color .2s ease,background .2s ease}
.fx-tip__trigger svg{width:15px;height:15px;color:var(--akzent, #7c87ff)}
.fx-tip:hover .fx-tip__trigger,.fx-tip:focus-within .fx-tip__trigger{border-color:var(--akzent, #7c87ff);background:#22222a}
.fx-tip__pop{position:absolute;bottom:calc(100% + 12px);left:50%;transform:translateX(-50%) translateY(6px);
  width:230px;max-width:78vw;background:#101014;border:1px solid #2b2b33;border-radius:12px;
  padding:12px 14px;box-shadow:0 18px 44px -18px rgba(0,0,0,.8);
  opacity:0;visibility:hidden;pointer-events:none;z-index:10;
  transition:opacity .22s ease,transform .22s cubic-bezier(.16,1,.3,1),visibility .22s ease}
.fx-tip__pop b{display:block;font-size:.82rem;font-weight:700;color:#f4f4f5;margin:0 0 4px}
.fx-tip__pop p{margin:0;font-size:.78rem;line-height:1.5;color:#a1a1aa}
.fx-tip__pop::after{content:"";position:absolute;top:100%;left:50%;transform:translateX(-50%);
  border:7px solid transparent;border-top-color:#101014}
.fx-tip:hover .fx-tip__pop,.fx-tip:focus-within .fx-tip__pop{opacity:1;visibility:visible;transform:translateX(-50%) translateY(0)}
@media (prefers-reduced-motion:reduce){.fx-tip__trigger,.fx-tip__pop{transition:none}}
</style>

Bild-Lightbox (rein CSS, :target)

Pol 1 · Craft Anlass: Galerie / Referenzbilder

Vergrößerung von Galeriebildern ohne JavaScript: jeder Kachel-Link springt per :target auf ein Overlay, X und Hintergrund tragen #_ und schließen wieder. Bewusst kompakt: die „Bilder" sind CSS-Verläufe statt echter Fotos, damit der Baustein self-contained bleibt — im Einsatz ersetzt man <i> durch ein <img>.

<div class="fx-lbGrid">
  <a class="fx-lbThumb" href="#lb-a" aria-label="Bild 1 vergrößern"><i class="fx-lbA"></i></a>
  <a class="fx-lbThumb" href="#lb-b" aria-label="Bild 2 vergrößern"><i class="fx-lbB"></i></a>
</div>

<div class="fx-lb" id="lb-a" role="dialog" aria-label="Bild 1">
  <a class="fx-lb__close" href="#_" aria-label="Schließen"></a>
  <div class="fx-lb__inner"><i class="fx-lbA"></i>
    <span class="fx-lb__cap">Referenz 1 · Innenausbau</span>
    <a class="fx-lb__x" href="#_" aria-label="Schließen">×</a></div>
</div>
<!-- im Einsatz: <i class="fx-lbA"> durch <img src="…" alt="…"> ersetzen -->

<style>
.fx-lbGrid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.fx-lbThumb{display:block;position:relative;aspect-ratio:4/3;border-radius:12px;overflow:hidden;
  border:1px solid #26262c;cursor:pointer;transition:transform .2s ease,border-color .2s ease}
.fx-lbThumb:hover,.fx-lbThumb:focus-visible{transform:translateY(-2px);border-color:var(--akzent, #7c87ff)}
.fx-lbThumb i{position:absolute;inset:0;display:block}
.fx-lbA{background:linear-gradient(135deg,#2a3a6a,#7c87ff)}
.fx-lbB{background:linear-gradient(135deg,#1e5f52,#3ecf8e)}
.fx-lbC{background:linear-gradient(135deg,#5a2d63,#c77dff)}
.fx-lb{position:fixed;inset:0;z-index:70;display:flex;align-items:center;justify-content:center;padding:24px;
  background:rgba(8,8,12,.82);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease}
.fx-lb:target{opacity:1;visibility:visible}
.fx-lb__inner{position:relative;width:min(360px,100%);aspect-ratio:4/3;border-radius:16px;overflow:hidden;
  border:1px solid #2b2b33;box-shadow:0 30px 70px -30px rgba(0,0,0,.9);
  transform:scale(.94);transition:transform .3s cubic-bezier(.16,1,.3,1)}
.fx-lb:target .fx-lb__inner{transform:none}
.fx-lb__inner i{position:absolute;inset:0;display:block}
.fx-lb__cap{position:absolute;left:0;right:0;bottom:0;padding:12px 14px;font-size:.8rem;color:#f4f4f5;
  background:linear-gradient(transparent,rgba(0,0,0,.72))}
.fx-lb__x{position:absolute;top:-14px;right:-14px;width:38px;height:38px;border-radius:50%;display:grid;
  place-items:center;background:#17171b;border:1px solid #34343e;color:#e9e9ee;font-size:20px;line-height:1;
  text-decoration:none;transition:background .2s ease,transform .2s ease}
.fx-lb__x:hover,.fx-lb__x:focus-visible{background:#23232b;transform:rotate(90deg)}
.fx-lb__close{position:absolute;inset:0;cursor:zoom-out}
@media (prefers-reduced-motion:reduce){
  .fx-lbThumb,.fx-lb,.fx-lb__inner,.fx-lb__x{transition:none}
  .fx-lbThumb:hover{transform:none}.fx-lb__x:hover{transform:none}}
</style>