Zum Hauptinhalt springen

Devlog · Phase 0 · 6/7

Catppuccin und Milchglas: das Design-System von aubia.dev

Zwei Catppuccin-Themes, durchscheinende Flächen und drei Schriften prägen aubia.dev. Gemeinsame Einstellungen sorgen für Konsistenz, mit Grenzen bei Unschärfe und Bewegung.

Veröffentlicht am 17. August 2026Aktualisiert am 27. September 20269 Min. Lesezeit
  • design-system
  • catppuccin
  • tailwind
  • motion

Der dunkle Hintergrund von aubia.dev ist nicht schwarz. Er ist blaugrau, #1e1e2e, überlagert von vier radialen Verläufen, die Farbflächen in Mauve, Lavendel, Pfirsich und Blau bilden. Diese Farbflächen bleiben beim Scrollen an derselben Stelle. Durch den durchscheinenden Hintergrund der Karten und der Navigationsleiste bleiben ihre Farben sichtbar.

Ich habe Catppuccin für die Themes der Website gewählt. Mauve erscheint in den Artikellinks und den Umrandungen für den Tastaturfokus; Flächen, Überschriften und Animationen haben eigene gemeinsame Einstellungen. Eine Komponente kann eine Karte oder eine Einblendanimation wiederverwenden, ohne jede Farbe und jede Dauer neu festzulegen.

Mocha und Latte, dieselben Namen für zwei Themes

Die offizielle Catppuccin-Palette umfasst vier Themes. Die Website nutzt zwei davon: Mocha, das dunkelste, und Latte, das helle. Jedes stellt sechsundzwanzig Farben mit denselben Namen bereit. text ist die Textfarbe, base der Haupthintergrund; mauve und peach sind Farbtöne, deren Verwendung von der Oberfläche abhängt.

Diese Farben werden zu CSS-Variablen mit dem Präfix --color-ctp-. Durch ihre Deklaration im @theme-Block von Tailwind stehen Klassen wie text-ctp-mauve und bg-ctp-base zur Verfügung. Die Klasse in der Komponente bleibt beim Theme-Wechsel gleich. Ihre Variable wechselt beispielsweise vom hellen Mauve aus Mocha, #cba6f7, zum kräftigeren Violett aus Latte, #8839ef.

Die sechsundzwanzig Farben beschreiben nicht die gesamte Website. Glas braucht eine Hintergrundfarbe, eine Umrandung und Lichtreflexe; die Verläufe der Überschriften haben eigene Farbstopps. Diese Einstellungen ergänzen die Palette im CSS. Die Akzente behalten in beiden Themes ihren ursprünglichen Catppuccin-Farbton, auch in den Überschriften und in der Phiki-Hervorhebung des Blogs. Auf hellen Hintergründen liegen einige unter der Kontrastschwelle WCAG AA; die Website nimmt diese gestalterische Entscheidung bewusst in Kauf.

Für das helle Theme reicht es nicht, Text und Hintergrund zu vertauschen. Im dunklen Modus verwenden die Karten das tiefe Grau mantle mit weißen Umrandungen geringer Deckkraft. Im hellen Modus verwenden sie das fast weiße base mit einer dunklen Umrandung. Die äußeren Schatten der Glasflächen, in Mocha unsichtbar, werden in Latte blaugrau. In einem Theme trennt Licht die Flächen voneinander, im anderen ein Schatten.

Das Theme vor der Seitenanzeige wählen

Beim ersten Besuch mit aktiviertem JavaScript zeigt die Website Mocha an, auch wenn das System den hellen Modus bevorzugt. Der Umschalter bietet "Hell", "Dunkel" und "System" an. Die letzte Option entfernt das Attribut data-theme vom HTML-Wurzelelement. Dadurch kann die Media Query prefers-color-scheme das System-Theme anwenden.

Ein kleines Skript im <head> liest die gespeicherte Einstellung, bevor React geladen wird. Fehlt ein gültiger Wert oder ist der Local Storage nicht zugänglich, setzt es data-theme="dark". Das CSS enthält die Werte von Latte unter [data-theme='light'] und unter der Media Query für den Systemmodus. Der Browser kann die passenden Farben wählen, ohne auf die Hydrierung der Seite zu warten.

Wenn Sie das Theme wechseln, wendet der Umschalter Ihre Auswahl auf das Dokument an und versucht, sie zu speichern. Ist der Speicher blockiert, funktioniert der Wechsel für diesen Besuch, bleibt aber möglicherweise beim nächsten Besuch nicht erhalten. Ohne JavaScript läuft das initiale Skript nicht: Das fehlende Attribut lässt das CSS der Systemeinstellung folgen.

Der Glaseffekt der Karten

Transparenz lässt den Hintergrund durchscheinen; Unschärfe schwächt seine Details ab. Bei den Karten werden diese beiden Einstellungen mit einem Rand und einem inneren Lichtreflex kombiniert:

