Balkendiagramm aus Datenattributen
Eine echte Tabelle wird zum Balkendiagramm: Die Zahl steht im Attribut, CSS rechnet die Länge, beim Einscrollen wachsen die Balken einmal auf ihren Wert.
Wirkung im Verein, Auslastung im Kulturhaus, Kennzahlen im Mittelstand: 3 bis 8 Zeilen mit echten Werten nebeneinander vergleichen.
Eine echte Tabelle oder Liste mit Werten wird ohne Skript zum ruhigen Balkendiagramm. Jede Zeile trägt ihren Wert als Zahl, der Balken daneben wächst auf seine Länge. Zahl und Beschriftung bleiben als Text sichtbar. Beim ersten Erscheinen wachsen die Balken einmal auf, danach steht alles.
Kennzahlen wirken glaubwürdiger, wenn man Verhältnisse sieht statt hochgezählter Riesenziffern. Weil die Daten im Markup stehen, bleibt alles lesbar, durchsuchbar und druckbar.
Wann einsetzen
Spendenziel und Mittelverwendung im Verein, Auslastung im Kulturhaus, Kennzahlen im Mittelstand, Tarifvergleich in SaaS. Wenige Zeilen (3 bis 8), echte Werte.
Wann nicht
Nicht für erfundene oder geschönte Zahlen. Nicht für komplexe Diagramme (dann daten-scrollytelling mit SVG). Nicht vor dem Spendenknopf als Inszenierung. Ohne Fallback nicht, denn Safari kann typisiertes attr() noch nicht.
Varianten
- balken: waagerechte Balken neben der Zahl
- anteil: gestapelter Balken über die volle Breite (Mittelverwendung)
- ziel: Fortschritt zu einem Ziel mit Zielmarke
Umsetzung
- Markup als table oder ul. Jede Zeile mit data-wert und zusätzlich style="
--wert: 42" (aus CMS oder Bau-Skript), damit alle Browser rechnen. - Balken als ::after oder eigenes Element mit
inline-size:calc(var(--wert) / var(--max) * 100%). - Kür:
@supports(width: calc(attr(data-wert type(<number>))* 1%)) setzt--wertaus dem Attribut. Die doppelte Angabe kann dann entfallen. - Einmaliges Wachsen mit scale von 0 auf 1 (transform-origin links) per css-view-reveal-nativ. Nie width animieren.
- Zahl in Tabellenziffern neben dem Balken. Farbe aus dem Signalton nur für die hervorgehobene Zeile.
- Fallback ohne Custom Property: nur die Tabelle.
Robust umsetzen
Telefon. Beschriftung über dem Balken statt daneben.
Ohne Bewegung. Balken stehen sofort in voller Länge.
Barrierefrei. Werte als Text in der Tabelle, Balken aria-hidden. Farbe nie einziger Träger, Balken 3:1 gegen den Grund. Bei 320 px und 200 % Zoom einspaltig, Zahlen nicht abgeschnitten. Keine Bewegung über 5 s.
Code
<!DOCTYPE html>
<!--
Balkendiagramm aus Datenattributen
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/css-diagramm-attr/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Balkendiagramm aus Datenattributen · 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: 'Big Shoulders'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/big-shoulders:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Archivo'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/archivo:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-stretch: 62% 125%; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
/* Rezept verein-nonprofit-wirkung */
:root {
--grund: #FFFFFF; --flaeche: #EDF5FA; --marke: #002E6D; --text: #222520; --grau: #DFDFD7; --spenden: #FFCA0A;
--balken: #6C8AC0; /* 3,5 : 1 auf Weiß */
--balken-2: #3E67A8; /* 5,7 : 1 */
--leise: #565a54; /* Meta 7 : 1 */
--linie: #DCDCDB;
--ease: cubic-bezier(0.83, 0, 0.17, 1); /* token-kinetisch-inout */
--ui: 150ms cubic-bezier(0.16, 1, 0.3, 1);
--rand: clamp(16px, 4vw, 48px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font: 400 1.0625rem/1.6 'Archivo', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { min-height: 100svh; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--marke); outline-offset: 3px; }
.label { font: 600 .75rem/1.35 'Archivo', system-ui, sans-serif; font-stretch: 112%; letter-spacing: .12em; text-transform: uppercase; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.zahl { font-family: 'Big Shoulders', system-ui, sans-serif; font-weight: 800; font-variant-numeric: tabular-nums lining-nums; }
/* ——— Seite ——— */
.nav { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 18px var(--rand); border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: center; gap: 10px; font: 800 1.375rem/1 'Big Shoulders', system-ui, sans-serif; text-transform: uppercase; color: var(--marke); letter-spacing: .01em; }
.marke svg { width: 30px; height: 18px; }
.nav-links { display: flex; align-items: center; gap: 22px; }
.spenden { display: inline-flex; align-items: center; min-height: 44px; padding: 0 20px; border-radius: 4px; background: var(--spenden); color: var(--text); font-weight: 600; text-decoration: none; }
.intro { min-height: min(66svh, 560px); display: grid; grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); align-content: end; gap: 24px var(--rand); padding: 48px var(--rand) 56px; background: var(--flaeche); }
.intro h1 { font: 800 clamp(2.75rem, 1.6rem + 5vw, 6rem)/.95 'Big Shoulders', system-ui, sans-serif; letter-spacing: .005em; text-transform: uppercase; color: var(--marke); margin-top: 14px; text-wrap: balance; }
.intro .lead { max-width: 40ch; font-size: 1.1875rem; }
.kennzahl { display: flex; align-items: baseline; gap: 14px; margin-bottom: 18px; color: var(--marke); }
.kennzahl .zahl { font-size: clamp(3.5rem, 2rem + 5vw, 6.5rem); line-height: .8; }
.kennzahl .label { max-width: 10em; }
.intro .hinweis { align-self: end; justify-self: end; display: flex; align-items: center; gap: 10px; color: var(--marke); }
.intro .hinweis::after { content: ""; width: 1px; height: 44px; background: currentColor; }
.sektion { padding: 72px var(--rand) 88px; }
.kopfzeile { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 16px var(--rand); align-items: end; margin-bottom: 40px; }
.kopfzeile h2 { font: 700 clamp(1.75rem, 1.2rem + 2vw, 3rem)/1 'Big Shoulders', system-ui, sans-serif; text-transform: uppercase; color: var(--marke); margin-top: 10px; }
.kopfzeile p { max-width: 52ch; }
.quelle { margin-top: 26px; color: var(--leise); font-size: .875rem; }
/* =====================================================================
DAS MUSTER: Wert im Markup, Balken rechnet CSS
- data-wert / data-max tragen die Zahl.
- Robuster Weg: style="--w: 4120" (Name absichtlich anders als --wert,
sonst gewinnt der Inline-Style immer gegen die attr()-Regel).
- Kür: typisiertes attr() liest die Zahl direkt aus dem Attribut.
===================================================================== */
[data-wert] { --wert: var(--w, 0); }
[data-max] { --max: var(--m, 1); }
[data-bei] { --bei: var(--b, 0); }
@supports (width: calc(attr(data-wert type(<number>)) * 1%)) {
:root:not(.weg-wert) [data-wert] { --wert: attr(data-wert type(<number>), 0); }
:root:not(.weg-wert) [data-max] { --max: attr(data-max type(<number>), 1); }
:root:not(.weg-wert) [data-bei] { --bei: attr(data-bei type(<number>), 0); }
}
/* Variante balken: echte Tabelle, Balken in eigener Zelle */
.tabelle { width: 100%; border-collapse: collapse; }
.tabelle caption { text-align: left; }
.tabelle th, .tabelle td { padding: 14px 0; border-bottom: 1px solid var(--linie); vertical-align: middle; text-align: left; font-weight: 400; }
.tabelle thead th { padding: 0 0 10px; color: var(--leise); }
.b-tabelle th[scope=row] { width: 15rem; padding-right: 24px; font-weight: 500; }
.b-tabelle .z { width: 7.5rem; text-align: right; font-size: clamp(1.75rem, 1.2rem + 1.6vw, 2.5rem); line-height: 1; color: var(--marke); }
.b-tabelle thead .z { font: inherit; color: var(--leise); }
.spur { position: relative; height: 22px;
/* Hilfslinien alle 1.000 Stunden, auch aus dem Maximum gerechnet */
background: repeating-linear-gradient(to right, var(--linie) 0 1px, transparent 1px calc(1000 / var(--max) * 100%)); }
.balken { position: absolute; inset-block: 0; left: 0; inline-size: calc(var(--wert) / var(--max) * 100%); background: var(--balken); transform-origin: left center; }
tr.hervor .balken { background: var(--marke); }
tr.hervor th { color: var(--marke); font-weight: 700; }
.b-tabelle tfoot td, .b-tabelle tfoot th { border-bottom: 0; padding-top: 18px; }
.b-tabelle tfoot .z { font-size: 1.5rem; }
/* Variante anteil: gestapelter Balken, Breite per flex-grow direkt aus dem Wert */
.stapel { display: flex; gap: 3px; height: clamp(64px, 9vw, 104px); }
.stapel span { flex: var(--wert) 1 0; min-width: 4px; position: relative; transform-origin: left center; display: flex; align-items: flex-end; padding: 10px 12px; color: #fff; overflow: hidden; }
.stapel span b { font: 800 clamp(1.25rem, .8rem + 1.6vw, 2.25rem)/1 'Big Shoulders', system-ui, sans-serif; font-variant-numeric: tabular-nums; white-space: nowrap; }
.s1 { background: var(--marke); }
.s2 { background: var(--balken-2); }
.s3 { background: var(--balken); }
.s4 { background: repeating-linear-gradient(-45deg, var(--marke) 0 2px, #fff 2px 7px); box-shadow: inset 0 0 0 1.5px var(--marke); }
.feld { display: inline-block; width: 14px; height: 14px; margin-right: 10px; vertical-align: -2px; }
.a-tabelle { margin-top: 28px; }
.a-tabelle td.z, .a-tabelle th.z { text-align: right; font-variant-numeric: tabular-nums; }
.a-tabelle td.z { font-family: 'Big Shoulders', system-ui, sans-serif; font-weight: 700; font-size: 1.5rem; line-height: 1; color: var(--marke); }
.a-tabelle th[scope=row] { font-weight: 500; }
/* Variante ziel: Fortschritt mit Zielmarken */
.ziel-zahl { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 18px; color: var(--marke); }
.ziel-zahl .zahl { font-size: clamp(4rem, 2rem + 7vw, 8.5rem); line-height: .85; }
.ziel-zahl .von { font: 700 clamp(1.5rem, 1rem + 1.6vw, 2.5rem)/1 'Big Shoulders', system-ui, sans-serif; text-transform: uppercase; }
.ziel-spur { position: relative; height: 40px; margin: 34px 0 64px; background: var(--flaeche); box-shadow: inset 0 0 0 1px #9fb3cf; }
.ziel-spur .balken { background: var(--marke); }
.marke-ziel { position: absolute; top: -10px; bottom: -10px; left: calc(var(--bei) / var(--max) * 100%); width: 2px; margin-left: -1px; background: var(--text); }
.marke-ziel:last-child { margin-left: -2px; }
.marke-ziel span { position: absolute; top: calc(100% + 6px); white-space: nowrap; translate: -50% 0; color: var(--leise); }
.marke-ziel:last-child span { translate: -100% 0; }
.ziel-tabelle th[scope=row] { font-weight: 500; }
.ziel-tabelle th, .ziel-tabelle td { padding-right: 14px; }
.ziel-tabelle .z { padding-right: 0; }
.ziel-tabelle .z { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.erreicht { color: #1f6b3a; font-weight: 600; }
.ziel-fakten { display: flex; flex-wrap: wrap; gap: 8px 28px; margin-top: 12px; color: var(--leise); }
.ziel-fakten b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
/* ——— Einmal wachsen: scrollgekoppelt (css-view-reveal-nativ), sonst einmalig per IntersectionObserver ——— */
@keyframes wachsen { from { scale: 0 1; } to { scale: 1 1; } }
@supports (animation-timeline: view()) {
/* Zeitleiste ist die ganze Grafik: ist sie vollständig im Bild (entry 100 %), stehen alle Balken auf voller Länge */
.diagramm { view-timeline: --diagramm block; }
:root:not(.reduziert):not(.ohne-zeitleiste) .wachsen {
animation: wachsen var(--ease) both;
animation-timeline: --diagramm;
animation-range: entry calc(var(--n, 0) * 6%) entry calc(70% + var(--n, 0) * 6%);
}
}
/* Rückfall ohne view(): einmal per Klasse, 800 ms, 50 ms Versatz je Zeile */
:root.ohne-zeitleiste:not(.reduziert) .wachsen { scale: 0 1; transition: scale 800ms var(--ease); transition-delay: calc(var(--n, 0) * 50ms); }
:root.ohne-zeitleiste:not(.reduziert) .gesehen .wachsen { scale: 1 1; }
tr:nth-child(2) .wachsen, .stapel span:nth-child(2) { --n: 1; }
tr:nth-child(3) .wachsen, .stapel span:nth-child(3) { --n: 2; }
tr:nth-child(4) .wachsen, .stapel span:nth-child(4) { --n: 3; }
tr:nth-child(5) .wachsen { --n: 4; }
/* Weg „nur Tabelle“: so sieht die Seite ohne Custom Properties aus */
:root.weg-tabelle .spur, :root.weg-tabelle .stapel, :root.weg-tabelle .ziel-spur { display: none; }
:root.weg-tabelle .feld { display: none; }
.fuss { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 16px 32px; padding: 40px var(--rand) 120px; background: var(--marke); color: #fff; }
.fuss p { max-width: 46ch; }
/* ——— Pult ——— */
.pult { position: fixed; left: 16px; bottom: 16px; z-index: 30; display: grid; gap: 10px; width: min(440px, calc(100vw - 32px)); padding: 12px 14px; border-radius: 12px; background: rgba(16, 20, 30, .92); color: rgba(255, 255, 255, .72); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); }
.pult .reihe { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
.pult .reihe > span:first-child { min-width: 5.5em; }
.pult button { font: inherit; letter-spacing: inherit; text-transform: inherit; min-height: 30px; padding: 0 10px; border-radius: 999px; border: 1px solid rgba(255, 255, 255, .22); background: none; color: rgba(255, 255, 255, .8); cursor: pointer; transition: background-color var(--ui), color var(--ui); }
.pult button:hover { color: #fff; }
.pult button[aria-pressed="true"] { background: #fff; color: #10141e; border-color: #fff; }
.pult button:disabled { opacity: .5; cursor: not-allowed; }
.pult input { flex: 1; min-width: 120px; accent-color: var(--spenden); }
.pult output { min-width: 5em; text-align: right; color: #fff; font-variant-numeric: tabular-nums; }
.pult .stand { color: #fff; text-transform: none; letter-spacing: .02em; }
.pult .stand i { font-style: normal; color: #9fe0b2; }
.pult .stand i.nein { color: #ffb59a; }
.pult :focus-visible { outline-color: var(--spenden); }
.vorschau body { overflow: hidden; }
.vorschau .fuss { padding-bottom: 40px; }
/* ——— schmal ——— */
@media (max-width: 760px) {
.intro { grid-template-columns: minmax(0, 1fr); min-height: 0; padding-top: 40px; }
.intro .hinweis { justify-self: start; }
.kopfzeile { grid-template-columns: minmax(0, 1fr); }
.nav-links .label { display: none; }
/* Beschriftung über dem Balken: Zeile als Raster, Rollen halten die Tabellensemantik */
.b-tabelle thead { display: none; }
.b-tabelle tr { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; border-bottom: 1px solid var(--linie); padding: 12px 0; }
.b-tabelle th, .b-tabelle td { border: 0; padding: 0; width: auto !important; }
.b-tabelle th[scope=row] { grid-row: 1; grid-column: 1; }
.b-tabelle td.z { grid-row: 1; grid-column: 2; }
.b-tabelle td.spur-zelle { grid-row: 2; grid-column: 1 / -1; padding-top: 10px; }
.b-tabelle tfoot tr { border: 0; }
.marke-ziel span { font-size: 10px; letter-spacing: .06em; }
.marke-ziel:nth-child(2) span { display: none; }
.a-tabelle th.z:nth-of-type(2), .a-tabelle td.z:nth-of-type(1) { display: none; }
}
@media (max-width: 380px) {
.stapel span b { display: none; }
}
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.balken, .stapel span, .marke-ziel { forced-color-adjust: none; background: CanvasText !important; }
.stapel span { border: 1px solid Canvas; }
}
</style>
</head>
<body>
<header class="nav">
<span class="marke"><svg viewBox="0 0 30 18" aria-hidden="true"><path d="M1 17 C 8 3, 22 3, 29 17" fill="none" stroke="currentColor" stroke-width="3"/><path d="M9 17 V 9 M15 17 V 7 M21 17 V 9" stroke="currentColor" stroke-width="2"/></svg>Lernbrücke e. V.</span>
<nav class="nav-links" aria-label="Beispielnavigation Verein"><span class="label">Angebote</span><span class="label">Mitmachen</span><span class="label">Transparenz</span><a class="spenden" href="#fuss">Spenden</a></nav>
</header>
<section class="intro" aria-labelledby="h1">
<div>
<p class="label">Jahresbericht 2025 · Bernburg (Saale)</p>
<h1 id="h1">Was aus Ihrer Spende wird</h1>
</div>
<div>
<p class="kennzahl"><span class="zahl">214</span><span class="label">Kinder in der Begleitung</span></p>
<p class="lead">Wir begleiten 214 Kinder durch die Schulwoche: Hausaufgaben, Lesen, Ferien. Hier stehen die Zahlen, so wie wir sie zählen.</p>
</div>
</section>
<!-- ========== Variante balken ========== -->
<section class="sektion" data-welt="balken" aria-labelledby="h-balken">
<div class="kopfzeile">
<div><p class="label">Wirkung</p><h2 id="h-balken">Lernstunden nach Angebot</h2></div>
<p>Jede Stunde zählt eine ehrenamtliche Kraft mit einem Kind. Am meisten Zeit fließt in die tägliche Hausaufgabenhilfe.</p>
</div>
<table class="tabelle b-tabelle diagramm" role="table">
<caption class="nur-sr">Lernstunden 2025 nach Angebot, in Stunden</caption>
<thead role="rowgroup"><tr role="row"><th scope="col" class="label" role="columnheader">Angebot</th><th scope="col" class="label" role="columnheader"><span class="nur-sr">Balken</span></th><th scope="col" class="label z" role="columnheader">Stunden</th></tr></thead>
<tbody data-max="4500" style="--m: 4500" role="rowgroup">
<tr class="hervor" data-wert="4120" style="--w: 4120" data-probe role="row"><th scope="row" role="rowheader">Hausaufgabenhilfe</th><td class="spur-zelle" role="cell"><div class="spur" aria-hidden="true"><span class="balken wachsen"></span></div></td><td class="z zahl" role="cell" data-probe-text>4.120</td></tr>
<tr data-wert="2680" style="--w: 2680" role="row"><th scope="row" role="rowheader">Lesepatenschaften</th><td class="spur-zelle" role="cell"><div class="spur" aria-hidden="true"><span class="balken wachsen"></span></div></td><td class="z zahl" role="cell">2.680</td></tr>
<tr data-wert="1340" style="--w: 1340" role="row"><th scope="row" role="rowheader">Ferienkurse</th><td class="spur-zelle" role="cell"><div class="spur" aria-hidden="true"><span class="balken wachsen"></span></div></td><td class="z zahl" role="cell">1.340</td></tr>
<tr data-wert="960" style="--w: 960" role="row"><th scope="row" role="rowheader">Sprachcafé für Eltern</th><td class="spur-zelle" role="cell"><div class="spur" aria-hidden="true"><span class="balken wachsen"></span></div></td><td class="z zahl" role="cell">960</td></tr>
<tr data-wert="720" style="--w: 720" role="row"><th scope="row" role="rowheader">Mathe-Werkstatt</th><td class="spur-zelle" role="cell"><div class="spur" aria-hidden="true"><span class="balken wachsen"></span></div></td><td class="z zahl" role="cell">720</td></tr>
</tbody>
<tfoot role="rowgroup"><tr role="row"><th scope="row" class="label" role="rowheader">Zusammen</th><td class="spur-zelle" role="cell"></td><td class="z zahl" role="cell" data-summe>9.820</td></tr></tfoot>
</table>
<p class="quelle">Gezählt über Anwesenheitslisten der Lernorte, Stand 31.12.2025. Demo mit erfundenen Zahlen.</p>
</section>
<!-- ========== Variante anteil ========== -->
<section class="sektion" data-welt="anteil" aria-labelledby="h-anteil" hidden>
<div class="kopfzeile">
<div><p class="label">Transparenz</p><h2 id="h-anteil">Mittelverwendung 2025</h2></div>
<p>186.400 Euro sind 2025 ausgegeben worden. Knapp vier von fünf Euro gingen direkt in die Arbeit mit den Kindern.</p>
</div>
<div class="stapel diagramm" aria-hidden="true">
<span class="s1 wachsen" data-wert="147300" style="--w: 147300" data-probe><b>79 %</b></span>
<span class="s2 wachsen" data-wert="16800" style="--w: 16800"><b>9 %</b></span>
<span class="s3 wachsen" data-wert="13000" style="--w: 13000"><b>7 %</b></span>
<span class="s4 wachsen" data-wert="9300" style="--w: 9300"></span>
</div>
<table class="tabelle a-tabelle">
<caption class="nur-sr">Mittelverwendung 2025 in Euro und Prozent</caption>
<thead><tr><th scope="col" class="label">Bereich</th><th scope="col" class="label z">Euro</th><th scope="col" class="label z">Anteil</th></tr></thead>
<tbody>
<tr><th scope="row"><span class="feld s1" aria-hidden="true"></span>Projektarbeit mit Kindern</th><td class="z" data-probe-text>147.300 €</td><td class="z">79 %</td></tr>
<tr><th scope="row"><span class="feld s2" aria-hidden="true"></span>Verwaltung und Buchhaltung</th><td class="z">16.800 €</td><td class="z">9 %</td></tr>
<tr><th scope="row"><span class="feld s3" aria-hidden="true"></span>Rücklage Lernraum Nord</th><td class="z">13.000 €</td><td class="z">7 %</td></tr>
<tr><th scope="row"><span class="feld s4" aria-hidden="true"></span>Spendenwerbung</th><td class="z">9.300 €</td><td class="z">5 %</td></tr>
</tbody>
</table>
<p class="quelle">Aus dem Jahresabschluss 2025. Anteile gerundet. Demo mit erfundenen Zahlen.</p>
</section>
<!-- ========== Variante ziel ========== -->
<section class="sektion" data-welt="ziel" aria-labelledby="h-ziel" hidden>
<div class="kopfzeile">
<div><p class="label">Spendenziel bis 31.12.2026</p><h2 id="h-ziel">Lernraum Nord</h2></div>
<p>Ein zweiter Raum in der Nordstadt, damit niemand mehr auf der Warteliste steht. Jede Marke ist ein fertiger Bauabschnitt.</p>
</div>
<p class="ziel-zahl"><span class="zahl" data-probe-text>31.420 €</span><span class="von">von 48.000 €</span></p>
<p class="ziel-fakten"><span>noch <b data-rest>16.580 €</b></span><span><b>412</b> Spenden</span><span>Stand <b>02.11.2026</b></span></p>
<div class="ziel-spur diagramm" data-max="48000" style="--m: 48000" aria-hidden="true">
<span class="balken wachsen" data-wert="31420" style="--w: 31420" data-probe></span>
<span class="marke-ziel" data-bei="16000" style="--b: 16000"><span class="mono">16.000</span></span>
<span class="marke-ziel" data-bei="32000" style="--b: 32000"><span class="mono">32.000</span></span>
<span class="marke-ziel" data-bei="48000" style="--b: 48000"><span class="mono">48.000</span></span>
</div>
<table class="tabelle ziel-tabelle">
<caption class="nur-sr">Bauabschnitte des Lernraums Nord</caption>
<thead><tr><th scope="col" class="label">Abschnitt</th><th scope="col" class="label">Stand</th><th scope="col" class="label z">ab</th></tr></thead>
<tbody>
<tr><th scope="row">Möbel, Regale und Leselampen</th><td class="erreicht" data-stand="16000">erreicht</td><td class="z">16.000 €</td></tr>
<tr><th scope="row">Zwölf Laptops mit Lernsoftware</th><td data-stand="32000">offen</td><td class="z">32.000 €</td></tr>
<tr><th scope="row">Miete für zwei Jahre</th><td data-stand="48000">offen</td><td class="z">48.000 €</td></tr>
</tbody>
</table>
<p class="quelle">Spendenstand täglich aus der Buchhaltung. Demo mit erfundenen Zahlen.</p>
</section>
<footer class="fuss" id="fuss">
<p>Lernbrücke e. V. · Breite Straße 40 · 06406 Bernburg. Gemeinnützig, Spenden sind steuerlich absetzbar (vom Kunden prüfen lassen).</p>
<a class="spenden" href="#fuss">Jetzt spenden</a>
</footer>
<div class="pult mono">
<div class="reihe" role="group" aria-label="Variante">
<span>Variante</span>
<button type="button" data-v-wahl="balken" aria-pressed="true">Balken</button>
<button type="button" data-v-wahl="anteil" aria-pressed="false">Anteil</button>
<button type="button" data-v-wahl="ziel" aria-pressed="false">Ziel</button>
</div>
<div class="reihe" role="group" aria-label="Rechenweg">
<span>Weg</span>
<button type="button" data-weg="attr" aria-pressed="true">attr()</button>
<button type="button" data-weg="wert" aria-pressed="false">--w inline</button>
<button type="button" data-weg="tabelle" aria-pressed="false">nur Tabelle</button>
</div>
<label class="reihe"><span data-probe-name>Probe</span><input type="range" data-probe-regler min="0" max="4500" step="10" value="4120"><output data-probe-aus>4.120</output></label>
<p class="stand" data-status></p>
</div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, w = document) => w.querySelector(s), $$ = (s, w = document) => [...w.querySelectorAll(s)];
const kannAttr = CSS.supports('width', 'calc(attr(data-wert type(<number>)) * 1%)');
const kannView = CSS.supports('animation-timeline', 'view()');
const zahl = new Intl.NumberFormat('de-DE');
if (!kannView) html.classList.add('ohne-zeitleiste');
const IDS = ['balken', 'anteil', 'ziel'];
// Probe: ändert NUR data-wert und den Text. Im Weg --w inline bleibt der Balken deshalb stehen.
const PROBE = {
balken: { name: 'Hausaufg.', min: 0, max: 4500, step: 10, text: w => zahl.format(w) },
anteil: { name: 'Projekte', min: 20000, max: 300000, step: 100, text: w => zahl.format(w) + ' €' },
ziel: { name: 'Gespendet', min: 0, max: 48000, step: 10, text: w => zahl.format(w) + ' €' }
};
const START = { balken: 4120, anteil: 147300, ziel: 31420 };
let v = new URLSearchParams(location.search).get('v');
if (!IDS.includes(v)) v = IDS[0];
let weg = kannAttr ? 'attr' : 'wert';
const regler = $('[data-probe-regler]'), aus = $('[data-probe-aus]'), stand = $('[data-status]');
function welt() { return $(`[data-welt="${v}"]`); }
function probe(w) {
const sek = welt(), p = PROBE[v];
const el = $('[data-probe]', sek);
el.setAttribute('data-wert', w); // nur das Attribut
$('[data-probe-text]', sek).textContent = p.text(w);
aus.textContent = p.text(w);
if (v === 'balken') {
const summe = $$('tbody tr[data-wert]', sek).reduce((s, tr) => s + +tr.dataset.wert, 0);
$('[data-summe]', sek).textContent = zahl.format(summe);
}
if (v === 'ziel') {
$('[data-rest]', sek).textContent = zahl.format(Math.max(0, 48000 - w)) + ' €';
$$('[data-stand]', sek).forEach(td => { const ok = w >= +td.dataset.stand; td.textContent = ok ? 'erreicht' : 'offen'; td.classList.toggle('erreicht', ok); });
}
if (v === 'anteil') {
// Anteile im Text neu rechnen, der Balken braucht dafür kein Skript (flex-grow)
const teile = $$('.stapel span', sek), summe = teile.reduce((s, t) => s + +t.dataset.wert, 0);
const zeilen = $$('.a-tabelle tbody tr', sek);
teile.forEach((t, i) => {
const pz = Math.round(t.dataset.wert / summe * 100) + ' %';
if ($('b', t)) $('b', t).textContent = pz;
zeilen[i].lastElementChild.textContent = pz;
});
}
}
function zeigeStand() {
const t = { attr: 'Balken aus attr(data-wert type(<number>))', wert: 'Balken aus --w im style-Attribut', tabelle: 'nur die Tabelle, keine Balken' }[weg];
stand.innerHTML = `attr() typisiert: ${kannAttr ? '<i>läuft hier</i>' : '<i class="nein">fehlt</i>'} · view(): ${kannView ? '<i>läuft hier</i>' : '<i class="nein">fehlt</i>'}<br>Jetzt: ${t}`;
}
function setzeWeg(neu) {
weg = neu;
html.classList.toggle('weg-wert', neu === 'wert');
html.classList.toggle('weg-tabelle', neu === 'tabelle');
$$('[data-weg]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.weg === neu)));
zeigeStand();
}
$$('[data-weg]').forEach(b => {
if (b.dataset.weg === 'attr' && !kannAttr) b.disabled = true;
b.addEventListener('click', () => { stopp(); setzeWeg(b.dataset.weg); });
});
function setze(neu, url) {
v = neu;
html.dataset.v = neu;
$$('[data-welt]').forEach(w => { w.hidden = w.dataset.welt !== neu; });
$$('[data-v-wahl]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.vWahl === neu)));
if (url) {
const q = new URLSearchParams(location.search); q.set('v', neu);
history.replaceState(null, '', location.pathname + '?' + q.toString().replace(/=(?=&|$)/g, '') + location.hash);
}
const p = PROBE[neu], w = +$('[data-probe]', welt()).dataset.wert;
$('[data-probe-name]').textContent = p.name;
regler.min = p.min; regler.max = p.max; regler.step = p.step; regler.value = w;
regler.setAttribute('aria-label', 'Probewert für ' + { balken: 'Hausaufgabenhilfe in Stunden', anteil: 'Projektarbeit in Euro', ziel: 'gespendeten Betrag in Euro' }[neu]);
aus.textContent = p.text(w);
beobachte();
}
$$('[data-v-wahl]').forEach(b => b.addEventListener('click', () => { stopp(); setze(b.dataset.vWahl, true); }));
regler.addEventListener('input', () => { stopp(); probe(+regler.value); });
document.addEventListener('click', e => { const a = e.target.closest('a[href^="#"]'); if (a && !a.closest('.pult')) e.preventDefault(); });
// Rückfall ohne view(): einmal einblenden, sobald die Grafik zu 25 % sichtbar ist
let io = null;
function beobachte() {
if (kannView) return;
if (io) io.disconnect();
io = new IntersectionObserver(es => es.forEach(e => { if (e.isIntersecting) { e.target.classList.add('gesehen'); io.unobserve(e.target); } }), { threshold: .25 });
$$('table, .stapel, .ziel-spur', welt()).forEach(el => io.observe(el));
}
// Vorführung: Seite scrollt zur Grafik, die Balken wachsen mit, kurz halten, zurück
let uhr = null, tick = null;
function stopp() { cancelAnimationFrame(uhr); clearTimeout(tick); uhr = tick = null; html.classList.remove('vorfuehrung'); }
function zuruecksetzen() {
probe(START[v]); regler.value = START[v];
if (!kannView) $$('.gesehen').forEach(el => el.classList.remove('gesehen'));
}
function neustart() { stopp(); zuruecksetzen(); window.scrollTo(0, 0); beobachte(); }
const kurve = t => t < .5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
function vorfuehren() {
stopp();
html.classList.add('vorfuehrung');
zuruecksetzen(); beobachte();
window.scrollTo(0, 0);
const sek = welt();
const ziel = Math.max(0, sek.getBoundingClientRect().top + scrollY - 40);
if (reduziert()) {
tick = setTimeout(() => { window.scrollTo(0, ziel); tick = setTimeout(vorfuehren, 3200); }, 900);
return;
}
const dauer = 2600; let t0 = null;
tick = setTimeout(() => {
const schritt = t => {
if (t0 === null) t0 = t;
const p = Math.min(1, (t - t0) / dauer);
window.scrollTo(0, ziel * kurve(p));
if (p < 1) uhr = requestAnimationFrame(schritt);
else tick = setTimeout(vorfuehren, 2600);
};
uhr = requestAnimationFrame(schritt);
}, 700);
}
function pause() { cancelAnimationFrame(uhr); clearTimeout(tick); uhr = tick = null; }
function weiter() { vorfuehren(); }
setzeWeg(weg);
setze(v, false);
})();
</script>
</body>
</html>