Aller au contenu principal

Journal de bord · Phase 0 · 6/7

Un design system Catppuccin, glass-first

Vingt-six tokens Catppuccin par saveur, un glassmorphism à 62 % de transparence et trois familles typographiques auto-hébergées : la peau d'aubia.dev, du token CSS au reduced-motion.

Publié le 17 août 20266 min de lecture
  • design-system
  • catppuccin
  • react

Une fois le backend rangé en modules, il a fallu donner un visage à aubia.dev. Je voulais une identité resserrée, sombre par défaut, qui respire le soin sans crier. Le résultat tient en quatre partis pris : une palette Catppuccin, un theming qui se règle tout seul, un langage visuel en verre, et un mouvement mesuré. Le code réel du repo appuie chacun d'eux.

Catppuccin : quatre saveurs, deux par défaut

La palette vient de Catppuccin, une famille de couleurs bien connue de l'écosystème dev. Aubia en bundle quatre saveurs, mais la landing n'en expose que deux : Mocha en sombre, Latte en clair. Les vingt-six tokens de chaque saveur sont déclarés en variables CSS dans le bloc @theme de resources/css/app.css. Tailwind v4 se configure ici entièrement en CSS, sans fichier tailwind.config.js.

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

Le mauve est mon accent primaire. Les deux autres saveurs, Frappé et Macchiato, sont bundlées comme variantes sombres pour la Phase 1, mais restent hors du switcher public tant que la landing garde son identité resserrée. Le point clé : aucun composant ne référence un hexadécimal en dur. Une classe comme bg-ctp-mauve change de valeur avec la saveur active, sans que le JSX en sache rien.

Le theming se règle tout seul

Le choix de thème suit aujourd'hui une règle simple : sombre par défaut. En avril, la landing partait de l'inverse, le système d'abord, en lisant prefers-color-scheme au montage pour appliquer Mocha ou Latte selon le réglage de l'OS. J'ai basculé ce défaut en sombre au mois de juin. Désormais, un visiteur arrive en Mocha quoi qu'il arrive, et ne suit le réglage de son OS que s'il choisit explicitement l'option « système » dans le switcher. La saveur active se pose sur <html data-theme="...">, et les valeurs de Latte écrasent celles de Mocha via cet attribut. Tout choix explicite, clair, sombre ou système, est persisté dans localStorage et prime sur le défaut au retour suivant.

Ce basculement n'est pas cosmétique. La cible d'Aubia, ce sont des développeurs qui vivent en dark mode : autant les accueillir dans leur environnement naturel plutôt que de leur infliger un flash blanc à l'ouverture de la page.

Glass-first : la transparence comme parti pris

Le langage visuel de la landing repose sur des cartes en glassmorphism : fond semi-transparent, bordure fine, flou d'arrière-plan. C'est ce qui laisse deviner le gradient de la page à travers chaque surface et donne cette impression de profondeur feutrée. Plutôt que de répéter la même pile de classes sur chaque carte, la recette vit dans une utility Tailwind v4 déclarée en 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);
}

Le détail qui fait ou défait l'effet, c'est la transparence. Un fond opaque annule le backdrop-filter : sans les 62 % du color-mix, le flou n'a plus rien à flouter et la carte devient un simple rectangle. Le saturate et le brightness redonnent au verre un peu de vie que le flou seul éteindrait, et l'ombre est intérieure, un simple filet de lumière sur l'arête haute. La discipline est stricte : fond translucide, bordure à peine marquée, jamais d'ombre portée lourde qui casserait l'illusion.

Trois familles typographiques auto-hébergées

La typographie repose sur trois familles, toutes hébergées en local : Geist pour le corps et l'interface, Geist Mono pour le code et les chemins de fichiers, Chakra Petch pour les grands titres d'affichage.

Aucun appel réseau à une fonderie tierce : les @font-face sont générés au build par le plugin de polices de Laravel, à partir des packages installés. En juin, j'ai basculé le chargement sur la directive @fonts, qui injecte ces déclarations dans le <head> avant la feuille de style principale.

Héberger localement dépasse la performance : appeler une fonderie tierce ferait fuiter l'IP de chaque visiteur vers un sous-traitant hors UE, ce que je refuse pour une vitrine qui se veut irréprochable sur la vie privée. J'ai composé ce trio à dessein. Le wordmark Aubia impose Chakra Petch, et garder un Inter pour le corps aurait créé une divergence permanente avec ce titre. Geist et Geist Mono viennent de la même famille, leur alignement vertical entre prose et code est natif.

Le bloc @theme reste la source de vérité des trois piles de polices :

--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;

La police fallback qui suit chaque famille neutralise le décalage de mise en page au moment du swap. Seule Chakra Petch est préchargée, parce qu'elle porte le titre principal et pèse dans le rendu perçu.

Le mouvement, avec un cran d'arrêt

Les animations passent par Motion. J'ai centralisé les variants réutilisables dans un seul fichier, motion-presets.ts, plutôt que de les redéfinir dans chaque composant. L'apparition standard des sections est un fondu montant, en easing signature Luma :

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

Le cran d'arrêt, c'est l'accessibilité. Un visiteur qui a demandé moins de mouvement au niveau système ne doit pas subir de fondus. Motion respecte prefers-reduced-motion nativement, et je normalise sa valeur avec un hook useSafeReducedMotion() qui retourne toujours un booléen, même pendant le rendu serveur où la media query n'est pas encore évaluée. Le budget d'animation est volontairement serré : rarement plus de deux éléments animés à la fois dans le champ visible, pour hiérarchiser l'attention au lieu de la disperser.

Ces choix visuels servent une conviction produit que je détaille dans la section Vision de la landing. Le dernier article de la série regarde ce que les moteurs, eux, voient de tout cela : le SEO multilingue et l'extraction par les moteurs de réponse IA. Si l'esprit d'Aubia vous parle, vous pouvez rejoindre la liste d'attente.

Ce chantier se raconte ici, article après article. Embarquez dans l'aventure : vos retours dessineront la suite.

Rejoindre la liste d'attente