@utility glass-card {
    background: color-mix(in oklch, var(--glass-bg-card) 62%, transparent);
    backdrop-filter: blur(40px) saturate(1.6) brightness(1.02);
    border: 1px solid color-mix(in oklch, var(--glass-stroke) 12%, transparent);
    box-shadow: inset 0 1px 0 color-mix(in oklch, var(--glass-highlight) 8%, transparent);
}

Die Mischung mit transparent ergibt hier einen Hintergrund mit 62 % Deckkraft. Er färbt das, was hinter der Karte erscheint, ohne es vollständig zu verdecken. Die Eigenschaft backdrop-filter zeichnet den Hintergrund weich, erhöht dann seine Sättigung und hebt seine Helligkeit leicht an. Der Kartentext bleibt scharf. Bei einem vollständig deckenden Hintergrund sind die dahinterliegenden Pixel weiterhin vorhanden, aber die Hintergrundfarbe verdeckt das Filterergebnis.

Der innere Lichtsaum hellt die obere Kante auf und bleibt in beiden Themes weiß mit geringer Deckkraft. Der Rand wechselt im hellen Modus unabhängig von der Unschärfe von Weiß zu Blaugrau. Er trennt die Karte von ihrem Hintergrund, ohne ihr eine leuchtende Umrandung zu geben.

Nicht jede Fläche verwendet dieselbe Mischung. Die inneren Menübuttons nutzen 58 % Hintergrundfarbe, die Navigation 70 % und die Variante glass-strong 80 %. Der Unschärferadius reicht von 40 Pixeln bei Karten bis zu 80 bei dieser dichten Variante, die das Anmeldeformular umgibt. Die Formularfläche verdeckt mehr von der Dekoration als die Menübuttons.

Das Sprachmenü behält einen deckenden Hintergrund

Zwei übereinanderliegende weichgezeichnete Flächen wenden denselben Effekt nicht zweimal auf den Seitenhintergrund an. Ein Vorfahrenelement, das bereits einen backdrop-filter hat, definiert eine Backdrop Root: eine Grenze, jenseits derer der Filter eines Nachfahren keine Pixel mehr abrufen kann. will-change: opacity kann dieselbe Grenze erzeugen. Das Kindelement erbt den Filter nicht; es ändert sich der Bereich, der seinem eigenen Filter zur Verfügung steht.

Diese Situation entsteht bei der Sprachauswahl innerhalb der Navigation. Ihr Panel verwendet deshalb einen deckenden Hintergrund in der Kartenfarbe. Die Beschriftungen sind nicht auf einen zweiten Weichzeichner angewiesen, um sich von den dahinter scrollenden Inhalten abzuheben.

Das Panel ist außerdem ein HTML-Element vom Typ <details>, dessen Links auch im geschlossenen Zustand gerendert werden. Sie sind im initialen HTML aus dem Server-Rendering enthalten. Das Öffnen funktioniert ohne JavaScript; React ergänzt das Schließen durch einen Klick außerhalb oder mit Escape.

Ein React-Portal könnte ein Panel an eine andere Stelle im Dokument verschieben, um die Einschränkungen seiner Vorfahren zu umgehen. Das Verschieben entfernt die Links nicht aus dem Dokument. Ein ausschließlich clientseitig gemountetes Panel stellt diese Links dagegen nicht in der initialen HTML-Antwort bereit. Das hier verwendete <details> erhält sowohl die serverseitig gerenderten Links als auch das native Öffnungsverhalten, ohne Portal.

Chakra Petch für Überschriften, Geist zum Lesen

Die kantigen Formen von Chakra Petch erscheinen in den Überschriften. Geist wird für den Fließtext und die Oberfläche verwendet, Geist Mono für Code. In einem Artikel wie diesem unterscheidet diese Aufteilung Zwischenüberschriften, Absätze und Codeausschnitte voneinander, ohne bei jedem Übergang einen Farbwechsel zu verlangen.

Alle drei Schriften werden selbst gehostet. Der Browser lädt sie von der Website herunter, ohne eine Anfrage an Google Fonts oder einen anderen Schriftanbieter zu senden. Die Dateien stammen aus den installierten Fontsource-Paketen; das Schriften-Plugin in laravel-vite-plugin bereitet die @font-face-Deklarationen beim Build vor. Anschließend bindet die Blade-Direktive @fonts sie in die Seite ein.

Alle drei Schriften verwenden den Provider local() direkt aus ihren Paketen. Chakra Petch lädt eine einzige lateinische WOFF2-Datei mit Schriftstärke 600 im normalen Stil, ohne zusätzliche WOFF-Variante. Geist und Geist Mono sind variable Schriften: Ihre WOFF2-Datei deckt Schriftstärken von 100 bis 900 ab.

Chakra Petch und Geist werden auf den öffentlichen Seiten vorgeladen. Geist Mono wird zusätzlich auf der Startseite und im Blog vorgeladen, aber nicht auf den rechtlichen Seiten: drei Schrift-Preloads im ersten Fall, zwei im zweiten. Der Konfigurationsausschnitt für Chakra Petch legt fest, was während des Downloads geschehen soll:

