Aller au contenu principal

Journal de bord · Phase 0 · 6/7

Catppuccin et verre dépoli : le design system d'aubia.dev

Deux thèmes Catppuccin, des surfaces translucides et trois polices composent aubia.dev. Leurs réglages communs donnent sa cohérence au site, avec des limites liées au flou et au mouvement.

Publié le 17 août 2026Mis à jour le 27 septembre 20269 min de lecture
  • design-system
  • catppuccin
  • tailwind
  • motion

Le fond sombre d'aubia.dev n'est pas noir. C'est un gris teinté de bleu, #1e1e2e, sur lequel quatre dégradés radiaux dessinent des nappes mauve, lavande, pêche et bleue. Ces nappes restent fixes pendant le défilement. Les cartes et la barre de navigation laissent voir leur couleur à travers un fond translucide.

J'ai retenu Catppuccin pour les thèmes du site. Le mauve se retrouve dans les liens des articles et les contours de focus au clavier ; les surfaces, les titres et les animations ont leurs propres réglages partagés. Un composant peut réutiliser une carte ou une apparition sans redéfinir chaque couleur et chaque durée.

Mocha et Latte, les mêmes noms pour deux thèmes

La palette officielle de Catppuccin comprend quatre thèmes. Le site en utilise deux : Mocha, le plus sombre, et Latte, le clair. Chacun fournit vingt-six couleurs, avec les mêmes noms. text désigne le texte, base le fond principal ; mauve et peach désignent des teintes dont l'usage dépend de l'interface.

Ces couleurs deviennent des variables CSS préfixées par --color-ctp-. Leur déclaration dans le bloc @theme de Tailwind rend disponibles des classes comme text-ctp-mauve ou bg-ctp-base. La classe reste identique dans le composant quand le thème change. Sa variable passe, par exemple, du mauve clair de Mocha, #cba6f7, au violet plus soutenu de Latte, #8839ef.

Les vingt-six couleurs ne suffisent pas à décrire tout le site. Le verre demande une couleur de fond, un contour et des reflets ; les dégradés des titres ont leurs étapes de couleur. Ces réglages complètent la palette dans le CSS. Les accents gardent leur teinte Catppuccin d'origine dans les deux thèmes, titres et coloration Phiki du blog compris. Sur fond clair, quelques-uns passent sous le seuil de contraste WCAG AA ; le site assume ce choix visuel.

Le thème clair demande davantage qu'un échange entre le texte et le fond. En sombre, les cartes utilisent le gris profond mantle, et leurs contours reçoivent du blanc à faible opacité. En clair, elles utilisent base, presque blanc, avec un contour sombre. Les ombres externes des surfaces en verre, invisibles en Mocha, deviennent gris bleuté en Latte. La séparation entre les surfaces se fait par la lumière dans un cas, par une ombre dans l'autre.

Le thème se choisit avant l'affichage

Lors d'une première visite avec JavaScript actif, le site affiche Mocha, même si le système préfère le clair. Le sélecteur propose « Clair », « Sombre » et « Système ». Ce dernier choix retire l'attribut data-theme de la racine HTML : la media query prefers-color-scheme peut alors appliquer le thème du système.

Un petit script dans le <head> lit la préférence enregistrée avant le chargement de React. Sans valeur valide, ou si le stockage local est inaccessible, il définit data-theme="dark". Le CSS contient les valeurs de Latte sous [data-theme='light'] et sous la media query du mode système. Le navigateur peut choisir les bonnes couleurs sans attendre l'hydratation de la page.

Lorsque vous changez de thème, le sélecteur applique le choix au document et tente de l'enregistrer. Si le stockage est bloqué, le changement fonctionne pendant la visite, mais sa conservation au retour suivant n'est pas assurée. Sans JavaScript, le script initial ne s'exécute pas : l'absence d'attribut laisse le CSS suivre le système.

L'effet de verre des cartes

La transparence laisse voir l'arrière-plan ; le flou en atténue les détails. Sur les cartes, ces deux réglages sont réunis avec une bordure et un reflet intérieur :

@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 mélange avec transparent donne ici un fond à 62 % d'opacité. Il colore ce qui apparaît derrière la carte sans le cacher complètement. La propriété backdrop-filter applique le flou à l'arrière-plan, puis augmente sa saturation et légèrement sa luminosité. Le texte de la carte reste net. Avec un fond entièrement opaque, les pixels de l'arrière-plan existent toujours, mais le fond masque le résultat du filtre.

Le filet intérieur éclaircit l'arête haute et reste blanc à faible opacité dans les deux thèmes. La bordure passe du blanc au gris bleuté en clair, indépendamment du flou. Elle distingue la carte de son fond sans lui donner un contour lumineux.

Toutes les surfaces n'ont pas le même dosage. Les boutons internes du menu utilisent 58 % de couleur de fond, la navigation 70 % et la variante glass-strong 80 %. Le rayon de flou va de 40 pixels pour les cartes à 80 pour cette variante dense, utilisée autour du formulaire d'inscription. La surface du formulaire masque davantage le décor que les boutons du menu.

Le menu de langues garde un fond plein

Empiler deux surfaces floutées ne revient pas à appliquer deux fois le même effet au fond de page. Un ancêtre qui possède déjà un backdrop-filter définit une backdrop root, la limite au-delà de laquelle le filtre d'un descendant ne récupère plus les pixels. will-change: opacity peut créer la même limite. Le filtre n'est pas hérité par l'enfant ; c'est la zone accessible à son propre filtre qui change.

