Diario di bordo · Fase 0 · 6/7
Catppuccin e vetro smerigliato: il design system di aubia.dev
Due temi Catppuccin, superfici traslucide e tre font definiscono aubia.dev. Le impostazioni condivise danno coerenza al sito, con alcuni limiti nella sfocatura e nel movimento.
- design-system
- catppuccin
- tailwind
- motion

Lo sfondo scuro di aubia.dev non è nero. È un grigio con una sfumatura blu, #1e1e2e, su cui quattro gradienti radiali disegnano macchie di colore malva, lavanda, pesca e blu. Queste macchie restano fisse durante lo scorrimento. Le card e la barra di navigazione ne lasciano vedere il colore attraverso uno sfondo traslucido.
Ho scelto Catppuccin per i temi del sito. Il malva compare nei link degli articoli e nei contorni del focus da tastiera; superfici, titoli e animazioni hanno le proprie impostazioni condivise. Un componente può riutilizzare una card o un'animazione di ingresso senza ridefinire ogni colore e ogni durata.
Mocha e Latte, gli stessi nomi per due temi
La palette ufficiale di Catppuccin comprende quattro temi. Il sito ne usa due: Mocha, il più scuro, e Latte, quello chiaro. Ognuno fornisce ventisei colori con gli stessi nomi. text indica il colore del testo, base lo sfondo principale; mauve e peach sono tonalità il cui uso dipende dall'interfaccia.
Questi colori diventano variabili CSS con il prefisso --color-ctp-. Dichiararli nel blocco @theme di Tailwind rende disponibili classi come text-ctp-mauve e bg-ctp-base. La classe nel componente resta la stessa quando cambia il tema. La sua variabile passa, per esempio, dal malva chiaro di Mocha, #cba6f7, al viola più intenso di Latte, #8839ef.
I ventisei colori non bastano a descrivere tutto il sito. Il vetro richiede un colore di sfondo, un contorno e dei riflessi; i gradienti dei titoli hanno i propri punti di colore. Queste impostazioni completano la palette nel CSS. Gli accenti mantengono la tinta Catppuccin originale in entrambi i temi, titoli ed evidenziazione Phiki del blog compresi. Sugli sfondi chiari alcuni scendono sotto la soglia di contrasto WCAG AA; il sito assume questa scelta visiva.
Il tema chiaro richiede più di uno scambio tra testo e sfondo. In modalità scura, le card usano il grigio intenso mantle, con contorni bianchi a bassa opacità. In modalità chiara usano base, quasi bianco, con un contorno scuro. Le ombre esterne delle superfici in vetro, invisibili in Mocha, diventano grigio-blu in Latte. La luce separa le superfici in un tema, l'ombra nell'altro.
La scelta del tema prima che appaia la pagina
Alla prima visita con JavaScript attivo, il sito mostra Mocha, anche se il sistema preferisce la modalità chiara. Il selettore propone "Chiaro", "Scuro" e "Sistema". Quest'ultima opzione rimuove l'attributo data-theme dalla radice HTML, consentendo alla media query prefers-color-scheme di applicare il tema del sistema.
Un piccolo script nel <head> legge la preferenza salvata prima che React venga caricato. Se non trova un valore valido, o se lo storage locale è inaccessibile, imposta data-theme="dark". Il CSS contiene i valori di Latte sotto [data-theme='light'] e sotto la media query della modalità sistema. Il browser può scegliere i colori corretti senza attendere l'idratazione della pagina.
Quando cambia tema, il selettore applica la sua scelta al documento e prova a salvarla. Se lo storage è bloccato, il cambiamento funziona durante la visita, ma potrebbe non conservarsi al suo ritorno. Senza JavaScript, lo script iniziale non viene eseguito: l'assenza dell'attributo lascia che il CSS segua il sistema.
L'effetto vetro delle card
La trasparenza lascia vedere lo sfondo; la sfocatura ne attenua i dettagli. Sulle card, queste due impostazioni si combinano con un bordo e un riflesso interno:
@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);
}
La miscela con transparent dà qui allo sfondo un'opacità del 62%. Colora ciò che appare dietro la card senza nasconderlo del tutto. La proprietà backdrop-filter sfoca lo sfondo, poi ne aumenta la saturazione e, leggermente, la luminosità. Il testo della card resta nitido. Con uno sfondo completamente opaco, i pixel retrostanti esistono ancora, ma lo sfondo nasconde il risultato del filtro.
Il riflesso interno schiarisce il bordo superiore e resta bianco a bassa opacità in entrambi i temi. Il bordo passa dal bianco al grigio-blu in modalità chiara, indipendentemente dalla sfocatura. Separa la card dallo sfondo senza darle un contorno luminoso.
Non tutte le superfici usano la stessa miscela. I pulsanti interni del menu usano il 58% di colore di sfondo, la navigazione il 70% e la variante glass-strong l'80%. Il raggio di sfocatura va dai 40 pixel delle card agli 80 di questa variante densa, usata intorno al form di iscrizione. La superficie del form nasconde una parte maggiore della decorazione rispetto ai pulsanti del menu.
Il menu delle lingue mantiene uno sfondo pieno
Sovrapporre due superfici sfocate non equivale ad applicare due volte lo stesso effetto allo sfondo della pagina. Un antenato che ha già un backdrop-filter definisce una backdrop root, un limite oltre il quale il filtro di un discendente non può più recuperare i pixel. will-change: opacity può creare lo stesso limite. Il figlio non eredita il filtro; cambia invece l'area accessibile al proprio filtro.
Il selettore della lingua si trova in questa situazione all'interno della navigazione. Il suo pannello usa quindi uno sfondo opaco, del colore delle card. Le etichette non dipendono da una seconda sfocatura per restare distinte dal contenuto che scorre dietro di loro.
Il pannello è anche un elemento HTML <details>, i cui link vengono renderizzati persino quando è chiuso. Sono presenti nell'HTML iniziale prodotto dal rendering lato server. Si apre senza JavaScript; React aggiunge la chiusura al clic all'esterno o con Esc.
Un portal React potrebbe spostare un pannello altrove nel documento per sottrarlo ai suoi antenati. Questo spostamento non rimuove i link dal documento. Un pannello montato solo sul client, invece, non fornisce quei link nella risposta HTML iniziale. Il <details> usato qui conserva sia i link renderizzati lato server sia l'apertura nativa, senza un portal.
Chakra Petch per i titoli, Geist per la lettura
Le forme angolose di Chakra Petch compaiono nei titoli. Geist è usato per il corpo del testo e l'interfaccia, Geist Mono per il codice. In un articolo come questo, tale ripartizione distingue sottotitoli, paragrafi ed estratti senza richiedere un cambio di colore a ogni passaggio.
Tutti e tre i font sono ospitati sul sito. Il browser li scarica dal sito, senza richieste a Google Fonts o a un altro fornitore di font. I file provengono dai pacchetti Fontsource installati; il plugin dei font di laravel-vite-plugin prepara le dichiarazioni @font-face durante la build, poi la direttiva Blade @fonts le include nella pagina.
Tutti e tre i font usano il provider local(), direttamente dai loro pacchetti. Chakra Petch carica un solo WOFF2 latino, con peso 600 e stile normale, senza una variante WOFF ridondante. Geist e Geist Mono sono font variabili: il loro file WOFF2 copre i pesi da 100 a 900.
Chakra Petch e Geist vengono precaricati sulle pagine pubbliche. Geist Mono viene precaricato anche sulla home page e sul blog, ma non sulle pagine legali: tre precaricamenti di font nel primo caso, due nel secondo. L'estratto di configurazione di Chakra Petch specifica cosa deve accadere durante il download:
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'],
}),
Con swap, il browser può mostrare il testo in un font di fallback prima di ricevere quello previsto. Il plugin adatta le metriche dei fallback di Chakra Petch e di Geist per avvicinarne le dimensioni a quelle dei font previsti. Queste approssimazioni riducono i possibili spostamenti al cambio di font, senza garantire l'assenza di variazioni del layout. Geist Mono mantiene i propri fallback monospace: la generazione di fallback con metriche adattate è disattivata per questa famiglia.
Animazioni di ingresso e sequenze animate
I titoli restano visibili nell'HTML del server. L'H1 è statico; il titolo della FAQ usa la variante text di HeadingReveal, con initial={false}. Ogni parola si sposta di 8 pixel in 700 millisecondi, senza variazioni di opacità né sfocatura. Questi stati sono definiti nel componente:
const wordVariants = {
hidden: { opacity: 1, y: 0 },
visible: {
opacity: 1,
y: [8, 0],
transition: { duration: 0.7, ease: EASE_LUMA },
},
};
Ogni parola è racchiusa in un m.span in inline-block che riceve wordVariants, con uno sfasamento predefinito di 60 millisecondi tra le parole. L'animazione di ingresso si attiva una sola volta, quando metà dell'elemento osservato è visibile. La costante condivisa EASE_LUMA vale [0.22, 0.61, 0.36, 1]: questa curva di Bézier controlla l'accelerazione e la decelerazione. I nodi delle illustrazioni usano animazioni di ingresso a molla, con rigidità e smorzamento.
Motion non controlla tutto. Cicli CSS animano le illustrazioni e timer JavaScript fanno scorrere il testo della frase introduttiva lettera per lettera. Le card della sezione Vision hanno un'animazione di ingresso CSS legata allo scorrimento, attiva solo se il browser supporta animation-timeline: view() e il visitatore non ha richiesto il movimento ridotto. Altrimenti mantengono il proprio stato visibile predefinito.
Reagire alla preferenza di movimento ridotto
L'impostazione di sistema "Riduci movimento" può cambiare mentre una pagina è aperta. L'hook locale useSafeReducedMotion legge la media query corrispondente con useSyncExternalStore e si sottoscrive all'evento change di matchMedia. React può quindi renderizzare nuovamente i componenti sottoscritti quando cambia la preferenza.
Questa implementazione locale evita un limite di Motion 13.2.0: il suo hook useReducedMotion inizializza uno stato React senza aggiornarlo, anche se la documentazione dichiara che reagisce ai cambiamenti.
Il server non conosce la preferenza del browser. L'hook locale restituisce quindi false per il rendering lato server e l'idratazione iniziale, poi legge il valore effettivo sul client. Questo uso dello snapshot del server in entrambi i casi corrisponde al comportamento descritto da React per useSyncExternalStore.
Il CSS interviene senza attendere questa lettura: le regole per il movimento ridotto fermano il cursore lampeggiante, l'animazione di ingresso fade-up e i cicli delle illustrazioni a cui si applicano. I coriandoli hanno una protezione propria. Queste regole non disattivano tutte le transizioni del sito.
Il testo animato mostra l'ultima frase nel rendering del server. Quando il movimento ridotto è attivo, mantiene quella frase e non avvia timer. Un cambio di preferenza durante la visita annulla il timer in corso; disattivare il movimento ridotto permette di riprendere la sequenza. Le frasi sovrapposte, invisibili ed escluse dagli screen reader, riservano l'altezza necessaria alla più lunga. Anche il contatore differito ha uno spazio riservato sotto il form.
Questo cantiere si racconta qui, articolo dopo articolo. Ciò che viene dopo dipende da ciò che lei ne dirà.
Si iscriva alla lista d'attesa