local('Chakra Petch', {
    variants: [{
        src: 'node_modules/@fontsource/chakra-petch/files/chakra-petch-latin-600-normal.woff2',
        weight: 600,
        style: 'normal',
    }],
    variable: '--font-display',
    display: 'swap',
    preload: [{ weight: 600, style: 'normal' }],
    fallbacks: ['system-ui', 'sans-serif'],
}),

Mit swap kann der Browser Text in einer Ersatzschrift anzeigen, bevor die vorgesehene Schrift eintrifft. Das Plugin passt die Metriken der Ersatzschriften von Chakra Petch und Geist an, um deren Abmessungen an die vorgesehenen Schriften anzunähern. Diese Näherungen verringern mögliche Verschiebungen beim Schriftwechsel, garantieren aber kein vollständig verschiebungsfreies Layout. Geist Mono behält seine Monospace-Ersatzschriften: Für diese Familie ist die Erzeugung angepasster Ersatzschriften deaktiviert.

Einblendanimationen und animierte Sequenzen

Die Überschriften bleiben im serverseitigen HTML sichtbar. Die H1 ist statisch; die FAQ-Überschrift verwendet die Variante text von HeadingReveal mit initial={false}. Jedes Wort bewegt sich innerhalb von 700 Millisekunden um 8 Pixel, ohne Änderung der Deckkraft oder Unschärfe. Diese Zustände sind in der Komponente definiert:

const wordVariants = {
    hidden: { opacity: 1, y: 0 },
    visible: {
        opacity: 1,
        y: [8, 0],
        transition: { duration: 0.7, ease: EASE_LUMA },
    },
};

Jedes Wort steckt in einem m.span mit inline-block, das wordVariants erhält. Der zeitliche Versatz zwischen den Wörtern beträgt standardmäßig 60 Millisekunden. Die Einblendanimation läuft einmal, sobald die Hälfte des beobachteten Elements sichtbar ist. Die gemeinsame Konstante EASE_LUMA hat den Wert [0.22, 0.61, 0.36, 1]: Diese Bézierkurve steuert Beschleunigung und Abbremsen. Die Knoten in den Illustrationen verwenden für ihre Einblendanimationen Federn mit Steifigkeit und Dämpfung.

Motion steuert nicht alles. CSS-Schleifen animieren die Illustrationen, und JavaScript-Timer lassen den Text der einleitenden Aussage Buchstabe für Buchstabe wechseln. Die Karten im Abschnitt Vision haben eine scrollgesteuerte CSS-Einblendanimation. Sie ist nur aktiv, wenn der Browser animation-timeline: view() unterstützt und der Besucher keine reduzierte Bewegung angefordert hat. Andernfalls behalten sie ihren standardmäßig sichtbaren Zustand.

Auf die Einstellung für reduzierte Bewegung reagieren

Die Systemeinstellung "Bewegung reduzieren" kann sich ändern, während eine Seite geöffnet ist. Der lokale Hook useSafeReducedMotion liest die entsprechende Media Query mit useSyncExternalStore und abonniert das change-Ereignis von matchMedia. React kann die abonnierten Komponenten dann bei einer Änderung der Einstellung erneut rendern.

Diese lokale Implementierung umgeht eine Einschränkung von Motion 13.2.0: Dessen Hook useReducedMotion initialisiert einen React-Zustand, ohne ihn zu aktualisieren, obwohl die Dokumentation eine Reaktion auf Änderungen beschreibt.

Der Server kennt die Browsereinstellung nicht. Der lokale Hook gibt daher beim Server-Rendering und bei der initialen Hydrierung false zurück und liest anschließend den tatsächlichen Wert auf dem Client. Diese Verwendung des Server-Snapshots in beiden Fällen entspricht dem Verhalten, das React für useSyncExternalStore beschreibt.

Das CSS wirkt, ohne auf diesen Lesevorgang zu warten: Die Regeln für reduzierte Bewegung stoppen den blinkenden Cursor, die Einblendanimation fade-up und die von ihnen erfassten Illustrationsschleifen. Das Konfetti hat eine eigene Schutzvorkehrung. Diese Regeln deaktivieren nicht sämtliche Übergänge der Website.

Der animierte Einleitungstext zeigt beim Server-Rendering seine letzte Aussage. Solange reduzierte Bewegung aktiv ist, behält er diesen Text bei und startet keinen Timer. Eine Änderung der Einstellung während des Besuchs beendet den laufenden Timer; nach dem Ausschalten reduzierter Bewegung kann die Sequenz weiterlaufen. Übereinanderliegende unsichtbare Texte, die für Screenreader ausgeblendet sind, reservieren die Höhe des längsten Textes. Auch der verzögert geladene Zähler hat einen reservierten Bereich unter dem Formular.

Dieses Projekt wird hier erzählt, Artikel für Artikel. Was als Nächstes kommt, hängt davon ab, was Sie dazu sagen.

Auf die Warteliste