Cours JavaScript et React · Module 14: Styles avancés
CSS-in-JS zero-runtime - Compilation cosmique des styles
Dans cette leçon8
Pilote, imagine deux types de moteurs de vaisseau spatial. Le premier calcule la trajectoire de vol en plein vol -- en consommant du carburant au fur et à mesure (CSS-in-JS runtime comme styled-components). Le second calcule la trajectoire avant le lancement et la sauvegarde dans l'ordinateur de bord -- pendant le vol, il ne consomme aucune énergie supplémentaire (CSS-in-JS zero-runtime). Dans le monde du frontend, c'est la différence entre les approches runtime et zero-runtime du stylage.
Le problème du CSS-in-JS runtime
Styled-components et Emotion fonctionnent ainsi : à chaque fois que React affiche un composant, la bibliothèque à l'exécution analyse le template literal, génère un hash de classe unique et injecte une balise <style> dans le DOM. Cela signifie :
1// Styled-components -- runtime
2const Card = styled.div`
3 background: ${({ theme }) => theme.colors.surface};
4 padding: ${({ theme }) => theme.spacing.lg};
5 border-radius: 12px;
6`;
7// À chaque render : analyser le template -> générer le hash -> injecter <style>Problèmes :
- Bundle plus gros -- la bibliothèque (~12-15 Ko gzip) doit être chargée
- Rendu plus lent -- générer les styles à l'exécution coûte du temps
- Complications de SSR -- les styles doivent être extraits et injectés dans le HTML
Qu'est-ce que le CSS-in-JS zero-runtime ?
Le CSS-in-JS zero-runtime est une approche où les styles sont générés au moment de la compilation (build time) -- le code résultant ne contient aucun JavaScript pour gérer les styles. Tu écris les styles en TypeScript, et le bundler les convertit en un fichier CSS classique.
Vanilla-extract -- du stylage TypeScript-first
Vanilla-extract est la bibliothèque zero-runtime la plus populaire. Tu définis les styles dans des fichiers .css.ts :
1// styles.css.ts -- fichier de styles
2import { style } from '@vanilla-extract/css';
3
4export const card = style({
5 background: '#1a1a4e',
6 border: '2px solid #3949ab',
7 borderRadius: '12px',
8 padding: '20px',
9 transition: 'all 0.3s ease',
10 ':hover': {
11 borderColor: '#7c4dff',
12 transform: 'translateY(-2px)',
13 },
14});
15
16export const heading = style({
17 color: '#7c4dff',
18 fontSize: '20px',
19 marginBottom: '16px',
20});Dans le composant React, tu importes les classes comme de simples chaînes :
1// Component.tsx
2import { card, heading } from './styles.css.ts';
3
4function MissionCard({ title, children }) {
5 return (
6 <div className={card}>
7 <h3 className={heading}>{title}</h3>
8 {children}
9 </div>
10 );
11}Après la compilation, card et heading ne sont que des chaînes contenant des noms de classes CSS -- zéro JavaScript à l'exécution.
Sprinkles -- classes utilitaires typées
Vanilla-extract propose sprinkles -- un équivalent typé des classes utilitaires (comme Tailwind, mais avec un typage TypeScript complet) :
1// sprinkles.css.ts
2import { defineProperties, createSprinkles } from '@vanilla-extract/sprinkles';
3
4const responsiveProperties = defineProperties({
5 conditions: {
6 mobile: {},
7 tablet: { '@media': 'screen and (min-width: 768px)' },
8 desktop: { '@media': 'screen and (min-width: 1024px)' },
9 },
10 defaultCondition: 'mobile',
11 properties: {
12 display: ['none', 'flex', 'block', 'grid'],
13 padding: { sm: '8px', md: '16px', lg: '24px' },
14 gap: { sm: '8px', md: '16px', lg: '24px' },
15 },
16});
17
18export const sprinkles = createSprinkles(responsiveProperties);Utilisation :
1<div className={sprinkles({
2 display: { mobile: 'block', tablet: 'grid' },
3 padding: { mobile: 'sm', desktop: 'lg' },
4 gap: 'md',
5})}>Tout est typé -- TypeScript te suggérera les valeurs disponibles et détectera les fautes de frappe.
Linaria -- zero-runtime avec la syntaxe de styled-components
Linaria propose une syntaxe identique à celle de styled-components, mais compile les styles au build time :
1import { styled } from '@linaria/react';
2
3const Card = styled.div`
4 background: #1a1a4e;
5 border: 2px solid #3949ab;
6 border-radius: 12px;
7 padding: 20px;
8 &:hover {
9 border-color: #7c4dff;
10 }
11`;Ça ressemble exactement à styled-components, mais après la compilation, Card affiche <div className="hash123"> -- sans aucun JS à l'exécution.
Panda CSS -- une alternative moderne
Panda CSS est le tout dernier acteur -- il combine l'approche utility-first (comme Tailwind) avec le typage et le zero-runtime :
1import { css } from '../styled-system/css';
2
3function MissionPanel() {
4 return (
5 <div className={css({
6 bg: 'surface',
7 p: '6',
8 borderRadius: 'lg',
9 border: '2px solid',
10 borderColor: 'border',
11 _hover: { borderColor: 'primary' },
12 })}>
13 Mission Panel
14 </div>
15 );
16}Panda génère du CSS atomique (comme Tailwind) au build time -- chaque propriété est une classe distincte, ce qui minimise la duplication.
Comparaison : runtime vs zero-runtime
| Caractéristique | styled-components | Vanilla-extract | Linaria | Panda CSS |
|---|---|---|---|---|
| JS à l'exécution | Oui (~12 Ko) | Non | Non | Non |
| Styles dynamiques | Complet (props) | Limité (recipes) | Limité | Limité |
| TypeScript | Bon | Natif | Bon | Natif |
| SSR | Configuration nécessaire | Sans accroc | Sans accroc | Sans accroc |
| DX (expérience) | Excellente | Bonne | Excellente | Excellente |
Quand choisir le zero-runtime ?
- Grandes applications -- chaque kilo-octet de runtime compte
- SSR (Next.js, Remix) -- aucun problème d'hydratation des styles
- TypeScript-first -- typage complet des valeurs CSS
- Performance -- aucun coût à l'exécution = rendu plus rapide
Code de cette leçon : App.jsx
1import React, { useState } from 'react';
2import './styles.css';
3
4// Comparaison : runtime vs zero-runtime CSS-in-JS
5// Dans Sandpack, on ne peut pas utiliser vanilla-extract/Linaria/Panda,
6// donc on illustre le concept de comparaison
7
8function CodeBlock({ title, code, language }) {
9 return (
10 <div className="code-block">
11 <div className="code-header">
12 <span className="code-lang">{language}</span>
13 <span className="code-title">{title}</span>
14 </div>
15 <pre className="code-content">{code}</pre>
16 </div>
17 );
18}
19
20function ApproachCard({ name, desc, runtime, bundle, typescript, ssr, active, onClick }) {
21 return (
22 <div className={`approach-card ${active ? 'approach-active' : ''}`} onClick={onClick}>
23 <h3 className="approach-name">{name}</h3>
24 <p className="approach-desc">{desc}</p>
25 <div className="approach-badges">
26 <span className={`badge ${runtime === 'Zero' ? 'badge-green' : 'badge-red'}`}>
27 Runtime: {runtime}
28 </span>
29 <span className={`badge ${bundle === 'Minimal' ? 'badge-green' : 'badge-yellow'}`}>
30 Bundle: {bundle}
31 </span>
32 <span className={`badge ${typescript === 'Natif' ? 'badge-green' : 'badge-blue'}`}>
33 TS: {typescript}
34 </span>
35 <span className={`badge ${ssr === 'Sans accroc' ? 'badge-green' : 'badge-yellow'}`}>
36 SSR: {ssr}
37 </span>
38 </div>
39 </div>
40 );
41}
42
43const approaches = [
44 {
45 name: 'Vanilla-extract',
46 desc: 'TypeScript-first, zero-runtime. Styles dans des fichiers .css.ts',
47 runtime: 'Zero', bundle: 'Minimal', typescript: 'Natif', ssr: 'Sans accroc',
48 code: `// styles.css.ts
49import { style } from '@vanilla-extract/css';
50
51export const card = style({
52 background: '#1a1a4e',
53 border: '2px solid #3949ab',
54 borderRadius: '12px',
55 padding: '20px',
56 ':hover': {
57 borderColor: '#7c4dff',
58 },
59});
60
61// Component.tsx
62import { card } from './styles.css.ts';
63<div className={card}>Panel de mission</div>`,
64 },
65 {
66 name: 'Linaria',
67 desc: 'Syntaxe de styled-components, mais zero-runtime',
68 runtime: 'Zero', bundle: 'Minimal', typescript: 'Bon', ssr: 'Sans accroc',
69 code: `import { styled } from '@linaria/react';
70
71const Card = styled.div\`
72 background: #1a1a4e;
73 border: 2px solid #3949ab;
74 border-radius: 12px;
75 padding: 20px;
76 &:hover {
77 border-color: #7c4dff;
78 }
79\`;
80
81// Ressemble à styled-components
82// mais se compile en CSS classique`,
83 },
84 {
85 name: 'Panda CSS',
86 desc: 'Utility-first + zero-runtime. Alternative moderne',
87 runtime: 'Zero', bundle: 'Minimal', typescript: 'Natif', ssr: 'Sans accroc',
88 code: `import { css } from '../styled-system/css';
89
90function Panel() {
91 return (
92 <div className={css({
93 bg: 'surface',
94 p: '6',
95 borderRadius: 'lg',
96 border: '2px solid',
97 borderColor: 'border',
98 _hover: { borderColor: 'primary' },
99 })}>
100 Panel de mission
101 </div>
102 );
103}`,
104 },
105 {
106 name: 'Styled-components (runtime)',
107 desc: 'Styles dynamiques complets, mais au prix de JS à l\'exécution',
108 runtime: '~12Ko', bundle: 'Gros', typescript: 'Bon', ssr: 'Configuration',
109 code: `import styled from 'styled-components';
110
111const Card = styled.div\`
112 background: \${({ theme }) => theme.colors.surface};
113 border: 2px solid \${({ active }) =>
114 active ? '#7c4dff' : '#3949ab'};
115 border-radius: 12px;
116 padding: 20px;
117\`;
118
119// Styles dynamiques complets avec les props
120// mais le JS à l'exécution analyse le template à chaque render`,
121 },
122];
123
124export default function App() {
125 const [selected, setSelected] = useState(0);
126
127 return (
128 <div className="app">
129 <h1 className="heading">Zero-runtime CSS-in-JS</h1>
130 <p className="subtitle">Comparaison des approches : build-time vs runtime</p>
131
132 <div className="grid">
133 {approaches.map((a, i) => (
134 <ApproachCard key={a.name} {...a} active={i === selected} onClick={() => setSelected(i)} />
135 ))}
136 </div>
137
138 <div className="detail-panel">
139 <h2 className="detail-title">{approaches[selected].name} -- exemple de code</h2>
140 <CodeBlock
141 title={approaches[selected].name}
142 language={approaches[selected].name === 'Vanilla-extract' ? 'TypeScript' : 'JSX'}
143 code={approaches[selected].code}
144 />
145 </div>
146 </div>
147 );
148}Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que signifie « zero-runtime » dans le contexte des bibliothèques CSS-in-JS ?
2. Dans quel format de fichier définit-on les styles avec vanilla-extract ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Implémente une comparaison des méthodes de stylage avec une mise en évidence conditionnelle
- Tri horizontal
Ordonne la syntaxe pour importer un style depuis un fichier vanilla-extract dans un composant :
- Éditeur de code
Implémente un tableau comparatif CSS-in-JS avec mise en évidence