Qu’est-ce que Vanilla Extract ?
Vanilla Extract se présente comme une bibliothèque permettant d’utiliser TypeScript comme préprocesseur CSS. Voilà. ◔_◔
Bon ok, on va essayer de développer un peu…
Vanilla Extract est issu de la famille des outils CSS-in-JS tels que styled-component ou Emotion à ceci près que Vanilla Extract est, comme sa propre présentation l’indique, orienté TypeScript.
Le CSS-in-JS
Si je devais présenter CSS-in-JS, je dirais que c’est une approche de stylisation des interfaces web qui consiste à écrire du CSS directement dans du JavaScript. Contrairement aux méthodes traditionnelles où les styles sont définis dans des fichiers .css
séparés, le CSS-in-JS permet de définir, appliquer et manipuler les styles depuis le code JavaScript. (Chose étonnante, il se trouve que Chat-GPT a exactement la même définition que moi ! ¯\_(ツ)_/¯).
Cela présente un certains nombre d’avantages concernant l’aspect « scopé » du css, la facilité pour rendre les classes dynamiques, la possibilité de faire cohabiter le js et le css dans chaque composant…
Pourquoi Vanilla Extract ?
Les outils CSS-in-JS présentent également un certain nombre désavantages (liste non exhaustive) :
- problème de performances lorsque le css est compilé au run time dans le navigateur (ce qui est le cas de la plupart des outils CSS-in-JS)
- fichiers JS plus importants
- courbe d’apprentissage plus « raide » pour un intégrateur qui ne connaîtrait pas JS
Si Vanilla-Extract ne solutionne pas le dernier point évoqué, il permet d’éviter les deux premiers : Vanilla-Extract va générer les fichiers au build time. Cela change tout : on garde les avantage du css-in-js (sauf la cohabitation du js et du css, on y reviendra…) en se débarrassant de certains de ses inconvénients !
Dernier petit Bonus, avec Vanilla-Extract, on bénéficie dans notre IDE, out-of-the-box, de deux gros avantages de typescript : le typechecking et l’autocomplétion !