Le sélecteur de langue se trouve dans cette situation dans la navigation. Son panneau utilise donc un fond opaque, avec la couleur des cartes. Les libellés ne dépendent pas d'un second flou pour rester séparés du contenu qui défile derrière eux.

Le panneau est aussi un élément HTML <details>, dont les liens sont rendus même lorsqu'il est fermé. Ils sont présents dans le HTML initial produit par le rendu serveur. L'ouverture fonctionne sans JavaScript ; React ajoute la fermeture au clic extérieur et avec Échap.

Un portail React pourrait déplacer un panneau ailleurs dans le document pour échapper à ses ancêtres. Ce déplacement ne supprime pas les liens du document. En revanche, un panneau monté uniquement côté client ne fournit pas ces liens dans la réponse HTML initiale. Le <details> utilisé ici conserve à la fois les liens rendus côté serveur et une ouverture native, sans portail.

Les titres en Chakra Petch, la lecture en Geist

Les formes anguleuses de Chakra Petch apparaissent dans les titres. Geist est utilisée pour le corps de texte et l'interface, Geist Mono pour le code. Dans un article comme celui-ci, cette répartition distingue les intertitres, les paragraphes et les extraits sans demander un changement de couleur à chaque passage.

Les trois polices sont auto-hébergées. Le navigateur les télécharge depuis le site, sans requête vers Google Fonts ou un autre fournisseur de polices. Les fichiers proviennent des packages Fontsource installés ; le plugin de polices de laravel-vite-plugin prépare les déclarations @font-face au build, puis la directive Blade @fonts les intègre à la page.

Les trois polices utilisent le provider local(), directement depuis leurs packages. Chakra Petch charge un seul WOFF2 latin, en graisse 600 et style normal, sans variante WOFF redondante. Geist et Geist Mono sont des polices variables : leur fichier WOFF2 couvre une plage de graisses, de 100 à 900.

Chakra Petch et Geist sont préchargées sur les pages publiques. Geist Mono l'est aussi sur l'accueil et le blog, mais pas sur les pages légales : trois préchargements de polices dans le premier cas, deux dans le second. L'extrait de configuration de Chakra Petch comprend le comportement à adopter pendant son téléchargement :

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'],
}),

Avec swap, le navigateur peut afficher le texte dans une police de repli avant de recevoir la police prévue. Le plugin ajuste les métriques des replis de Chakra Petch et de Geist pour rapprocher leur encombrement. Ces approximations réduisent les déplacements possibles au changement de police, sans garantir un décalage de mise en page nul. Geist Mono conserve ses replis monospace : la génération de replis ajustés est désactivée pour cette famille.

Des apparitions et des séquences animées

Les titres restent visibles dans le HTML serveur. Le H1 est statique ; le titre de la FAQ utilise la variante text du composant HeadingReveal, avec initial={false}. Chaque mot effectue un déplacement de 8 pixels en 700 millisecondes, sans baisse d'opacité ni flou. Ces états sont définis dans le composant :

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

Chaque mot est contenu dans un m.span en inline-block qui reçoit wordVariants, avec un décalage de 60 millisecondes par défaut entre les mots. L'apparition se déclenche une seule fois, quand la moitié de l'élément observé est visible. La constante partagée EASE_LUMA vaut [0.22, 0.61, 0.36, 1] : cette courbe de Bézier règle l'accélération et le ralentissement. Les apparitions de noeuds dans les illustrations utilisent des ressorts, avec une raideur et un amortissement.

Motion ne pilote pas tout. Des boucles CSS animent les illustrations, et des timers JavaScript font défiler le texte de l'accroche lettre par lettre. Les cartes de la section Vision ont une apparition liée au défilement en CSS, activée seulement si le navigateur prend en charge animation-timeline: view() et si le visiteur n'a pas demandé de mouvement réduit. Sinon, elles gardent leur état visible de base.

Réagir à la préférence de mouvement réduit

Le réglage système « Réduire les animations » peut changer pendant qu'une page est ouverte. Le hook local useSafeReducedMotion lit la media query correspondante avec useSyncExternalStore et s'abonne à l'événement change de matchMedia. React peut alors rendre à nouveau les composants abonnés quand la préférence change.

Cette lecture locale évite une limite de la version 13.2.0 de Motion : son hook useReducedMotion initialise un état React sans le mettre à jour, bien que la documentation annonce une réaction aux changements.

Le serveur ne connaît pas la préférence du navigateur. Le hook local retourne donc false pour le rendu serveur et pour l'hydratation initiale, puis lit la valeur réelle côté client. Ce double usage du snapshot serveur est celui décrit par React pour useSyncExternalStore.

Le CSS intervient sans attendre cette lecture : les règles de mouvement réduit arrêtent le curseur clignotant, l'apparition fade-up et les boucles d'illustration qu'elles ciblent. Les confettis ont leur propre protection. Ces règles ne désactivent pas toutes les transitions du site.

Le texte animé de l'accroche affiche sa dernière phrase au rendu serveur. Lorsque le mouvement réduit est actif, il conserve cette phrase et n'arme aucun timer. Un changement de préférence pendant la visite annule le timer en cours ; désactiver le mouvement réduit permet de reprendre la séquence. Les phrases superposées, invisibles et exclues des lecteurs d'écran, réservent la hauteur nécessaire à la plus longue. Le compteur différé dispose aussi d'un espace réservé sous le formulaire.

Ce chantier se raconte ici, article après article. Ce qui vient ensuite dépend de ce que vous en direz.

Rejoindre la liste d'attente