Skip to main content

Devlog · Phase 0 · 6/7

A Catppuccin, Glass-First Design System

Twenty-six Catppuccin tokens per flavor, glassmorphism at 62% transparency, and three self-hosted type families: the skin of aubia.dev, from CSS token to reduced-motion.

Published on 17 August 20265 min read
  • design-system
  • catppuccin
  • react

Once the backend was filed into modules, aubia.dev needed a face. I wanted a tight identity, dark by default, that breathes care without shouting. The result comes down to four choices: a Catppuccin palette, a theming that sets itself, a visual language in glass, and measured motion. The real code from the repo backs each of them.

Catppuccin: Four Flavors, Two by Default

The palette comes from Catppuccin, a color family well known in the dev ecosystem. Aubia bundles four flavors, but the landing exposes only two: Mocha in dark, Latte in light. Each flavor's twenty-six tokens are declared as CSS variables in the @theme block of resources/css/app.css. Tailwind v4 is configured entirely in CSS here, with no tailwind.config.js file.

@theme {
    --color-ctp-base: #1e1e2e;
    --color-ctp-mantle: #181825;
    --color-ctp-text: #cdd6f4;
    --color-ctp-mauve: #cba6f7;
    /* ... vingt-six tokens pour Mocha */
}

Mauve is my primary accent. The two other flavors, Frappé and Macchiato, are bundled as dark variants for Phase 1, but stay out of the public switcher as long as the landing keeps its tight identity. The key point: no component references a hardcoded hex value. A class like bg-ctp-mauve changes value with the active flavor, without the JSX knowing anything about it.

The Theming Sets Itself

The theme choice today follows a simple rule: dark by default. In April, the landing started from the opposite, the system first, reading prefers-color-scheme on mount to apply Mocha or Latte according to the OS setting. I flipped that default to dark in June. Now a visitor arrives in Mocha no matter what, and only follows their OS setting if they explicitly pick the "system" option in the switcher. The active flavor is set on <html data-theme="...">, and Latte's values override Mocha's through that attribute. Any explicit choice, light, dark, or system, is persisted in localStorage and takes precedence over the default on the next return.

This switch isn't cosmetic. Aubia's target is developers who live in dark mode: better to welcome them into their natural environment than to hit them with a white flash when the page opens.

Glass-First: Transparency as a Stance

The landing's visual language rests on glassmorphism cards: semi-transparent background, thin border, backdrop blur. That's what lets the page's gradient show through each surface and gives that muffled sense of depth. Rather than repeating the same stack of classes on each card, the recipe lives in a Tailwind v4 utility declared in CSS:

@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);
}

The detail that makes or breaks the effect is transparency. An opaque background cancels the backdrop-filter: without the 62% in the color-mix, the blur has nothing left to blur and the card becomes a plain rectangle. The saturate and the brightness give the glass back a bit of life that the blur alone would snuff out, and the shadow is inset, a simple thread of light on the top edge. The discipline is strict: translucent background, barely marked border, never a heavy drop shadow that would break the illusion.

Three Self-Hosted Type Families

The typography rests on three families, all hosted locally: Geist for body and interface, Geist Mono for code and file paths, Chakra Petch for the large display headings.

No network call to a third-party foundry: the @font-face declarations are generated at build by Laravel's font plugin, from the installed packages. In June, I moved the loading to the @fonts directive, which injects these declarations into the <head> before the main stylesheet.

Hosting locally goes beyond performance: calling a third-party foundry would leak every visitor's IP to a subprocessor outside the EU, which I refuse for a website that means to be beyond reproach on privacy. I put this trio together on purpose. The Aubia wordmark demands Chakra Petch, and keeping an Inter for the body would have created a permanent divergence with that heading. Geist and Geist Mono come from the same family, their vertical alignment between prose and code is native.

The @theme block stays the source of truth for the three font stacks:

--font-display: 'Chakra Petch', 'Chakra Petch fallback', system-ui, sans-serif;
--font-sans: 'Geist', 'Geist fallback', system-ui, sans-serif;
--font-mono: 'Geist Mono', 'Geist Mono fallback', ui-monospace, monospace;

The fallback font that follows each family neutralizes the layout shift at swap time. Only Chakra Petch is preloaded, because it carries the main heading and weighs on perceived rendering.

Motion, With a Safety Catch

The animations go through Motion. I centralized the reusable variants in a single file, motion-presets.ts, rather than redefining them in each component. The standard appearance of sections is a rising fade, in the signature Luma easing:

export const fadeUpSpring: Variants = {
    hidden: { opacity: 0, y: 24 },
    visible: {
        opacity: 1,
        y: 0,
        transition: { duration: 0.6, ease: EASE_LUMA },
    },
};

The safety catch is accessibility. A visitor who has asked for less motion at the system level must not be subjected to fades. Motion respects prefers-reduced-motion natively, and I normalize its value with a useSafeReducedMotion() hook that always returns a boolean, even during server rendering where the media query isn't evaluated yet. The animation budget is deliberately tight: rarely more than two animated elements at a time in the visible field, to rank attention instead of scattering it.

These visual choices serve a product conviction I detail in the landing's Vision section. The last article in the series looks at what the engines themselves see of all this: multilingual SEO and extraction by AI answer engines. If Aubia's spirit speaks to you, you can join the waitlist.

This build is chronicled here, article after article. Come aboard: your feedback will shape what comes next.

Join the waitlist