Utiliser Vanilla Extract
Créer et appliquer un style
Vanilla Extract nous propose un ensemble d’API qui va nous permettre de générer le CSS. Comme expliqué dans la doc Vanilla Extract, l’API style, par exemple, va nous permettre de générer une classe CSS et de lui associé des règles.
Imaginons que nous voulions gérer le style d’un composant Header (Header.tsx). Dans un fichier Header.css.ts, on va écrire le code suivant :
//Header.css.ts
import { style } from '@vanilla-extract/css';
export const headerMenu = style({
backgroundColor: "white",
color: "black",
});
…qui va générer et exporter la classe suivante :
.Header_headerMenu__lmf3270 {
color: black;
background-color: white;
}
… et que l’on pourra appliquer ainsi dans notre composant Header (Header.tsx) :
// Header.tsx
const Header: React.FC = () => {
...
return (
<header>
<nav className={headerMenu}>
...
</nav>
</header>
);
};
Il faut prendre en compte de petits changements en termes de syntaxe, mais jusqu’ici, rien de bien compliqué, surtout si on s’appuie sur la très bonne doc de VE.
La ‘cohabitation’ n’est pas possible avec Vanilla-Extract
Les fonctions de style telles que style(), styleVariant(), globalStyle(), etc… ne peuvent être appelées que dans un fichier *.css.ts. Cela oblige chaque composant à avoir son propre fichier de style. Cela peut être considéré comme un inconvénient pour les adeptes de la cohabitation (le fait d’avoir le style et le code dans un même fichier comme dans les Single File Component de Vue JS.
Créer un thème
Imaginons maintenant que nous voulions un site qui possède un thème light et un thème dark. Il me faut remplacer les valeur en dur du CSS par des variables. Avec Vanilla Extract ca sera très simple :
On va crée un contrat de thème en utilisant l’api createThemeContract. Un contrat de thème c’est simplement la déclaration de toutes les variables (sans valeurs) qui seront utilisées dans un thème. Ce contrat sera ensuite fourni à chaque thème. Vanilla Extract s’assurera ensuite que chaque thème respecte scrupuleusement ce contrat. Si on reprend l’exemple ci-dessus, ca nous donnerait quelque chose comme ceci :
//themeContract.css.ts
import { createThemeContract } from "@vanilla-extract/css";
export const themeVars = createThemeContract({
header: {
background: {
color: null,
},
font: {
color: null,
},
}
//themeLight.css.ts
import { createTheme } from "@vanilla-extract/css";
export const themeLight = createTheme(themeVars, {
header: {
background: {
color: "white",
},
font: {
color: "black",
},
}
});
//themeDark.css.ts
import { createTheme } from "@vanilla-extract/css";
export const themeDark = createTheme(themeVars, {
header: {
background: {
color: "black",
},
font: {
color: "white",
},
}
});
On utilise ensuite le variables de notre contrat de thème dans le fichier CSS du composant :
//Header.css.ts
export const headerMenu = style({
color: themeVars.header.font.color,
backgroundColor: themeVars.header.background.color
});
Enfin, il ne nous reste plus qu’à appliquer notre thème comme un style sur un élément parent :
// Layout.tsx
const Layout: React.FC = () => {
...
return (
<div className={themeDark}>
<header>
<nav className={headerMenu}>
...
</nav>
</header>
</div>
);
};
Créer ses propres classes utilitaires avec Vanilla Extract et Sp'(rinkles
Pour créer son propre design system, VE nous propose un package optionnel : SPRINKLES
Sprinkles va nous permettre de définir des propriétés et des valeurs possibles pour ces propriétés, de combiner plusieurs propriétés et configurer des conditions.
Comme vous pouvez le voir dans la doc, une fois que l’on a configuré le fichier sprinkles.css.ts
qui va bien, on peut utiliser la fonction sprinkles()
de cette manière :
//styles.css.ts
import { sprinkles } from './sprinkles.css.ts';
export const container = sprinkles({
display: 'flex',
//on a préalablement défini dans sprinkles.css.ts que 'paddingX' = paddingLeft + paddingRight :
paddingX: 'small' // 'small' fait partie des valeurs possibles définies également dans sprinkles.css.ts
// Les règles conditionnelles en fonction du support (mobile ou ordinateur) et du thème (light ou dark) :
flexDirection: {
mobile: 'column',
desktop: 'row'
},
background: {
lightMode: 'blue-50',
darkMode: 'gray-700'
}
});
C’est un peu sympa, mais c’est un peu verbeux, surtout si on doit combiner les fonctions sprinkles()
et style()
:
export const container = style([
sprinkles({
display: 'flex',
padding: 'small'
}),
{
':hover': {
outline: '2px solid currentColor'
}
}
]);
Là où ça devient vraiment cool, c’est quand on se crée un petit…
…COMPOSANT PRIMITIF POLYMORPHE !!!

Un composant primitif polymorphe
Derrière cette expression qui vous permettra d’avoir l’air intelligent, se cache un principe assez simple : créer un composant générique qui pourra représenter n’importe quel élément html (ici via la prop react « as »), qui pourra accepter n’importe quelle prop native HTML mais aussi les props utilisant les propriétés que l’on vient de définir grâce à Sprinkles.
Par convention, nous nommerons ce composant « Box » 📦.
ℹ Ce pattern n’est pas une invention de Vanilla Extract (qui n’en parle d’ailleurs pas explicitement dans sa doc) mais un pattern déjà utilisé dans bon nombre de design system (Chakra Ui, Theme UI…) et est recommandé par les utilisateurs de V-E. Il existe même une petit librairie qui peut créer ce composant pour nous : dessert-box.
Maintenant la question que tout le monde se pose :

Eh bien dans la boite, il y a ceci :
//Box.tsx
import React from "react";
import clsx from "clsx";
import { sprinkles, Sprinkles } from "../styles/sprinkles.css";
export type BoxProps = React.PropsWithChildren &
Sprinkles &
Omit<React.AllHTMLAttributes<HTMLElement>, "color"> & {
as?: React.ElementType;
};
export const Box = React.forwardRef<unknown, BoxProps>(({ as = "div", className, ...props }, ref) => {
const sprinklesProps: Record<string, unknown> = {};
const nativeProps: Record<string, unknown> = {};
Object.entries(props).forEach(([key, value]) => {
if (sprinkles.properties.has(key as keyof Sprinkles)) {
sprinklesProps[key] = value;
} else {
nativeProps[key] = value;
}
});
return React.createElement(as, {
className: clsx([sprinkles(sprinklesProps), className]),
ref,
...nativeProps,
});
});
C’est pas bien compliqué mais voyons quand même ce code dans le détail :
Imports
import React from "react";
import clsx from "clsx";
import { sprinkles, Sprinkles } from "../styles/sprinkles.css";
React
→ nécessaire pour créer des composants React.clsx
→ permet de fusionner facilement plusieurs classes CSS (évite les concatenations manuelles).sprinkles
etSprinkles
→ viennent probablement d’un système de design basé sur Vanilla Extract
Type des props (BoxProps
)
export type BoxProps = React.PropsWithChildren &
Sprinkles &
Omit<React.AllHTMLAttributes<HTMLElement>, "color"> & {
as?: React.ElementType;
};
React.PropsWithChildren
→ autorise d’avoir deschildren
dans ce composant.Sprinkles
→ permet de passer toutes les props définies par le systèmesprinkles
(par ex.padding="small"
,background="blue"
…).Omit<React.AllHTMLAttributes<HTMLElement>, "color">
→ autorise toutes les props HTML classiques (id
,onClick
, etc.), saufcolor
(car déjà gérée parsprinkles
).{ as?: React.ElementType }
→ permet de changer le type de balise HTML (par défautdiv
).
Le composant Box
export const Box = React.forwardRef<unknown, BoxProps>(
({ as = "div", className, ...props }, ref) => {
React.forwardRef
→ permet de transmettre uneref
au DOM (utile pour manipuler l’élément directement, ex. focus).as = "div"
→ valeur par défaut si l’utilisateur ne précise pas la balise.className
→ récupère une éventuelle classe CSS donnée par l’utilisateur....props
→ récupère toutes les autres props (styles, HTML, événements…).
Tri des props
const sprinklesProps: Record<string, unknown> = {};
const nativeProps: Record<string, unknown> = {};
Object.entries(props).forEach(([key, value]) => {
if (sprinkles.properties.has(key as keyof Sprinkles)) {
sprinklesProps[key] = value;
} else {
nativeProps[key] = value;
}
});
- On sépare les props de style (sprinkles) des props natives HTML.
- Exemple :
<Box padding="large" id="myBox" onClick={...} />
padding="large"
→ va danssprinklesProps
.id="myBox"
etonClick={...}
→ vont dansnativeProps
.
Rendu
return React.createElement(as, {
className: clsx([sprinkles(sprinklesProps), className]),
ref,
...nativeProps,
});
});
React.createElement(as, {...})
→ crée dynamiquement l’élément HTML (div
,span
, etc.).className: clsx([sprinkles(sprinklesProps), className])
→ applique :- la classe générée par
sprinkles
(styles utilitaires)- la classe custom passée via
className
.
- la classe custom passée via
ref
→ transmet la référence au DOM....nativeProps
→ applique les autres props HTML normales (id, aria-label, onClick, etc.).
Ca y est, on est tout bon voici par exemple comment on peut désormais l’utiliser :
<Box
as={"div"}
className={"container"}
display="flex"
justifyContent="center"
px={"16"}
backgroundColor="primary"
>
<Box
as={"h2"}
className={"title"}
mb={"8"}
fontSize={{ mobile: "xxLarge", tablet: "xxLarge", smallDesktop: "large", largeDesktop: "large" }}
>
Title
</Box>
<Box as={"button"} ml={"64"} backgroundColor="secondary"></Box>
</Box>
Franchement ? Elle est pas belle la vie ?
Conclusion
Bien évidemment, par sa syntaxe css-in-ts, V-E est un peu déstabilisant au début. Cependant la prise en main est assez rapide (et le sera d’autant plus dans le cadre d’un onboarding).
Le fait d’avoir un le style en typescript ouvre tout un horizon pour rendre nos classes css encore plus dynamiques.
Par ailleurs le cadre assez stricte offert pas Vanilla Extract me semble opportun dans le cadre du travail en équipe. C’est un bon moyen de garder un css bien structuré.