Thématisation
Un thème repose sur un ensemble de variables CSS (custom properties). Personnaliser le thème consiste simplement à en redéfinir certaines, sans aucune étape de build ni recompilation. Aucun composant n'utilise de valeurs en dur (couleur, rayon de bordure, durée de transition) : tous s'appuient sur des tokens sémantiques --vectis-*.
Modifier un token répercute immédiatement le changement sur l'ensemble des composants. Le thème est piloté via l'attribut data-theme, qui peut être appliqué à n'importe quel élément HTML, et pas seulement à la racine de la page.
Changer de thème
Vectis UI inclut deux thèmes natifs : clair et sombre. Tous deux réutilisent les mêmes tokens sémantiques en les liant à des nuances différentes des palettes. Basculer de thème ne demande aucun fichier CSS supplémentaire et n'impose aucun re-rendu de vos composants Vue. Il suffit de modifier un attribut HTML à tout moment, sans recompilation ni flash visuel (FOUC).
<!-- No attribute at all is the light theme --> <html> <!-- The same page, dark --> <html data-theme="dark">
Clair
Sombre
Les deux exemples ci-dessus partagent exactement le même balisage HTML : seul l'attribut de thème diffère. Si l'attribut est généralement appliqué sur la balise <html> pour toute l'application, il peut être défini sur n'importe quel élément du DOM. Grâce à l'héritage CSS, l'attribut le plus proche du composant prévaut sur toute son arborescence. Une barre de navigation sombre dans un layout clair, une carte de prévisualisation claire dans un éditeur sombre, ou une facture qui reste claire : aucun de ces cas ne nécessite un thème dédié, il suffit d'ajouter l'attribut sur le conteneur ciblé.
<!-- A dark aside inside a light page: the nearest attribute wins,
and everything under it inherits. -->
<main data-theme="light">
<VDataTable :columns="columns" :rows="rows" />
<aside data-theme="dark">
<VButton tone="accent">Deploy</VButton>
</aside>
</main>Les éléments enfants héritent automatiquement du thème de leur parent tant qu'ils n'en définissent pas un nouveau. En effet, l'attribut data-theme ne fait que réassigner un ensemble de variables CSS, qui se propagent naturellement le long du DOM. L'imbrication de thèmes se fait ainsi sans aucun surcoût de performance et reste réversible à n'importe quelle profondeur.
L'attribut applique également la propriété CSS color-scheme. Ainsi, les éléments non gérés directement par le design system s'adaptent automatiquement au thème actif au lieu de conserver leur style clair par défaut. Cela concerne principalement les barres de défilement, les contrôles de formulaire natifs et les composants propres au navigateur.
Les tokens ne contiennent volontairement aucune media query prefers-color-scheme. Suivre la préférence du système est un choix qui appartient à l'application, pas au design system. Cela s'implémente facilement en une ligne de JavaScript pour lire la configuration système et appliquer l'attribut. Gérer cela en CSS pur rendrait impossible la surcharge manuelle par l'utilisateur.
// Following the system, in one line. The application makes that call, not the library.
const dark = window.matchMedia('(prefers-color-scheme: dark)')
document.documentElement.dataset.theme = dark.matches ? 'dark' : 'light'Personnaliser les couleurs et les tokens
L'intégralité des styles de la bibliothèque repose sur des design tokens. Générés à partir d'une source TypeScript typée selon un format inspiré de la spécification DTCG du W3C, ils sont ensuite exposés sous forme de variables CSS structurées sur deux niveaux :
- Primitives : Cinq palettes OKLCH de 11 nuances chacune (ex.
--vectis-color-indigo-500), ainsi que les échelles d'espacement, de typographie, de rayons, d'ombres, de durées et de transitions. - Rôles sémantiques : Les seuls tokens directement consommés par les composants :
--vectis-color-surface,--vectis-color-text-muted,--vectis-color-accent,--vectis-radius-interactive,--vectis-focus-ring-color.
Un composant consomme le token d'accent, jamais une teinte spécifique comme un indigo précis. C'est ce qui rend la bibliothèque totalement personnalisable : modifiez la valeur du rôle d'accent, et l'ensemble des composants s'adapte sans jamais devoir surcharger leur CSS (boutons, badges, éléments sélectionnés, etc.).
L'anneau de focus dispose lui aussi d'un rôle dédié plutôt que d'être une simple déclinaison de l'accent. Ce choix répond à des exigences de contraste différentes : la couleur d'accent doit assurer la lisibilité du texte (souvent blanc), tandis que l'anneau de focus doit être immédiatement visible sur le fond de page. Veillez donc à réadapter ces deux tokens en parallèle.
Surcharger un token se fait via une simple déclaration CSS : elle s'insère naturellement dans vos feuilles de styles, que ce soit sur :root pour toute l'application, sur une classe pour une zone ciblée, ou sous [data-theme='dark'] pour varier selon le thème. Sa valeur peut être une couleur, un autre token ou une fonction calc().
Dans l'exemple ci-dessous, six tokens d'accent sont réassignés vers une nuance corail, l'anneau de focus est ajusté pour garantir un bon contraste sur chaque fond, et le rayon des contrôles et des puces est lié au token pill. Neuf déclarations CSS suffisent, sans modifier le moindre composant.
/* Any selector at all. This one is on the panel below. */
.coral {
--vectis-color-accent: oklch(64% 0.16 32);
--vectis-color-accent-hover: oklch(58% 0.16 32);
--vectis-color-accent-active: oklch(52% 0.15 32);
--vectis-color-accent-text: oklch(48% 0.15 32);
/* Mixed towards the surface, so both tints follow whichever theme is showing. */
--vectis-color-accent-surface: color-mix(in oklch, var(--vectis-color-accent) 14%, var(--vectis-color-surface));
--vectis-color-accent-border: color-mix(in oklch, var(--vectis-color-accent) 40%, var(--vectis-color-surface));
/* The focus ring is a role of its own, not the accent under another name: the accent
carries white text, the ring has to be seen against the page. This one value clears 3:1
on both grounds, so it needs no dark counterpart. */
--vectis-focus-ring-color: oklch(58% 0.16 32);
--vectis-radius-interactive: var(--vectis-radius-pill);
--vectis-radius-chip: var(--vectis-radius-pill);
}
/* Text needs a lighter step on a dark ground: the one role that has to differ. */
[data-theme='dark'] .coral {
--vectis-color-accent-text: oklch(78% 0.13 32);
}Bouton plein, bouton contour, chip, champ, zone de texte : aucun de ces composants ne spécifie de couleur ou de rayon en dur. Ils héritent donc tous automatiquement de cette redéfinition, et le comportement serait exactement le même pour n'importe quel autre composant placé dans ce panneau. La zone de texte est le cas intéressant : plus haute qu'un contrôle, elle prend le rayon qu'aurait un contrôle de même taille et reste alignée sur le champ au-dessus d'elle au lieu de devenir une ellipse.
Toutes les couleurs sont exprimées en OKLCH, et ce pour deux raisons : la clarté perceptuelle est strictement identique d'une palette à l'autre pour un même niveau de nuance, et le mélange de teintes produit des transitions naturelles sans passer par des tons grisâtres.
Vectis UI calcule automatiquement les états survolés, teintés ou désactivés de vos rôles grâce à la fonction color-mix(). Si vous fournissez une valeur issue d'un autre espace colorimétrique (où la perception de la clarté diffère), ces variations calculées risquent de perdre en cohérence. Conserver vos valeurs personnalisées en OKLCH vous garantit un rendu visuel parfaitement prévisible.
Vectis UI intègre uniquement cinq palettes fondamentales afin de couvrir les besoins essentiels sans alourdir le CSS : gray pour les surfaces, textes et bordures, indigo pour l'accentuation, puis red, green et amber pour les états d'erreur, de succès et d'avertissement. Inclure des palettes supplémentaires non utilisées ajouterait inutilement des variables CSS sur toutes vos pages.
L'ajout d'une teinte personnalisée relève donc de l'application. Il suffit de déclarer vos 11 nuances de variables CSS et d'y lier le rôle sémantique souhaité. La prise en compte est immédiate, sans aucune étape de compilation ni attente d'une nouvelle version de la bibliothèque
Tous les tokens sémantiques, avec ce que chacun contrôle et sa valeur par défaut dans les deux thèmes, sont listés sur la page Design tokens.
Couches CSS
Les styles de la bibliothèque sont structurés en quatre couches de cascade (@layer), ordonnées comme suit : vectis.reset, vectis.tokens, vectis.components et vectis.utilities. En CSS, les couches sont évaluées avant la spécificité des sélecteurs, et les styles non encapsulés (unlayered) ont la priorité sur l'ensemble des couches. Par conséquent, tout CSS rédigé hors couche dans votre projet surchargera nativement celui de la bibliothèque, quelle que soit la spécificité de votre sélecteur.
/* The order the library declares, for reference. */
@layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
/* Your rule is in no layer, so it wins over all four.
One class, no !important, nothing added to buy specificity. */
.v-button {
text-transform: uppercase;
}Ce comportement est un choix d'architecture délibéré qui élimine le recours aux contournements habituels : aucun usage de !important, aucun besoin de sur-spécifier vos sélecteurs (en ajoutant un ID ou en enchaînant des classes), et aucun conteneur superflu injecté dans le DOM uniquement pour augmenter la spécificité. Un simple nom de classe suffit à surcharger n'importe quel style d'un composant, tout en conservant une feuille de styles lisible et maintenable.
Attention : N'insérez pas vos propres règles directement dans @layer vectis.components. Les noms de couches étant globaux, le navigateur fusionnerait vos règles au sein de la couche de la bibliothèque : la priorité dépendrait alors uniquement de l'ordre d'apparition dans le code au lieu de garantir l'application de vos surcharges. Rédigez vos surcharges hors couche ou, si votre application utilise sa propre structure @layer, veillez à déclarer vos couches après celles de la bibliothèque.
Le CSS moderne et votre build
Vectis UI n'embarque que le CSS dont elle a besoin, sans rétrocompatibilité : :dir(), color-mix(), des couleurs OKLCH, le positionnement par ancre. Votre bundler minifie ces feuilles avec le reste de votre application, et un minifieur réglé sur des navigateurs inférieurs à ceux que la bibliothèque supporte ne se contente pas d'écarter ce qu'ils ne savent pas lire. Il le réécrit.
L'une de ces réécritures change le sens d'une règle au lieu d'approcher son résultat. Lightning CSS, le minifieur par défaut de Vite 8 et de Parcel, remplace :dir(rtl) par une liste de sélecteurs :lang() dès que ses cibles sont antérieures à Chrome 120. Cette liste teste la langue de la page, là où la bibliothèque bascule sur sa direction : sur une page <html dir="rtl" lang="en">, la règle ne s'applique donc jamais. Onze composants cessent de se refléter : les flèches de la pagination, des onglets, du fil d'Ariane, du menu, du calendrier, du sélecteur de date et du carrousel, le coin du badge en surimpression, le sens de rotation de la progression circulaire, la copie de texte détourée de la progression linéaire et la vague du squelette de chargement. Rien n'échoue, et un serveur de développement n'en montre rien, puisque seul un build de production minifie.
Vite déduit build.cssTarget de build.target, dont la valeur par défaut désigne des navigateurs très en deçà de ce que la bibliothèque exige. Y nommer le socle supporté suffit, et allège au passage la sortie : abaissée, chaque couleur OKLCH de la palette est émise deux fois, une fois en repli sRGB et une fois dans un autre espace colorimétrique.
// vite.config.ts: the browsers Vectis UI is written for
export default defineConfig({
build: { cssTarget: ['chrome134', 'edge134', 'safari26', 'firefox147'] },
})
// nuxt.config.ts: the same value, one level down
export default defineNuxtConfig({
vite: { build: { cssTarget: ['chrome134', 'edge134', 'safari26', 'firefox147'] } },
})