Anruf-Knopf mit Erreichbarkeit
Der gelbe Anruf-Knopf eines Pflegedienstes weiß, ob das Büro besetzt ist: „Jetzt anrufen“, „Noch 20 Minuten“, nach Feierabend „Rückruf ab 8 Uhr anfragen“ mit Formular, auf dem Telefon als feste Leiste.
Betriebe, die vor allem telefonisch erreichbar sind und feste Bürozeiten haben (pflege, praxis, handwerk, kfz, it).
Der Anruf- oder Terminknopf kennt die Bürozeiten. Während der Zeit heißt er „Jetzt anrufen“ und sagt darunter, bis wann jemand rangeht. Kurz vor Schluss nennt er die Minuten. Außerhalb wird er zu „Rückruf ab 8 Uhr“ und öffnet ein kurzes Rückruf-Formular; die Nummer bleibt als Nebenweg sichtbar. Mit Rufbereitschaft wechselt nachts die Nummer, mit klarem Hinweis, für wen sie gilt. Auf dem Telefon steht der Knopf als feste Leiste unten.
Ein Anruf ins Leere kostet Vertrauen. Wer abends die Seite eines Pflegedienstes oder einer Werkstatt öffnet, will wissen, ob jetzt jemand rangeht. Der Knopf beantwortet das, bevor man wählt, und bietet den Weg an, der gerade funktioniert. So landen weniger Anrufe auf dem Anrufbeantworter und mehr Rückrufwünsche mit Zeitfenster im Postfach.
Wann einsetzen
Betriebe, die vor allem über das Telefon kommen: Pflege, Praxis, Handwerk, Kfz, Bestattung, Apotheke, IT-Systemhaus, Gebäudedienste. Immer zusammen mit echten, gepflegten Bürozeiten.
Wann nicht
Wenn die Zeiten nicht gepflegt werden (Urlaub, Feiertage): ein falscher Status ist schlimmer als keiner. Nicht für reine Online-Betriebe ohne Telefon. Keinen Status erfinden („3 Berater frei“), der nicht aus einer echten Quelle kommt.
Varianten
- Knopf: „Jetzt anrufen“ während der Bürozeit, kurz vor Schluss mit Minuten, sonst „Rückruf ab … anfragen“ mit Formular
- Rufbereitschaft: außerhalb der Bürozeit wird der Knopf zur Bereitschaftsnummer, nur für betreute Kund:innen; Neue fragen einen Rückruf an
- Kopf: Status als Pille in der festen Kopfzeile, auf dem Telefon die Leiste immer sichtbar
Umsetzung
Zeiten als Daten: WOCHE je Wochentag mit Zeiträumen, SONDER für Feiertage und Urlaub, BEREITSCHAFT als eigene Nummer mit Geltung. Ortszeit über Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hourCycle: 'h23', weekday, hour, minute }).formatToParts(). Zustand: offen, bald zu (unter 30 Min.), Pause, zu (mit nächstem Beginn). Der Knopf ist im HTML ein <a href="tel:…"> mit neutralem Text „Anrufen: 05235 …“; JS ersetzt nur Label und Statuszeile, die Höhe ist reserviert. Außerhalb der Zeit wird er zu <button aria-expanded>, der ein Rückruf-Formular (Telefon, Zeitfenster) öffnet; die Nummer bleibt als Textlink. Takt: setTimeout bis zur nächsten vollen Minute und bei visibilitychange. Auf dem Telefon position: fixed unten mit padding-bottom: env(safe-area-inset-bottom) und Platz am Seitenende.
Robust umsetzen
Telefon. Feste Leiste unten mit zwei Knöpfen (Anrufen bzw. Rückruf, Anfragen), mindestens 56 px hoch, mit safe-area. Die Statuszeile steht in der Leiste als Kurztext („Erreichbar bis 17 Uhr“). Das Rückruf-Formular öffnet als Blatt über der Leiste.
Ohne Bewegung. Formular und Blatt öffnen ohne Gleiten. Kein pulsierender Punkt.
Barrierefrei. Status steht als Wort im Knopf bzw. darunter, nie nur als Farbe. Die minütliche Aktualisierung ist still, nur ein Zustandswechsel wird über aria-live=polite angesagt. tel:-Link mit lesbarer Nummer. Knöpfe mindestens 56 px hoch und 4,5 : 1. Feiertage sind Ländersache: Liste je Standort pflegen und vom Kunden prüfen lassen.
Code
<!DOCTYPE html>
<!--
Anruf-Knopf mit Erreichbarkeit
Fundus · Buttons · https://cecodesign.de/lab/fundus/buttons/erreichbarkeits-button/
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, viewport-fit=cover">
<title>Anruf-Knopf mit Erreichbarkeit · 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: 'Literata'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/literata:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 200 900; font-display: swap; }
@font-face { font-family: 'Hanken Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/hanken-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-style: normal; font-display: swap; }
/* Welt: Rezept pflege-dienst-naehe · Pflegedienst Ahornweg, Blomberg (Lippe), erfunden */
:root {
--grund: #FAF7F2; --text: #1F1B16; --pflaume: #5B2A4A; --gelb: #FFC845; --hinweis: #FFF1CC; --leise: #4A443D; --linie: #D8D0C4;
--offen: #1E6B45; --zu: #8A7F72;
--ein: cubic-bezier(0.16, 1, 0.3, 1); --aus: cubic-bezier(0.7, 0, 0.84, 0);
--t-ui: 150ms; --t-auf: 300ms; --t-zu: 200ms;
--serif: 'Literata', Georgia, serif; --sans: 'Hanken Grotesk', system-ui, sans-serif;
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: var(--sans); font-size: 100%; -webkit-text-size-adjust: 100%; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; font-size: 1.25rem; line-height: 1.6; }
:focus-visible { outline: 3px solid var(--text); outline-offset: 3px; }
.dunkel :focus-visible { outline-color: #fff; }
a { color: inherit; }
button, input { font: inherit; color: inherit; }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.klein { font-size: 1rem; }
/* Kopf */
.kopf { display: flex; justify-content: space-between; align-items: center; gap: 12px 24px; flex-wrap: wrap; padding: 18px clamp(20px, 4vw, 56px); background: var(--grund); border-bottom: 1px solid var(--linie); }
.marke { font: 600 1.375rem/1.1 var(--serif); color: var(--pflaume); text-decoration: none; }
.marke span { display: block; font: 500 1rem/1.3 var(--sans); color: var(--leise); }
.kopf nav { display: flex; gap: 24px; font-size: 1.0625rem; font-weight: 500; }
.kopf nav a { text-decoration: none; }
.kopf-status { display: none; align-items: center; gap: 14px; }
.pille { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 6px 16px 6px 12px; border-radius: 999px; background: var(--hinweis); font-size: 1rem; font-weight: 600; }
.kopf-tel { font: 700 1.5rem/1 var(--sans); font-variant-numeric: tabular-nums; text-decoration: none; }
[data-variante=kopf] .kopf { position: sticky; top: 0; z-index: 20; }
[data-variante=kopf] .kopf nav { display: none; }
[data-variante=kopf] .kopf-status { display: flex; }
/* Punkt: Zustand nie nur über Farbe, das Wort steht immer daneben */
.punkt { width: 12px; height: 12px; border-radius: 50%; flex: none; background: var(--zu); transition: background-color var(--t-ui) linear, box-shadow var(--t-ui) linear; }
[data-z=offen] .punkt, .punkt[data-z=offen] { background: var(--offen); box-shadow: 0 0 0 5px rgba(30, 107, 69, .16); }
[data-z=bald] .punkt, .punkt[data-z=bald] { background: #C98A00; box-shadow: 0 0 0 5px rgba(201, 138, 0, .18); }
[data-z=pause] .punkt, .punkt[data-z=pause] { background: transparent; box-shadow: inset 0 0 0 2px var(--leise); }
[data-z=bereitschaft] .punkt, .punkt[data-z=bereitschaft] { background: var(--pflaume); box-shadow: 0 0 0 5px rgba(91, 42, 74, .16); }
/* Bühne */
.buehne { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: clamp(32px, 5vw, 80px); align-items: start; max-width: 1280px; margin: 0 auto; padding: clamp(36px, 6vw, 88px) clamp(20px, 4vw, 56px) 120px; }
.buehne h1 { font: 600 clamp(2.5rem, 1.8rem + 3vw, 4rem)/1.1 var(--serif); letter-spacing: -.01em; color: var(--pflaume); max-width: 16ch; text-wrap: balance; }
.lead { margin: 20px 0 34px; max-width: 34ch; }
/* ---------- Der Knopf ---------- */
.erreichbar { display: grid; gap: 14px; max-width: 34rem; }
.ruf { position: relative; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 4px 16px; min-height: 76px; padding: 12px 22px 12px 18px; border: 2px solid var(--text); border-radius: 12px; background: var(--gelb); color: var(--text); text-decoration: none; cursor: pointer; text-align: left; transition: background-color var(--t-ui) linear, transform var(--t-ui) var(--ein); }
.ruf:hover { background: #FFBC1F; }
.ruf:active { transform: translateY(1px); }
.ruf .icon { width: 44px; height: 44px; border-radius: 50%; background: var(--text); color: var(--gelb); display: grid; place-items: center; }
.ruf .titel { display: block; font: 700 1.375rem/1.2 var(--sans); }
.ruf .nr { display: block; font: 600 1.125rem/1.3 var(--sans); font-variant-numeric: tabular-nums; }
.ruf .pfeil { transition: transform var(--t-auf) var(--ein); }
.ruf:hover .pfeil { transform: translateX(4px); }
.ruf[aria-expanded=true] .pfeil { transform: rotate(90deg); }
.ruf[hidden] { display: none; }
.nebenweg button { background: none; border: 0; padding: 0; font-weight: 600; text-decoration: underline; text-underline-offset: 3px; cursor: pointer; min-height: 44px; }
.status { display: flex; align-items: center; gap: 12px; min-height: 2.2em; font-size: 1.125rem; }
.status b { font-weight: 700; }
.nebenweg { font-size: 1.0625rem; color: var(--leise); }
.nebenweg a { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
/* Rückruf-Formular öffnet unter dem Knopf */
.rueckruf { display: grid; grid-template-rows: 0fr; transition: grid-template-rows var(--t-zu) var(--aus); }
.rueckruf.auf { grid-template-rows: 1fr; transition: grid-template-rows var(--t-auf) var(--ein); }
.rueckruf > div { overflow: hidden; }
.rueckruf form, .rueckruf .notiert { margin-top: 2px; padding: 20px; border: 1px solid var(--linie); border-radius: 12px; background: #fff; }
.rueckruf label, .rueckruf legend { display: block; font-weight: 600; font-size: 1.0625rem; margin-bottom: 8px; }
.rueckruf input[type=tel] { width: 100%; height: 56px; border: 2px solid var(--leise); border-radius: 8px; padding: 0 14px; font-size: 1.25rem; background: var(--grund); }
.rueckruf input[aria-invalid=true] { border-color: #A3261B; }
.fehler { color: #A3261B; font-weight: 600; font-size: 1rem; margin-top: 6px; }
.fehler:empty { display: none; }
.rueckruf fieldset { border: 0; padding: 0; margin: 18px 0 0; min-width: 0; }
.fenster { display: flex; flex-wrap: wrap; gap: 8px; }
.fenster label { margin: 0; font-weight: 500; }
.fenster input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.fenster span { display: inline-flex; align-items: center; min-height: 48px; padding: 0 16px; border: 2px solid var(--linie); border-radius: 8px; cursor: pointer; }
.fenster input:checked + span { border-color: var(--text); background: var(--hinweis); font-weight: 700; }
.fenster input:focus-visible + span { outline: 3px solid var(--text); outline-offset: 2px; }
.senden { margin-top: 18px; min-height: 56px; padding: 0 22px; border: 2px solid var(--text); border-radius: 8px; background: var(--pflaume); color: #fff; font-weight: 700; cursor: pointer; }
.senden:hover { background: #4A2140; }
.notiert { display: flex; gap: 14px; align-items: flex-start; }
.notiert[hidden] { display: none; }
.notiert p:focus { outline: none; }
/* Seitenkarte: Zeiten */
.zeiten { background: var(--hinweis); border-radius: 12px; padding: 26px 26px 22px; }
.zeiten h2 { font: 600 1.5rem/1.2 var(--serif); color: var(--pflaume); margin-bottom: 14px; }
.zeiten table { width: 100%; border-collapse: collapse; font-size: 1.0625rem; font-variant-numeric: tabular-nums; }
.zeiten th, .zeiten td { text-align: left; padding: 9px 0; border-top: 1px solid rgba(74, 68, 61, .18); vertical-align: top; }
.zeiten th { font-weight: 500; width: 38%; }
.zeiten tr.heute th, .zeiten tr.heute td { font-weight: 700; }
.zeiten tr.heute th::after { content: ' · heute'; font-weight: 500; color: var(--leise); }
.zeiten .sonder { margin-top: 12px; padding: 10px 12px; background: #fff; border-left: 4px solid var(--pflaume); font-weight: 600; font-size: 1.0625rem; }
.zeiten .sonder[hidden] { display: none; }
.zeiten .bereit { margin-top: 18px; padding-top: 16px; border-top: 2px solid var(--pflaume); font-size: 1.0625rem; }
.zeiten .bereit b { display: block; }
.zeiten .bereit a { font-weight: 700; font-variant-numeric: tabular-nums; }
/* Telefon: feste Leiste unten */
.leiste { display: none; }
@media (max-width: 640px) {
.leiste { position: fixed; z-index: 30; left: 0; right: 0; bottom: 0; display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); gap: 8px; padding: 8px 10px calc(8px + env(safe-area-inset-bottom)); background: var(--grund); border-top: 1px solid var(--linie); box-shadow: 0 -6px 20px -12px rgba(31, 27, 22, .35); transform: translateY(0); transition: transform var(--t-auf) var(--ein); }
.leiste.weg { transform: translateY(110%); transition: transform var(--t-zu) var(--aus); visibility: hidden; transition: transform var(--t-zu) var(--aus), visibility 0s var(--t-zu); }
.leiste a, .leiste button { display: flex; flex-direction: column; justify-content: center; min-height: 58px; padding: 6px 12px; border: 2px solid var(--text); border-radius: 10px; text-decoration: none; font-weight: 700; font-size: 1.0625rem; line-height: 1.2; cursor: pointer; text-align: left; }
.leiste .l-ruf { background: var(--gelb); }
.leiste .l-ruf small { font-weight: 500; font-size: 1rem; display: flex; align-items: center; gap: 6px; }
.leiste .l-ruf .punkt { width: 9px; height: 9px; box-shadow: none !important; }
.leiste .l-anfrage { background: #fff; align-items: flex-start; }
body { padding-bottom: 92px; }
body:has(.leiste:not(.weg)) .pult { bottom: calc(88px + env(safe-area-inset-bottom)); } /* über der Leiste, sonst verdeckt „Steuerung +“ den Hauptknopf */
.kopf nav { display: none; }
[data-variante=kopf] .kopf-tel { display: none; }
.buehne { grid-template-columns: minmax(0, 1fr); }
.ruf { grid-template-columns: auto minmax(0, 1fr); padding: 12px 14px; }
.ruf .pfeil { display: none; }
}
/* Pult: simulierte Uhr */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 50; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; max-width: min(46rem, calc(100vw - 32px)); padding: 10px 12px; background: rgba(31, 27, 22, .93); color: #f3f2ee; font: 600 11px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase; border-radius: 10px; }
.pult .gruppe { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; }
.pult .gruppe > span { color: rgba(243, 242, 238, .72); margin-right: 4px; }
.pult button { background: transparent; color: inherit; border: 1px solid rgba(243, 242, 238, .3); border-radius: 5px; height: 28px; padding: 0 8px; font: inherit; letter-spacing: inherit; text-transform: inherit; cursor: pointer; }
.pult button[aria-pressed=true] { background: #f3f2ee; color: #1F1B16; border-color: #f3f2ee; }
.pult button:focus-visible { outline: 2px solid #f3f2ee; outline-offset: 2px; }
.pult output { font-variant-numeric: tabular-nums; letter-spacing: .04em; min-width: 9.5em; }
.vorschau body { overflow: hidden; }
.vorschau .kopf nav { display: none; }
:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
@media (hover: none) { .ruf:hover { background: var(--gelb); } .ruf:hover .pfeil { transform: none; } }
@media (forced-colors: active) {
button, [role=button], input, .ruf, .fenster span { border: 1px solid ButtonText !important; }
.punkt { forced-color-adjust: none; border: 1px solid CanvasText; }
}
</style>
</head>
<body>
<header class="kopf">
<a class="marke" href="#start">Pflegedienst Ahornweg<span>Blomberg und Umgebung</span></a>
<nav aria-label="Hauptmenü"><a href="#leistungen">Leistungen</a><a href="#kosten">Was die Kasse zahlt</a><a href="#team">Team</a><a href="#karriere">Karriere</a></nav>
<div class="kopf-status">
<span class="pille" data-kopf-status><span class="punkt" aria-hidden="true"></span><span data-kurz>Erreichbar bis 16:30 Uhr</span></span>
<a class="kopf-tel" href="tel:+495235504300" data-kopf-tel>05235 50 43-0</a>
</div>
</header>
<main id="start">
<section class="buehne" aria-labelledby="h1">
<div>
<h1 id="h1">Pflege zu Hause, in Blomberg und den Dörfern rundum.</h1>
<p class="lead">Wir helfen beim Waschen und Anziehen (Grundpflege), bringen Medikamente und beraten Angehörige. Rufen Sie uns an. Frau Aydin berät Sie.</p>
<div class="erreichbar" id="erreichbar" data-z="offen">
<a class="ruf" id="ruf" href="tel:+495235504300">
<span class="icon" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24"><path d="M6.6 10.8a15.1 15.1 0 0 0 6.6 6.6l2.2-2.2c.3-.3.7-.4 1-.2 1.1.4 2.3.6 3.6.6.6 0 1 .4 1 1V20c0 .6-.4 1-1 1A17 17 0 0 1 3 4c0-.6.4-1 1-1h3.5c.6 0 1 .4 1 1 0 1.3.2 2.5.6 3.6.1.3 0 .7-.2 1l-2.3 2.2z" fill="currentColor"/></svg></span>
<span><span class="titel" data-titel>Jetzt anrufen</span><span class="nr" data-nr>05235 50 43-0</span></span>
<svg class="pfeil" width="22" height="22" viewBox="0 0 22 22" aria-hidden="true"><path d="M3 11h15M12 5l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2.2"/></svg>
</a>
<button class="ruf" id="ruf-r" type="button" aria-expanded="false" aria-controls="rueckruf-box" hidden>
<span class="icon" aria-hidden="true"><svg width="22" height="22" viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 9 9" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 7v5l3 2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><path d="M17 3h4v4" fill="none" stroke="currentColor" stroke-width="2"/></svg></span>
<span><span class="titel" data-r-titel>Rückruf ab 8 Uhr anfragen</span><span class="nr" data-r-nr>Wir melden uns, wann es Ihnen passt</span></span>
<svg class="pfeil" width="22" height="22" viewBox="0 0 22 22" aria-hidden="true"><path d="M3 11h15M12 5l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2.2"/></svg>
</button>
<p class="status" id="status"><span class="punkt" aria-hidden="true"></span><span data-status><b>Erreichbar.</b> Frau Aydin ist bis 16:30 Uhr am Telefon.</span></p>
<div class="rueckruf" id="rueckruf-box">
<div inert>
<form id="rueckruf" novalidate>
<label for="r-tel">Ihre Telefonnummer</label>
<input type="tel" id="r-tel" autocomplete="tel" inputmode="tel" enterkeyhint="send" aria-describedby="r-tel-f">
<p class="fehler" id="r-tel-f"></p>
<fieldset>
<legend>Wann passt es Ihnen? <span class="klein" data-tag-text>(morgen)</span></legend>
<div class="fenster">
<label><input type="radio" name="fenster" value="8 und 10 Uhr" checked><span>8 bis 10 Uhr</span></label>
<label><input type="radio" name="fenster" value="10 und 12 Uhr"><span>10 bis 12 Uhr</span></label>
<label><input type="radio" name="fenster" value="13 und 16 Uhr"><span>13 bis 16 Uhr</span></label>
</div>
</fieldset>
<button class="senden" type="submit">Rückruf anfragen</button>
</form>
<div class="notiert" hidden>
<svg width="28" height="28" viewBox="0 0 28 28" aria-hidden="true"><circle cx="14" cy="14" r="13" fill="#1E6B45"/><path d="M8 14.5l4 4 8-9" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p tabindex="-1" data-notiert>Notiert. Wir rufen Sie morgen zwischen 8 und 10 Uhr an.</p>
</div>
</div>
</div>
<p class="nebenweg" data-neben>Oder schreiben Sie uns: <a href="#anfrage">Pflege anfragen</a></p>
</div>
</div>
<aside class="zeiten" aria-labelledby="zeiten-titel">
<h2 id="zeiten-titel">So erreichen Sie uns</h2>
<table>
<caption class="nur-sr">Telefonzeiten des Büros</caption>
<tbody>
<tr data-tag="1"><th scope="row">Montag</th><td>8 bis 12:30 Uhr, 13 bis 16:30 Uhr</td></tr>
<tr data-tag="2"><th scope="row">Dienstag</th><td>8 bis 12:30 Uhr, 13 bis 16:30 Uhr</td></tr>
<tr data-tag="3"><th scope="row">Mittwoch</th><td>8 bis 12:30 Uhr, 13 bis 16:30 Uhr</td></tr>
<tr data-tag="4"><th scope="row">Donnerstag</th><td>8 bis 12:30 Uhr, 13 bis 16:30 Uhr</td></tr>
<tr data-tag="5"><th scope="row">Freitag</th><td>8 bis 14 Uhr</td></tr>
<tr data-tag="6"><th scope="row">Wochenende</th><td>Büro geschlossen</td></tr>
</tbody>
</table>
<p class="sonder" data-sonder hidden></p>
<p class="bereit"><b>Sie werden schon von uns gepflegt?</b>Die Rufbereitschaft ist rund um die Uhr da: <a href="tel:+491713344550">0171 33 44 55-0</a></p>
</aside>
</section>
</main>
<nav class="leiste" id="leiste" aria-label="Schnellkontakt">
<a class="l-ruf" href="tel:+495235504300" data-l-ruf><span data-l-titel>Anrufen</span><small><span class="punkt" aria-hidden="true"></span><span data-l-status>bis 16:30 Uhr</span></small></a>
<a class="l-anfrage" href="#anfrage">Pflege anfragen</a>
</nav>
<p class="nur-sr" aria-live="polite" id="ansage"></p>
<div class="pult">
<div class="gruppe" role="group" aria-label="Variante"><span>Variante</span>
<button type="button" data-v="knopf" aria-pressed="true">Knopf</button>
<button type="button" data-v="bereitschaft" aria-pressed="false">Rufbereitschaft</button>
<button type="button" data-v="kopf" aria-pressed="false">Kopf</button>
</div>
<div class="gruppe" role="group" aria-label="Simulierte Uhrzeit"><span>Uhr</span>
<button type="button" data-uhr="echt" aria-pressed="true">Echt</button>
<button type="button" data-uhr="2026-11-04T10:15">Mi 10:15</button>
<button type="button" data-uhr="2026-11-04T16:10">Mi 16:10</button>
<button type="button" data-uhr="2026-11-04T12:40">Mi 12:40</button>
<button type="button" data-uhr="2026-11-06T19:30">Fr 19:30</button>
<button type="button" data-uhr="2026-12-25T10:00">25.12.</button>
</div>
<output data-uhr-anzeige aria-live="off"></output>
</div>
<script>
(() => {
const h = document.documentElement, $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
const reduziert = () => h.classList.contains('reduziert');
const VARIANTEN = ['knopf', 'bereitschaft', 'kopf'];
const q = new URLSearchParams(location.search);
let variante = VARIANTEN.includes(q.get('v')) ? q.get('v') : 'knopf';
/* Daten, wie sie der Betrieb pflegt (0 = Sonntag). Feiertage sind Ländersache (hier NRW): vom Kunden prüfen lassen. */
const WOCHE = { 1: [['08:00', '12:30'], ['13:00', '16:30']], 2: [['08:00', '12:30'], ['13:00', '16:30']], 3: [['08:00', '12:30'], ['13:00', '16:30']], 4: [['08:00', '12:30'], ['13:00', '16:30']], 5: [['08:00', '14:00']], 6: [], 0: [] };
const SONDER = { '2026-12-24': { grund: 'Heiligabend', zeiten: [['08:00', '12:00']] }, '2026-12-25': { grund: '1. Weihnachtstag', zeiten: [] }, '2026-12-26': { grund: '2. Weihnachtstag', zeiten: [] }, '2026-12-31': { grund: 'Silvester', zeiten: [['08:00', '12:00']] }, '2027-01-01': { grund: 'Neujahr', zeiten: [] } };
const BUERO = { nr: '05235 50 43-0', tel: '+495235504300', person: 'Frau Aydin' };
const BEREIT = { nr: '0171 33 44 55-0', tel: '+491713344550' };
const BALD = 30;
const zuMin = t => { const [a, b] = t.split(':'); return +a * 60 + +b; };
const uhr = m => { const hh = Math.floor(m / 60), mm = m % 60; return mm ? `${hh}:${String(mm).padStart(2, '0')} Uhr` : `${hh} Uhr`; };
const tagPlus = (iso, n) => { const d = new Date(iso + 'T12:00:00Z'); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); };
const wtag = iso => new Date(iso + 'T12:00:00Z').getUTCDay();
const TAGE = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
const zeitenAm = iso => (SONDER[iso] ? SONDER[iso].zeiten : WOCHE[wtag(iso)]).map(([a, b]) => [zuMin(a), zuMin(b)]);
let sim = null; /* { datum: 'YYYY-MM-DD', min } */
function jetzt() {
if (sim) return sim;
const p = Object.fromEntries(new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).formatToParts(new Date()).map(x => [x.type, x.value]));
return { datum: `${p.year}-${p.month}-${p.day}`, min: +p.hour * 60 + +p.minute };
}
function zustand({ datum, min }) {
const heute = zeitenAm(datum);
for (const [a, b] of heute) if (min >= a && min < b) return { art: b - min <= BALD ? 'bald' : 'offen', ende: b, rest: b - min };
const spaeter = heute.find(([a]) => a > min), frueher = heute.some(([, b]) => b <= min);
if (spaeter && frueher) return { art: 'pause', ab: spaeter[0], wann: 'heute', tagWort: 'heute' };
if (spaeter) return { art: 'zu', ab: spaeter[0], wann: 'heute', tagWort: 'heute', grund: SONDER[datum] && SONDER[datum].grund };
for (let n = 1; n <= 14; n++) {
const d = tagPlus(datum, n), z = zeitenAm(d);
if (z.length) {
const wann = n === 1 ? 'morgen' : n < 7 ? 'am ' + TAGE[wtag(d)] : 'am ' + new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', timeZone: 'UTC' }).format(new Date(d + 'T12:00:00Z'));
return { art: 'zu', ab: z[0][0], wann, tagWort: wann, grund: SONDER[datum] && SONDER[datum].grund };
}
}
return { art: 'zu', ab: 480, wann: 'bald', tagWort: 'bald' };
}
const er = $('#erreichbar'), ruf = $('#ruf'), rufR = $('#ruf-r'), box = $('#rueckruf-box'), boxInnen = box.firstElementChild;
const status = $('[data-status]'), neben = $('[data-neben]'), ansage = $('#ansage'), leiste = $('#leiste'), lRuf = $('[data-l-ruf]');
let letzte = null, aktuell = null;
function render() {
const j = jetzt(), z = zustand(j); aktuell = z;
const auf = z.art === 'offen' || z.art === 'bald';
const bereit = variante === 'bereitschaft' && !auf;
er.dataset.z = bereit ? 'bereitschaft' : z.art;
/* Hauptknopf: Anruf-Link oder Rückruf-Knopf */
ruf.hidden = !(auf || bereit); rufR.hidden = !ruf.hidden;
if (auf) {
ruf.href = 'tel:' + BUERO.tel; ruf.querySelector('[data-titel]').textContent = 'Jetzt anrufen'; ruf.querySelector('[data-nr]').textContent = BUERO.nr;
status.innerHTML = z.art === 'bald'
? `<b>Noch ${z.rest} Minuten.</b> Danach ${naechstText(j)}`
: `<b>Erreichbar.</b> ${BUERO.person} ist bis ${uhr(z.ende)} am Telefon.`;
} else if (bereit) {
ruf.href = 'tel:' + BEREIT.tel; ruf.querySelector('[data-titel]').textContent = 'Rufbereitschaft anrufen'; ruf.querySelector('[data-nr]').textContent = BEREIT.nr;
status.innerHTML = `<b>Rufbereitschaft.</b> Nur für Menschen, die wir schon pflegen. Das Büro ist ${z.wann} ab ${uhr(z.ab)} wieder da.`;
} else {
rufR.querySelector('[data-r-titel]').textContent = `Rückruf ab ${uhr(z.ab)} anfragen`;
rufR.querySelector('[data-r-nr]').textContent = z.art === 'pause' ? 'Mittagspause, wir melden uns danach' : `Wir melden uns ${z.wann}, wann es Ihnen passt`;
status.innerHTML = z.art === 'pause'
? `<b>Mittagspause bis ${uhr(z.ab)}.</b> Danach ist ${BUERO.person} wieder da.`
: `<b>${z.grund ? z.grund + ', Büro' : 'Büro'} geschlossen.</b> ${z.wann[0].toUpperCase() + z.wann.slice(1)} ab ${uhr(z.ab)} wieder erreichbar.`;
}
/* Nebenweg */
if (bereit) neben.innerHTML = 'Neu bei uns? <button type="button" data-rr>Rückruf anfragen</button>';
else if (auf) neben.innerHTML = variante === 'bereitschaft' ? `Nachts und am Wochenende für Kund:innen: <a href="tel:${BEREIT.tel}">${BEREIT.nr}</a>` : 'Oder schreiben Sie uns: <a href="#anfrage">Pflege anfragen</a>';
else neben.innerHTML = `Anrufbeantworter: <a href="tel:${BUERO.tel}">${BUERO.nr}</a>`;
const rr = neben.querySelector('[data-rr]'); if (rr) rr.addEventListener('click', () => oeffne(!box.classList.contains('auf'), rr));
if (!(auf || bereit) || bereit) { /* Formular passt zum nächsten Tag */ $('[data-tag-text]').textContent = '(' + (z.art === 'pause' ? 'heute' : z.tagWort) + ')'; }
if (auf && box.classList.contains('auf') && !bereit) oeffne(false);
/* Kopf-Pille und Leiste */
const kurz = auf ? (z.art === 'bald' ? `Noch ${z.rest} Min. erreichbar` : `Erreichbar bis ${uhr(z.ende)}`) : bereit ? 'Rufbereitschaft' : z.art === 'pause' ? `Mittagspause bis ${uhr(z.ab)}` : `Zu · ${z.wann} ab ${uhr(z.ab)}`;
$('[data-kurz]').textContent = kurz; $('[data-kopf-status]').dataset.z = er.dataset.z;
const kt = $('[data-kopf-tel]'); kt.textContent = bereit ? BEREIT.nr : BUERO.nr; kt.href = 'tel:' + (bereit ? BEREIT.tel : BUERO.tel);
lRuf.dataset.z = er.dataset.z;
lRuf.querySelector('[data-l-titel]').textContent = auf ? 'Anrufen' : bereit ? 'Rufbereitschaft' : 'Rückruf';
lRuf.querySelector('[data-l-status]').textContent = auf ? (z.art === 'bald' ? `noch ${z.rest} Min.` : `bis ${uhr(z.ende)}`) : bereit ? 'für Kund:innen' : `ab ${uhr(z.ab)}`;
lRuf.href = auf ? 'tel:' + BUERO.tel : bereit ? 'tel:' + BEREIT.tel : '#erreichbar';
lRuf.querySelector('.punkt').dataset.z = er.dataset.z;
/* Heute in der Tabelle */
const w = wtag(j.datum); $$('.zeiten tr').forEach(tr => tr.classList.toggle('heute', +tr.dataset.tag === (w === 0 ? 6 : w)));
const so = SONDER[j.datum], sz = $('[data-sonder]'); sz.hidden = !so; if (so) sz.textContent = 'Heute, ' + so.grund + ': ' + (so.zeiten.length ? so.zeiten.map(([a, b]) => uhr(zuMin(a)).replace(' Uhr', '') + ' bis ' + uhr(zuMin(b))).join(', ') : 'Büro geschlossen') + '.';
/* Ansage nur beim Wechsel */
const art = er.dataset.z;
if (letzte && letzte !== art) ansage.textContent = { offen: 'Das Büro ist jetzt erreichbar.', bald: 'Das Büro ist nur noch kurz erreichbar.', pause: 'Das Büro macht Mittagspause.', zu: 'Das Büro ist jetzt geschlossen. Sie können einen Rückruf anfragen.', bereitschaft: 'Jetzt ist die Rufbereitschaft für Kundinnen und Kunden erreichbar.' }[art];
letzte = art;
const a = $('[data-uhr-anzeige]'); if (a) a.textContent = new Intl.DateTimeFormat('de-DE', { weekday: 'short', day: 'numeric', month: 'short', timeZone: 'UTC' }).format(new Date(j.datum + 'T12:00:00Z')) + ', ' + uhr(j.min);
}
function naechstText(j) { const z = zustand({ datum: j.datum, min: (aktuell && aktuell.ende) || j.min }); return z.art === 'pause' ? `Mittagspause bis ${uhr(z.ab)}.` : `erreichen Sie uns ${z.wann} ab ${uhr(z.ab)}.`; }
/* Rückruf-Formular */
const form = $('#rueckruf'), rTel = $('#r-tel'), notiert = box.querySelector('.notiert');
function oeffne(an, ausloeser = rufR) {
box.classList.toggle('auf', an); boxInnen.inert = !an;
rufR.setAttribute('aria-expanded', an ? 'true' : 'false');
if (an) { form.hidden = false; notiert.hidden = true; setTimeout(() => rTel.focus({ preventScroll: true }), reduziert() ? 0 : 120); }
else if (ausloeser && document.activeElement && box.contains(document.activeElement)) ausloeser.focus();
}
rufR.addEventListener('click', () => oeffne(!box.classList.contains('auf')));
form.addEventListener('submit', e => {
e.preventDefault();
const ok = (rTel.value.match(/\d/g) || []).length >= 6;
rTel.setAttribute('aria-invalid', ok ? 'false' : 'true'); $('#r-tel-f').textContent = ok ? '' : 'Bitte geben Sie Ihre Telefonnummer ein, z. B. 05235 12345.';
if (!ok) { rTel.focus(); return; }
const f = form.querySelector('[name=fenster]:checked').value, z = aktuell;
notiert.querySelector('[data-notiert]').textContent = `Notiert. Wir rufen Sie ${z.art === 'pause' ? 'heute' : z.tagWort} zwischen ${f} an, unter ${rTel.value.trim()}.`;
form.hidden = true; notiert.hidden = false; notiert.querySelector('p').focus({ preventScroll: true });
ansage.textContent = 'Rückruf notiert.';
});
lRuf.addEventListener('click', e => {
if (lRuf.getAttribute('href') !== '#erreichbar') return;
e.preventDefault(); er.scrollIntoView({ behavior: reduziert() ? 'auto' : 'smooth', block: 'center' }); oeffne(true);
});
/* Leiste nur, wenn der große Knopf nicht zu sehen ist (in der Variante Kopf immer) */
let knopfSichtbar = true;
const sichtbar = new Set();
const io = new IntersectionObserver(es => { es.forEach(e => e.isIntersecting ? sichtbar.add(e.target) : sichtbar.delete(e.target)); knopfSichtbar = sichtbar.size > 0; leisteZeigen(); });
io.observe(ruf); io.observe(rufR);
function leisteZeigen() { const weg = variante !== 'kopf' && knopfSichtbar; leiste.classList.toggle('weg', weg); leiste.inert = weg; }
/* Takt: zur nächsten vollen Minute und beim Zurückkehren in den Tab */
let takt = null;
function plane() { clearTimeout(takt); if (sim) return; takt = setTimeout(() => { render(); plane(); }, 60000 - Date.now() % 60000 + 50); }
document.addEventListener('visibilitychange', () => { if (!document.hidden) { render(); plane(); } });
/* Pult */
function setzeVariante(v, schreiben) {
variante = v; document.body.dataset.variante = v;
$$('[data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v ? 'true' : 'false'));
if (schreiben) { const p = new URLSearchParams(location.search); p.set('v', v); history.replaceState(null, '', location.pathname + '?' + p + location.hash); }
leisteZeigen(); render();
}
function setzeUhr(wert) {
sim = wert === 'echt' ? null : { datum: wert.slice(0, 10), min: zuMin(wert.slice(11)) };
$$('[data-uhr]').forEach(b => b.setAttribute('aria-pressed', b.dataset.uhr === wert ? 'true' : 'false'));
render(); plane();
}
$$('[data-v]').forEach(b => b.addEventListener('click', () => { pause(); setzeVariante(b.dataset.v, true); }));
$$('[data-uhr]').forEach(b => b.addEventListener('click', () => { pause(); setzeUhr(b.dataset.uhr); }));
function neustart() { oeffne(false); form.reset(); rTel.setAttribute('aria-invalid', 'false'); $('#r-tel-f').textContent = ''; form.hidden = false; notiert.hidden = true; render(); }
/* Vorführung: Vormittag, kurz vor Schluss, Abend mit Rückruf */
let lauf = 0;
const warte = (ms, id) => new Promise((ok, nein) => setTimeout(() => (id === lauf ? ok() : nein('stopp')), ms));
async function runde(id) {
neustart();
setzeUhr('2026-11-04T10:15'); await warte(2000, id);
setzeUhr('2026-11-04T16:10'); await warte(2000, id);
for (let m = 16 * 60 + 11; m <= 16 * 60 + 14; m++) { sim.min = m; render(); await warte(260, id); }
setzeUhr('2026-11-06T19:30'); await warte(1600, id);
if (variante === 'bereitschaft') { const rr = neben.querySelector('[data-rr]'); oeffne(true, rr); } else oeffne(true);
await warte(500, id);
for (const c of '05235 98 12 07') { rTel.value += c; await warte(reduziert() ? 0 : 40, id); }
form.querySelector('[value="10 und 12 Uhr"]').checked = true; await warte(600, id);
form.requestSubmit(); await warte(2600, id);
runde(id).catch(() => {});
}
function vorfuehren() { const id = ++lauf; h.classList.add('vorfuehrung'); runde(id).catch(() => {}); }
function pause() { lauf++; h.classList.remove('vorfuehrung'); }
function weiter() { vorfuehren(); }
setzeVariante(variante, false);
plane();
})();
</script>
</body>
</html>