hugin/static/css-framework
2026-10-27 08:33:21 +01:00
..
.opencode initial commit 2026-10-27 08:33:21 +01:00
dist initial commit 2026-10-27 08:33:21 +01:00
docs initial commit 2026-10-27 08:33:21 +01:00
.gitignore initial commit 2026-10-27 08:33:21 +01:00
AGENTS.md initial commit 2026-10-27 08:33:21 +01:00
base.css initial commit 2026-10-27 08:33:21 +01:00
build.py initial commit 2026-10-27 08:33:21 +01:00
CHANGELOG.md initial commit 2026-10-27 08:33:21 +01:00
components.css initial commit 2026-10-27 08:33:21 +01:00
demo.css initial commit 2026-10-27 08:33:21 +01:00
demo.js initial commit 2026-10-27 08:33:21 +01:00
fonts.css initial commit 2026-10-27 08:33:21 +01:00
framework.js initial commit 2026-10-27 08:33:21 +01:00
index.html initial commit 2026-10-27 08:33:21 +01:00
main.css initial commit 2026-10-27 08:33:21 +01:00
README.md initial commit 2026-10-27 08:33:21 +01:00
REPO_MAP.md initial commit 2026-10-27 08:33:21 +01:00
theme.js initial commit 2026-10-27 08:33:21 +01:00
tokens.css initial commit 2026-10-27 08:33:21 +01:00
utilities.css initial commit 2026-10-27 08:33:21 +01:00

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'));
<!-- 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:

  1. Deklarativ über data-action (empfohlen). Ein einziger Listener am document wertet die Attribute aus. Das funktioniert unter einer strengen Content Security Policy und auch für Inhalte, die HTMX später nachlädt.
  2. Als globale Funktion, z. B. openModal('m'), für eigene Skripte. Inline-Handler wie onclick="…" 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, sondern data-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-indicator liefert components.css. hx-on nicht verwenden (braucht eval).

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.