| .. | ||
| .opencode | ||
| dist | ||
| docs | ||
| .gitignore | ||
| AGENTS.md | ||
| base.css | ||
| build.py | ||
| CHANGELOG.md | ||
| components.css | ||
| demo.css | ||
| demo.js | ||
| fonts.css | ||
| framework.js | ||
| index.html | ||
| main.css | ||
| README.md | ||
| REPO_MAP.md | ||
| theme.js | ||
| tokens.css | ||
| utilities.css | ||
CSS-Framework — CSS Framework für Jinja2 / HTMX Webapps
Ein schlankes CSS-Framework für konsistente, moderne Webapplikationen. Classless wo möglich, opt-in Klassen wo nötig. Gebaut für Jinja2-Templates und HTMX-Interaktionen.
Dateien & Struktur
framework/
├── tokens.css — Design Tokens (CSS Custom Properties)
├── base.css — Classless Reset + semantische HTML-Elemente
├── components.css — Opt-in Komponenten-Klassen
├── utilities.css — Minimale Helfer-Klassen
├── main.css — Entry Point (importiert alle vier)
└── fonts.css — Template für lokale Google Fonts (optional)
Einbinden in Projekten:
<link rel="stylesheet" href="/static/css/main.css">
Design Tokens (tokens.css)
Alle Designentscheidungen sind als CSS Custom Properties definiert. Zentrale Anlaufstelle für alle Anpassungen.
Farbsystem
Zweischichtig aufgebaut: Rohe Palette + Semantische Aliases.
/* Nie direkt verwenden — nur als Rohwerte */
--color-accent-500: #6366f1;
/* Diese verwenden — semantische Bedeutung */
--accent: var(--color-accent-500);
--accent-hover: var(--color-accent-600);
--accent-subtle: var(--color-accent-50);
Primärfarbe anpassen: Nur --color-accent-* in tokens.css ändern — alle Komponenten folgen automatisch.
| Token | Verwendung |
|---|---|
--bg, --bg-subtle, --bg-muted |
Seitenhintergründe |
--surface, --surface-raised, --surface-overlay |
Karten, Modals, Dropdowns |
--border, --border-strong, --border-focus |
Rahmenlinien |
--text, --text-muted, --text-subtle |
Texthierarchie |
--accent, --accent-hover, --accent-active |
Primärfarbe + Zustände |
--color-success/warning/danger/info |
Semantische Status-Farben |
Dark Mode
Nur semantische Aliases werden überschrieben — Rohpalette bleibt unverändert.
<html data-theme="light"> <!-- oder "dark" -->
[data-theme="dark"] {
--bg: var(--color-black);
--surface: #161920;
--text: var(--color-gray-100);
/* ... alle Aliases werden überschrieben -->
}
Umschalten und Speichern übernimmt theme.js (siehe „JavaScript API → Theme“), ohne Inline-Skript.
Spacing
4px-Grid, 12 Stufen: --space-1 (4px) bis --space-24 (96px).
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
Typografie
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
--text-xs: 0.75rem; /* 12px */
--text-sm: 0.875rem; /* 14px */
--text-base: 1rem; /* 16px */
--text-lg: 1.25rem; /* 20px */
--text-xl: 1.5rem; /* 24px */
--text-2xl: 1.875rem; /* 30px */
--text-3xl: 2.25rem; /* 36px */
--text-4xl: 3rem; /* 48px */
Weitere Token-Gruppen
| Gruppe | Tokens |
|---|---|
| Border Radius | --radius-sm/md/lg/xl/2xl/full |
| Schatten | --shadow-xs/sm/md/lg/xl, --shadow-focus |
| Übergänge | --duration-fast/base/slow, --ease/ease-out/ease-in, --transition |
| Layout | --container-sm/md/lg/xl/2xl, --nav-height, --sidebar-width |
| Z-Index | --z-base/raised/dropdown/sticky/overlay/modal/toast |
Classless Base (base.css)
Semantisches HTML benötigt keine Klassen für grundlegendes Styling. Einfach valides HTML schreiben:
<!-- Alles automatisch gestylt: -->
<h1>Überschrift</h1>
<p>Text mit <strong>fett</strong>, <em>kursiv</em>, <a href="#">Link</a>, <code>Code</code>.</p>
<blockquote>Zitat<cite>— Autor</cite></blockquote>
<table>...</table>
<input type="text" placeholder="...">
<select>...</select>
<details><summary>Akkordeon</summary>Inhalt</details>
Gestylt ohne Klassen: h1–h6, p, a, strong, em, small, mark, kbd, code, pre, blockquote, hr, ul, ol, dl, table, thead, tbody, tfoot, th, td, input, textarea, select, fieldset, legend, label, details, summary, figure, figcaption, img.
Checkboxen und Radios behalten die native Darstellung des Browsers und werden über accent-color in --accent eingefärbt (hell wie dunkel).
Komponenten (components.css)
Opt-in Klassen für alles was semantisches HTML nicht abdeckt.
Layout
<!-- Container -->
<div class="container"> <!-- max-width: 1280px, zentriert -->
<div class="container container-md"> <!-- max-width: 768px -->
<!-- Vertikaler Stack -->
<div class="stack"> <!-- flex-direction: column, gap: 1rem -->
<div class="stack stack-sm"> <!-- gap: 0.5rem -->
<div class="stack stack-lg"> <!-- gap: 2rem -->
<!-- Horizontaler Cluster (umbrechend) -->
<div class="cluster"> <!-- flex-wrap, gap: 0.75rem, align-items: center -->
<!-- Grid -->
<div class="grid"> <!-- auto-fill, min 20rem pro Spalte -->
<div class="grid grid-2"> <!-- 2 Spalten -->
<div class="grid grid-3"> <!-- 3 Spalten -->
<div class="grid grid-4"> <!-- 4 Spalten -->
<!-- Sidebar-Layout -->
<div class="with-sidebar"> <!-- sidebar-width + 1fr -->
Navigation
Breadcrumbs (.breadcrumbs)
<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol>
<li><a href="/dashboard">Dashboard</a></li>
<li><a href="/projekte">Projekte</a></li>
<li><a href="/projekte/acme">Acme Redesign</a></li>
<li><span aria-current="page">Einstellungen</span></li>
</ol>
</nav>
Semantischer Navigationspfad für hierarchische Seiten. Nutzt vorhandene Text-, Radius- und Surface-Tokens, trennt Einträge per Slash und bleibt auf schmalen Viewports horizontal scrollbar.
Top Navigation (.topnav)
<nav class="topnav">
<div class="topnav-inner">
<a class="topnav-brand" href="/">Brand</a>
<ul class="topnav-links">
<li><a href="/" class="active">Dashboard</a></li>
<li>
<button data-action="nav-dropdown">
Mehr <svg class="nav-chevron">...</svg>
</button>
<div class="topnav-dropdown">
<a href="/x">Link</a>
</div>
</li>
</ul>
<div class="topnav-end">
<!-- Buttons, User-Dropdown, Theme-Toggle -->
</div>
<button class="topnav-burger" ...>☰</button>
</div>
<div class="topnav-mobile">...</div>
</nav>
Modifier: .topnav-static (nicht sticky)
Sidebar (.sidebar)
<aside class="sidebar">
<div class="sidebar-header">...</div>
<nav class="sidebar-nav">
<div class="sidebar-section">Abschnitt</div>
<a href="/" class="active">
<svg>...</svg>
Dashboard
<span class="nav-count">4</span>
</a>
<details class="sidebar-group">
<summary><svg>...</svg> Gruppe</summary>
<div class="sidebar-group-items">
<a href="/x">Unterlink</a>
</div>
</details>
</nav>
<div class="sidebar-footer">
<a href="/profil" class="sidebar-user">
<div class="avatar avatar-sm">AM</div>
<div class="sidebar-user-info">
<div class="sidebar-user-name">Anna Müller</div>
<div class="sidebar-user-role">Admin</div>
</div>
</a>
</div>
</aside>
Mobile Sidebar-Overlay (< 64rem):
Bei schmalen Viewports bricht .with-sidebar das Grid um. Die Sidebar wird dabei per JavaScript als festes Overlay eingeblendet. Benötigte HTML-Elemente:
<!-- Direkt vor .with-sidebar -->
<div class="mobile-topbar">
<button id="sidebar-toggle" aria-label="Menü öffnen">☰</button>
<span>Meine App</span>
</div>
<div id="sidebar-backdrop" class="sidebar-backdrop"></div>
<div class="with-sidebar">
<aside class="sidebar">...</aside>
<main>...</main>
</div>
JavaScript (im Basis-Template eintragen):
document.getElementById('sidebar-toggle')
.addEventListener('click', () => document.body.classList.add('sidebar-open'));
document.getElementById('sidebar-backdrop')
.addEventListener('click', () => document.body.classList.remove('sidebar-open'));
Footer (.footer)
<!-- Einfach (eine Zeile) -->
<footer class="footer">
<div class="footer-inner">
<a class="footer-brand" href="/">Brand</a>
<ul class="footer-links">
<li><a href="/impressum">Impressum</a></li>
</ul>
<p class="footer-copy">© 2025</p>
</div>
</footer>
<!-- Mehrspaltig -->
<footer class="footer footer-full">
<div class="footer-inner">
<div class="footer-col">
<p class="footer-col-title">Produkt</p>
<a href="/features">Features</a>
</div>
...
</div>
<div class="footer-bottom">
<span>© 2025 Acme GmbH</span>
<ul class="footer-social">
<li><a href="https://github.com/..."><svg>...</svg></a></li>
</ul>
</div>
</footer>
Buttons (.btn)
<!-- Varianten -->
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-ghost">Ghost</button>
<button class="btn btn-danger">Danger</button>
<button class="btn btn-success">Success</button>
<!-- Größen -->
<button class="btn btn-primary btn-xs">XSmall</button>
<button class="btn btn-primary btn-sm">Small</button>
<button class="btn btn-primary btn-lg">Large</button>
<button class="btn btn-primary btn-xl">XLarge</button>
<!-- Modifiers -->
<button class="btn btn-primary btn-full">Volle Breite</button>
<button class="btn btn-ghost btn-icon"><svg>...</svg></button>
<!-- Als Link -->
<a href="/ziel" class="btn btn-primary">Link-Button</a>
<!-- Disabled -->
<button class="btn btn-primary" disabled>Gesperrt</button>
<!-- Verbundene Button-Leiste -->
<div class="button-group" role="group" aria-label="Ansicht wechseln">
<button class="btn btn-secondary">Liste</button>
<button class="btn btn-secondary">Board</button>
<button class="btn btn-secondary">Kalender</button>
</div>
<!-- Segmented Control auf Radio-Basis -->
<div class="segmented" role="radiogroup" aria-label="Zeitraum wählen">
<label class="segmented-option" for="range-day">
<input class="segmented-input" type="radio" name="range" id="range-day">
<span class="segmented-button">Tag</span>
</label>
<label class="segmented-option" for="range-week">
<input class="segmented-input" type="radio" name="range" id="range-week" checked>
<span class="segmented-button">Woche</span>
</label>
<label class="segmented-option" for="range-month">
<input class="segmented-input" type="radio" name="range" id="range-month">
<span class="segmented-button">Monat</span>
</label>
</div>
Die Button Group fasst normale .btn-Elemente bündig zusammen. Die Segmented Control bleibt technisch eine Gruppe nativer Radio-Inputs und eignet sich für Filter, Ansichten und Moduswechsel ohne JavaScript.
Badges (.badge)
<span class="badge badge-default">Standard</span>
<span class="badge badge-primary">Primär</span>
<span class="badge badge-success">Erfolg</span>
<span class="badge badge-warning">Warnung</span>
<span class="badge badge-danger">Fehler</span>
<span class="badge badge-info">Info</span>
<span class="badge badge-outline">Outline</span>
<!-- Mit Dot-Indikator -->
<span class="badge badge-success badge-dot">Live</span>
Cards (.card)
<div class="card">
<div class="card-header">
<h3>Titel</h3>
<span class="badge badge-success">Aktiv</span>
</div>
<div class="card-body">
Inhalt
</div>
<div class="card-footer">
<button class="btn btn-primary btn-sm">Aktion</button>
</div>
</div>
Varianten: .card-flat, .card-raised, .card-ghost
Hover-Effekt: .card.card-hover
Image Card:
<div class="card">
<div class="card-media">
<img src="..." alt="...">
</div>
<div class="card-body">...</div>
</div>
Formulare
<!-- Opt-in Gruppen-Klassen -->
<div class="form-group">
<label for="email">E-Mail</label>
<input type="email" id="email" name="email">
<span class="form-hint">Wird für Login genutzt.</span>
</div>
<!-- Fehler-State -->
<div class="form-group">
<label for="pwd">Passwort</label>
<input type="password" id="pwd" class="error">
<span class="form-error">Mindestens 8 Zeichen.</span>
</div>
<!-- Mehrere Felder in einer Zeile -->
<div class="form-row">
<div class="form-group">...</div>
<div class="form-group">...</div>
</div>
<!-- Input mit Prefix/Suffix -->
<div class="input-group">
<span class="input-group-prefix">https://</span>
<input type="text" placeholder="beispiel.de">
</div>
<!-- File Upload mit Hint -->
<div class="form-group">
<label for="contract-upload">Vertragsdokument</label>
<label class="file-upload" for="contract-upload">
<input class="file-upload-input" type="file" id="contract-upload">
<span class="file-upload-head">
<span class="file-upload-body">
<span class="file-upload-title">PDF oder DOCX hochladen</span>
<span class="file-upload-description">Für Angebote, Verträge oder Briefings.</span>
</span>
<span class="file-upload-button">Datei wählen</span>
</span>
<span class="file-upload-meta" aria-hidden="true">
<span>Max. 10 MB</span>
<span>PDF, DOCX</span>
</span>
</label>
<span class="form-hint">Die Datei wird beim Absenden des Formulars normal übertragen.</span>
</div>
<!-- Dropzone-Optik mit Fehler-State -->
<div class="form-group">
<label for="asset-upload">Brand Assets</label>
<label class="file-upload file-upload-dropzone is-error" for="asset-upload">
<input class="file-upload-input" type="file" id="asset-upload">
<span class="file-upload-head">
<span class="file-upload-body">
<span class="file-upload-title">Datei hier ablegen oder auswählen</span>
<span class="file-upload-description">Optionaler Dropzone-Look ohne Preview-Logik.</span>
</span>
<span class="file-upload-button">Asset auswählen</span>
</span>
</label>
<span class="form-error">Bitte nur Logos im SVG- oder PNG-Format verwenden.</span>
</div>
<!-- Switch auf nativer Checkbox-Basis -->
<label class="switch" for="notify-comments">
<input class="switch-input" type="checkbox" id="notify-comments" checked>
<span class="switch-control" aria-hidden="true"></span>
<span class="switch-body">
<span class="switch-label">Kommentar-Benachrichtigungen</span>
<span class="switch-description">Informiert bei Antworten und Erwähnungen.</span>
</span>
</label>
Die Formular-Helfer decken damit einfache Gruppen, Hinweise, Fehlerstates, Switches und auch ein natives File-Upload-Pattern ab. .file-upload kapselt den klickbaren Bereich, .file-upload-dropzone schaltet auf eine gestrichelte Dropzone-Optik um und .is-error markiert visuell fehlerhafte Upload-Felder, ohne dass dafür JavaScript nötig ist.
Accordion
<div class="accordion">
<details class="accordion-item" open>
<summary>Was ist ein classless Framework?</summary>
<div class="accordion-body">
<p>Ein classless Framework stylt HTML-Elemente direkt.</p>
<p>Die Accordion-Komponente organisiert mehrere details-Elemente als zusammengehörige Gruppe.</p>
</div>
</details>
<details class="accordion-item">
<summary>Wie passe ich Farben an?</summary>
<div class="accordion-body">
<p>Farben und semantische Aliases werden in tokens.css gepflegt.</p>
</div>
</details>
</div>
<!-- Varianten -->
<div class="accordion accordion-flush">...</div>
<div class="accordion accordion-muted">...</div>
Das Pattern baut bewusst auf nativen details- und summary-Elementen auf. .accordion organisiert Abstände und Gruppierung, .accordion-body kapselt den Inhaltsbereich, und die Varianten .accordion-flush sowie .accordion-muted decken kompaktere oder weichere Darstellungen ab.
Sortierbare Tabellen
<table class="table-sortable">
<thead>
<tr>
<th data-action="sort" data-sort="">Name</th>
<th data-action="sort" data-sort="">Datum</th>
<th>Status</th> <!-- nicht sortierbar: kein data-action/data-sort -->
</tr>
</thead>
<tbody>...</tbody>
</table>
Erkennt automatisch numerische vs. alphabetische Werte. Deutsche Sortierung via localeCompare.
Tabellen
<!-- Classless (kein Wrapper nötig) -->
<table>
<thead><tr><th>Name</th><th>Status</th></tr></thead>
<tbody><tr><td>Anna</td><td>Aktiv</td></tr></tbody>
</table>
<!-- Mit Rahmen und Scroll -->
<div class="table-wrap">
<table>...</table>
</div>
<!-- Modifier -->
<table class="table-striped">...</table>
<table class="table-compact">...</table>
<!-- Sticky Header -->
<div class="table-wrap is-scroll">
<table class="table-sticky">...</table>
</div>
<!-- Sticky erste Spalte -->
<div class="table-wrap">
<table class="table-sticky-col">...</table>
</div>
<!-- Mobile Key-Value-Stacking (data-label auf jedem td erforderlich) -->
<table class="table-stack">
<thead><tr><th>Name</th><th>Status</th></tr></thead>
<tbody>
<tr>
<td data-label="Name">Anna</td>
<td data-label="Status">Aktiv</td>
</tr>
</tbody>
</table>
Alerts
<div class="alert alert-info">
<svg class="alert-icon">...</svg>
<div class="alert-content">
<p class="alert-title">Hinweis</p>
<p>Deine Daten werden gespeichert.</p>
</div>
</div>
Varianten: alert-info, alert-success, alert-warning, alert-danger
Dismissible Alert:
<div class="alert alert-success">
<div class="alert-content">
<p class="alert-title">Erfolgreich</p>
<p>Deine Änderungen wurden gespeichert.</p>
</div>
<button class="alert-dismiss" data-action="alert-dismiss" aria-label="Schließen">✕</button>
</div>
function dismissAlert(btn) {
const alert = btn.closest('.alert');
alert.classList.add('is-dismissing');
alert.addEventListener('transitionend', () => alert.remove(), { once: true });
}
Modals
<!-- Struktur -->
<div class="modal-backdrop" id="mein-modal"
data-action="modal-backdrop">
<div class="modal">
<div class="modal-header">
<h3>Titel</h3>
<button class="btn btn-ghost btn-icon btn-sm"
data-action="modal-close" data-target="mein-modal">✕</button>
</div>
<div class="modal-body">Inhalt</div>
<div class="modal-footer">
<button class="btn btn-ghost" data-action="modal-close" data-target="mein-modal">Abbrechen</button>
<button class="btn btn-primary">Bestätigen</button>
</div>
</div>
</div>
<button data-action="modal-open" data-target="mein-modal">Öffnen</button>
Größen: .modal-sm, .modal-lg, .modal-xl
Tabs
<div class="tabs">
<ul class="tab-list" role="tablist">
<li><button class="active" role="tab"
data-action="tab" data-target="panel-1">Tab 1</button></li>
<li><button role="tab"
data-action="tab" data-target="panel-2">Tab 2</button></li>
</ul>
<div id="panel-1" class="tab-panel active">Inhalt 1</div>
<div id="panel-2" class="tab-panel">Inhalt 2</div>
</div>
Dropdowns
<div class="dropdown" id="dd-1">
<button class="btn btn-secondary" data-action="dropdown" data-target="dd-1">
Aktionen
</button>
<div class="dropdown-menu">
<div class="dropdown-label">Aktionen</div>
<button class="dropdown-item">Bearbeiten</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item danger">Löschen</button>
</div>
</div>
Varianten: .dropdown-menu-left, .dropdown-menu-top, .dropdown-wide
Spezielle Item-Typen:
<!-- Mit Beschreibung -->
<button class="dropdown-item dropdown-item-rich">
<div class="dropdown-item-title">Projekt</div>
<div class="dropdown-item-desc">Neue Projektmappe erstellen</div>
</button>
<!-- Checkmark (Filter) -->
<button class="dropdown-item dropdown-item-check checked"
data-action="toggle-class" data-class="checked">
Aktiv
</button>
<!-- User-Header -->
<div class="dropdown-header">
<div class="avatar">AM</div>
<div>
<div class="dropdown-header-name">Anna Müller</div>
<div class="dropdown-header-sub">anna@beispiel.de</div>
</div>
</div>
Pagination
<ul class="pagination">
<li class="disabled"><a>←</a></li>
<li><a class="active" href="?page=1">1</a></li>
<li><a href="?page=2">2</a></li>
<li><a href="?page=3">3</a></li>
<li><a href="?page=2">→</a></li>
</ul>
Feedback & Status
<!-- Spinner -->
<div class="spinner"></div>
<div class="spinner spinner-sm"></div>
<div class="spinner spinner-lg"></div>
<!-- Fortschrittsbalken -->
<div class="progress">
<div class="progress-bar" data-progress="75"></div>
</div>
<div class="progress">
<div class="progress-bar success" data-progress="100"></div>
</div>
<!-- Skeleton (Lade-Platzhalter) -->
<div class="skeleton skeleton-line is-short"></div>
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-block"></div>
<!-- KPI / Stat -->
<div class="stat">
<span class="stat-label">Umsatz</span>
<span class="stat-value">€ 48.291</span>
<span class="stat-change up">↑ +12.4%</span>
</div>
<!-- Stat mit trend-basierter Einfärbung des Hauptwerts -->
<!-- data-trend="positive" → grün, data-trend="negative" → rot -->
<div class="stat" data-trend="positive">
<span class="stat-label">Saldo</span>
<span class="stat-value">+ € 1.240</span>
</div>
<div class="stat" data-trend="negative">
<span class="stat-label">Saldo</span>
<span class="stat-value">− € 320</span>
</div>
<!-- Avatar -->
<div class="avatar">AM</div>
<div class="avatar avatar-sm">AM</div>
<div class="avatar avatar-lg"><img src="..."></div>
<!-- Avatar Group -->
<div class="avatar-group">
<div class="avatar avatar-sm">AM</div>
<div class="avatar avatar-sm">BS</div>
</div>
<!-- Empty State -->
<div class="empty">
<svg class="empty-icon">...</svg>
<p class="empty-title">Keine Einträge</p>
<p class="empty-description">Erstelle deinen ersten Eintrag.</p>
<button class="btn btn-primary btn-sm">Erstellen</button>
</div>
<!-- Toast Container (einmalig im Basis-Template, direkt nach <body>) -->
<div class="toast-container" id="toast-container"></div>
<!-- Divider mit Label -->
<div class="divider">oder</div>
Tooltip
<!-- Standard: oben -->
<button data-tooltip="Erklärungstext">Hover mich</button>
<!-- Positionsvarianten -->
<button data-tooltip="Unten" data-tooltip-pos="bottom">...</button>
<button data-tooltip="Links" data-tooltip-pos="left">...</button>
<button data-tooltip="Rechts" data-tooltip-pos="right">...</button>
CSS-only via data-tooltip-Attribut. Kein JavaScript nötig. Funktioniert auf jedem Element, reagiert auch auf :focus-visible.
Meta List / Definition List (.meta-list)
<dl class="meta-list">
<div class="meta-item">
<dt class="meta-label">Status</dt>
<dd class="meta-value"><span class="badge badge-success">Aktiv</span></dd>
</div>
<div class="meta-item">
<dt class="meta-label">Erstellt am</dt>
<dd class="meta-value">12. Januar 2025</dd>
</div>
</dl>
Varianten: .meta-list-stacked (Label über Wert), .meta-list-compact, .meta-list-flush (kein oberer Border)
Filter Chips (.chip-group)
<!-- Mehrfachauswahl (Checkbox) -->
<div class="chip-group">
<label class="chip">
<input class="chip-input" type="checkbox" checked>
<span class="chip-label">Design</span>
</label>
<label class="chip">
<input class="chip-input" type="checkbox">
<span class="chip-label">Entwicklung</span>
</label>
</div>
<!-- Einzelauswahl (Radio) -->
<div class="chip-group">
<label class="chip">
<input class="chip-input" type="radio" name="filter" checked>
<span class="chip-label">Alle</span>
</label>
<label class="chip">
<input class="chip-input" type="radio" name="filter">
<span class="chip-label">Aktiv</span>
</label>
</div>
<!-- Klein -->
<div class="chip-group chip-sm">...</div>
Timeline / Activity Feed (.timeline)
<div class="timeline">
<div class="timeline-item timeline-success">
<div class="timeline-marker">
<div class="timeline-dot"></div>
<div class="timeline-connector"></div>
</div>
<div class="timeline-content">
<p class="timeline-title">Deployment abgeschlossen</p>
<p class="timeline-meta">vor 2 Stunden</p>
<p class="timeline-body">Version 2.4.1 ist live.</p>
</div>
</div>
<!-- letztes Item: .timeline-connector kann weggelassen werden -->
</div>
Dot-Varianten: timeline-accent, timeline-success, timeline-warning, timeline-danger
Stepper / Progress Steps (.stepper)
<!-- Horizontal -->
<div class="stepper">
<div class="step step-done">
<div class="step-indicator">✓</div>
<span class="step-label">Konto</span>
</div>
<div class="step step-active">
<div class="step-indicator">2</div>
<span class="step-label">Profil</span>
</div>
<div class="step">
<div class="step-indicator">3</div>
<span class="step-label">Fertig</span>
</div>
</div>
<!-- Vertikal -->
<div class="stepper stepper-vertical">
<div class="step step-done">
<div class="step-indicator">✓</div>
<div class="step-body">
<p class="step-title">Konto anlegen</p>
<p class="step-desc">E-Mail und Passwort festlegen.</p>
</div>
</div>
...
</div>
Step-Modifier: .step-done, .step-active
List Group / Action List
<!-- List Group (statisch) -->
<ul class="list-group">
<li class="list-item list-item-active">Dashboard</li>
<li class="list-item">Projekte</li>
<li class="list-item list-item-muted">Archiv</li>
</ul>
<!-- Action List (klickbar) -->
<div class="action-list">
<a href="/profil" class="action-item">
<svg class="action-item-icon">...</svg>
<div class="action-item-content">
<p class="action-item-title">Profil bearbeiten</p>
<p class="action-item-desc">Name, Avatar und Kontakt</p>
</div>
<svg class="action-item-end">›</svg>
</a>
<button class="action-item action-item-danger">Konto löschen</button>
</div>
Varianten: .list-group-compact, .action-list-compact, .list-group-flush, .action-list-flush
Utilities (utilities.css)
Minimale Helfer — bewusst schlank gehalten.
<!-- Filter-Leiste: Flex-Row, auf Mobile gestapelt (< 40rem) -->
<div class="filter-bar">
<div class="form-group"><label>Monat</label><input type="month"></div>
<div class="form-group"><label>Typ</label><select>...</select></div>
<button class="btn btn-secondary btn-sm">Filtern</button>
</div>
<!-- Flex -->
<div class="flex-center"> <!-- align + justify: center -->
<div class="flex-between"> <!-- space-between -->
<div class="flex-1"> <!-- flex: 1 -->
<!-- Alignment (standalone, kombinierbar mit .flex / .grid-u) -->
<div class="flex items-center justify-between">...</div>
<div class="flex flex-col items-center">...</div>
<!-- align-items -->
<div class="items-start"> <!-- flex-start -->
<div class="items-center">
<div class="items-end"> <!-- flex-end -->
<div class="items-stretch">
<div class="items-baseline">
<!-- justify-content -->
<div class="justify-start">
<div class="justify-center">
<div class="justify-end">
<div class="justify-between">
<!-- align-self (auf Kind-Elementen) -->
<div class="self-start">
<div class="self-center">
<div class="self-end">
<div class="self-stretch">
<!-- Text -->
<p class="text-sm text-muted">Kleiner Hilfstext</p>
<span class="font-semibold truncate">Langer Text wird abgeschnitten</span>
<p class="text-accent">Akzentfarbe</p>
<p class="text-success/warning/danger">Status-Farben</p>
<!-- Spacing -->
<div class="mt-4 mb-8 px-6">...</div>
<div class="ml-auto">Schiebt nach rechts</div>
<!-- Verschiedenes -->
<div class="rounded-xl shadow-md border">...</div>
<div class="bg-subtle overflow-x-auto">...</div>
<span class="sr-only">Nur für Screenreader</span>
JavaScript API
framework.js bietet jede Funktion auf zwei Wegen an:
- Deklarativ über
data-action(empfohlen). Ein einziger Listener amdocumentwertet die Attribute aus. Das funktioniert unter einer strengen Content Security Policy und auch für Inhalte, die HTMX später nachlädt. - Als globale Funktion, z. B.
openModal('m'), für eigene Skripte. Inline-Handler wieonclick="…"gehen zwar weiterhin, verlangen aber'unsafe-inline'in der CSP.
<button class="btn" data-action="modal-open" data-target="mein-modal">Öffnen</button>
Aktionen
data-action |
Weitere Attribute | Wirkung | Entspricht |
|---|---|---|---|
modal-open |
data-target="id" |
Modal öffnen, Seite nicht mehr scrollen | openModal(id) |
modal-close |
data-target="id" (optional, sonst umgebendes Modal) |
Modal schließen | closeModal(id) |
modal-backdrop |
auf .modal-backdrop mit id |
Klick auf den Hintergrund schließt | closeOnBackdrop(e, id) |
tab |
data-target="panel-id" |
Tab wechseln | switchTab(btn, id) |
dropdown |
data-target="id" (optional, sonst umgebendes .dropdown) |
Dropdown öffnen/schließen | toggleDropdown(id) |
nav-dropdown |
– | Nav-Dropdown öffnen/schließen | toggleNavDd(btn) |
sidebar-open / sidebar-close |
– | Mobile Sidebar ein-/ausblenden | toggleSidebar() / closeSidebar() |
alert-dismiss |
– | Alert mit Transition entfernen | dismissAlert(btn) |
sort |
auf th, mit data-sort="" |
Spalte sortieren | sortTable(th) |
toggle-class |
data-class="checked" |
Klasse am Element umschalten | – |
toast |
data-message="…", data-type="success" |
Toast einblenden | showToast(msg, type) |
toast-close |
– | Toast schließen (setzt showToast selbst) |
removeToast(id) |
theme-toggle |
– | Hell/Dunkel umschalten (theme.js) |
– |
Funktionsübersicht
| Funktion | Beschreibung |
|---|---|
openModal(id) |
Modal öffnen, Scroll sperren (body.modal-open) |
closeModal(id) |
Modal schließen |
closeOnBackdrop(e, id) |
Modal schließen, wenn der Klick auf dem Hintergrund lag |
switchTab(btn, panelId) |
Tab wechseln |
toggleDropdown(id) |
Dropdown öffnen/schließen |
toggleNavDd(btn) |
Nav-Dropdown öffnen/schließen |
showToast(message, type) |
Toast einblenden (type: 'success', 'warning', 'danger', 'info'). Die Meldung wird als Text gesetzt, HTML darin nicht ausgeführt |
removeToast(id) |
Toast ausblenden und entfernen |
dismissAlert(btn) |
Alert mit Transition entfernen |
sortTable(th) |
Tabellenspalte auf-/absteigend sortieren |
toggleSidebar() |
Mobile Sidebar einblenden |
closeSidebar() |
Mobile Sidebar ausblenden |
ESC schließt automatisch alle offenen Modals. Klick außerhalb schließt Dropdowns.
Theme (theme.js)
theme.js setzt Hell/Dunkel vor dem ersten Rendern (gespeicherter Wert, sonst prefers-color-scheme) und schaltet bei data-action="theme-toggle" um. Im <head> ohne defer einbinden. Die Icons wechseln per CSS: .icon-theme-light ist im hellen, .icon-theme-dark im dunklen Modus sichtbar.
<head>
<script src="/static/js/theme.js"></script>
<link rel="stylesheet" href="/static/css/main.css">
</head>
…
<button class="btn btn-ghost btn-icon" data-action="theme-toggle" aria-label="Hell/Dunkel">
<svg class="icon-theme-light">…Mond…</svg>
<svg class="icon-theme-dark">…Sonne…</svg>
</button>
Content Security Policy
Das Framework läuft unter einer strengen CSP ohne 'unsafe-inline' und ohne 'unsafe-eval':
default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:
Dafür gilt:
- Keine
on…-Attribute, sonderndata-action. - Keine
style-Attribute, sondern Klassen (Tokens, Utilities, z. B.min-w-0,skeleton-line). - Kein Inline-
<script>, sondern Dateien (framework.js,theme.js, eigene Skripte). - HTMX ohne eigene Inline-Styles:
<meta name="htmx-config" content='{"includeIndicatorStyles": false, "allowEval": false}'>. Die Styles für.htmx-indicatorliefertcomponents.css.hx-onnicht verwenden (brauchteval).
Nicht in framework.js enthalten — markup-abhängig, gehört in eine eigene Datei des Projekts:
- Scroll-basierte aktive Nav-Markierung (abhängig von der Seitenstruktur)
Fonts lokal hosten (fonts.css)
/* fonts.css — Vorlage, Pfade anpassen */
@font-face {
font-family: "Inter";
font-weight: 400;
font-display: swap;
src: url("/static/fonts/inter-regular.woff2") format("woff2");
}
Empfohlene Quelle: gwfh.mranftl.com
Aktivieren in main.css:
@import "fonts.css"; /* vor allen anderen Imports */
@import "tokens.css";
/* ... */
Projektstruktur (empfohlen)
projekt/
└── static/
└── css/
├── main.css — Entry Point
├── fonts.css — Lokale Fonts (optional)
├── tokens.css
├── base.css
├── components.css
└── utilities.css
Framework in ein bestehendes Projekt integrieren
1. CSS-Dateien kopieren
static/css/
├── main.css
├── tokens.css
├── base.css
├── components.css
└── utilities.css
Optional: fonts.css für lokal gehostete Schriften.
2. CSS und JS einbinden
<head>
<script src="/static/js/theme.js"></script>
<link rel="stylesheet" href="/static/css/main.css">
<script src="/static/js/framework.js" defer></script>
</head>
framework.js enthält alle Funktionen und den Listener für data-action (Modal, Toast, Tabs, Dropdown usw.), theme.js das Umschalten von Hell/Dunkel — kein weiterer Setup-Schritt nötig.
3. KI-Guidance mitgeben (OpenCode)
Den .opencode/-Ordner aus diesem Repo ins Consumer-Projekt kopieren:
.opencode/
├── commands/ — Prompt-Checklisten für neue Komponenten, Tokens, Sync
└── skills/
├── SKILL.md — Schnellreferenz (wird beim Skill-Aufruf geladen)
└── references/ — Vollständige Referenzen für Komponenten, Tokens, Jinja2
Außerdem AGENTS.md ins Projektstamm-Verzeichnis kopieren — sie enthält die verbindlichen Arbeitsregeln für KI-Agenten.
4. Basis-Template einrichten
Das fertige base.html für FastAPI + Jinja2 + HTMX liegt in .opencode/skills/references/jinja2.md. Es enthält:
- Theme-Toggle über
theme.js(CSP-fest, ohne Inline-Skript) - Sidebar-Layout mit Mobile-Overlay
- Alle Aktionen über
data-action(CSP-fest) - HTMX-Einbindung
5. Version prüfen
Die aktuelle Framework-Version steht im Kommentar von main.css. Beim Aktualisieren des Frameworks einfach die CSS-Dateien ersetzen — keine Build-Schritte nötig.