Sitemap

CSS in JS en 2026, Tailwind 4 ou Linaria ?

20 min readDec 19, 2025

--

Auteur : Thibaut GILBERT

Press enter or click to view image in full size
Photo de Nathan da Silva sur Unsplash

Je tiens à préciser que cet article a été rédigé entièrement sans IA. En effet, bien qu’offrant un gain de temps dans le processus de rédaction, j’estime que l’utilisation de l’IA dans la démarche rédactionnelle tend à homogénéiser et lisser la mise en forme des articles (et potentiellement leur fond).

Cette fin d’année 2025 m’ayant conduit à jeter un petit regard en arrière, j’ai pris le temps de me pencher sur le sondage “State of CSS 2025” et de mesurer l’étendue du chemin parcouru dans la pratique du développement Web depuis quelques années.

Depuis 2020, les possibilités offertes par le CSS natif ont atteint un niveau tel que l’on peut désormais se passer d’un préprocesseur comme Sass. Il nous est maintenant possible d’intégrer dans nos feuilles de styles les fonctionnalités suivantes (supportées par les principaux navigateurs, mobile compris):

  • pseudo classe :has, qui, en introduisant une sélectivité “bottom up”, permet de styler un élément parent selon son contenu enfant
  • “nesting”, rend plus claire la déclaration de règles imbriquées (une bonne pratique étant de ne pas aller au-delà de 3 niveaux d’imbrication)
  • “at rule” @container, qui permet d’attacher une media query sur un élément et non plus uniquement sur le viewport
  • “at rule” @layer, qui permet de définir notre propre cascade CSS, et de “scoper” les blocs de déclarations CSS associés
  • La fonction clamp() qui donne de la fluidité aux changements de dimensions d’un élément HTML en suivant les changements de dimensions du viewport. Et cela de façon plus subtile qu’avec des medias querries
  • Le positionnement par ancrage (”anchor positionning”) qui lie deux éléments entre eux, quelque soit leurs positions dans le DOM (n’est pas encore disponible sur Firefox)

Ces fonctionnalités incarnent de véritables “ruptures technologiques” dans le milieu de l’intégration web. En offrant plus de marge de manœuvre au développeur dans son travail d’intégration, elles posent la question de la pertinence des frameworks CSS que nous utilisions jusqu’à présent.

Sur ce point justement, l’étude “State of CSS 2025” montre que la tendance observée depuis quelques années se confirme : Tailwind s’impose comme le framework CSS de référence, au détriment de Bootstrap.

On assiste donc à un changement de pratique radical dans le développement CSS, changement qui implique que nos choix technologiques à venir soit suffisamment éclairés pour nous permettre de nous engager sereinement sur nos futurs projets.

C’est dans cette optique que j’ai tenté d’établir un état des lieux des frameworks et des outils CSS qui sont à notre disposition depuis 2010. A’ l’issue de cette présentation je donnerai un avis pratique sur deux de ces solutions, Linaria et Tailwind 4, **en détaillant leurs avantages et leurs inconvénients.

Frameworks “Bootstrap like”, basés sur Sass

Une note sur Sass

A partir de 2010, des préprocesseurs sont venus en surcouche à CSS afin de pallier aux limitations de ce dernier. Sass étant le plus populaire de ces préprocesseurs.

A l’époque Sass*,* en plus d’étendre les possibilités de CSS (nesting, opérateur & …), permet une approche programmatique et surtout modulaire de CSS (via les mixins, les fonctions, les opérateurs logiques… ).

J’ai pu constater que sur des bases de code importantes, le code Sass était peu maintenable, car très difficilement testable (en dépit de quelques tentatives comme *Sassaby* pour adresser ce problème).

Dans le courant des années 2015, la démocratisation des frameworks JavaScript et leur approche de composition, rend inutile l’utilisation de Sass sur le plan de la modularité. Il n’en restait pas moins indispensable, notamment grâce à son “grid system”, permettant le développement de sites web responsives.

En parallèle, l’évolution rapide du CSS natif, dans un premier temps avec flex-box et grid, puis sur des fonctionnalités différentes mais complémentaires (pseudo classe :has, @container …) rendent Sass de facto superflu.

Bootstrap

De mon point de vue, c’est avant tout grâce à son “grid system” et à un ensemble de composants permettant le développement de designs responsives que Bootstrap a acquis une grande notoriété durant les dix premières années de son existence.

Désormais, c’est uniquement pour ses composants “out of the box”, que Bootstrap peut encore être envisagé (à condition d’être prêt à avoir une dépendance Sass dans son projet).

En résumé, voici les points forts/faibles de Bootstrap

Avantages:

  • Grande communauté et support étendu
  • Bootstrap 5 apporte de bons “breaking changes”, notamment en ce qui concerne la customisation des couleurs et la gestion du dark/light mode
  • Accessibilité des composants (de nombreux attributs ARIA sont proposés selon les composants et les comportements que l’on veut rendre accessibles)
  • Agnostique de tout framework JS, l’intégration se faisant en pur HTML et classes CSS

Inconvénients:

  • Repose sur Sass
  • Bootstrap donne un style marqué, qu’on peut identifier immédiatement et qui casse le côté unique que l’on aimerait donner à la web app ou au site web sur lesquels on travaille
  • La configuration des composants Bootstrap se fait via des classes CSS, ce qui est fastidieux. Une petite partie de la customisation peut se faire via variables CSS (”custom properties”), mais pour l’essentiel il faut rester dans Sass

D’autres frameworks inspirés par Bootstrap ont tenté de poser leur patte sur le marché des frameworks CSS, avec plus ou moins de réussite.

On notera notamment Foundation (abandonné depuis https://github.com/foundation/foundation-sites/issues/11767) et Bulma. Bulma est une alternative intéressante à Bootstrap car beaucoup plus légère, mais qui présente le même défaut de reposer sur Sass pour sa configuration.

Frameworks “Tailwind like”, frameworks CSS dits “atomiques”

Une note sur le CSS atomique

Dès sa conception en 1991, le CSS a été pensé comme une couche de présentation venant enrichir la couche structurelle du HTML.

Ce partage des responsabilités présente de nombreux avantages : un HTML sémantique et sans surpoids, portabilité du style, possibilité de définir plusieurs thèmes pour un même site.

Cependant, sur de grandes bases de code ou sur des projets n’ayant jamais consacré de temps à l’architecture de leurs composants web (comme par exemples par la mise en place de bibliothèques de composants, de bonnes pratiques, voire d’un design system), on constate qu’il devient difficile de surcharger des styles, de réutiliser ou de factoriser du CSS.

Le premier à avoir identifié ces limitations est Thierry Koblentz, ingénieur chez Yahoo, qui en 2013 rencontre des difficultés à étendre et modifier sans effets de bords le CSS des différents sites de la marque. Il propose alors l’application du CSS de façon atomique, c’est à dire la définition de blocs de déclaration ne contenant qu’une unique règle CSS. Chacun de ces blocs étant identifié par une seule classe CSS.

Cette granularité extrême du CSS offre ainsi une totale ré-utilisabilité. Les deux autres avantages indéniables de cette approche sont :

  • une prédictibilité absolue (l’ajout d’une classe se traduit obligatoirement par l’application du style associé)
  • pas de code mort, tout le CSS importé dans la page est utilisé

Il tire de ses réflexions un outil, Atomizer, qui définit des classes CSS utilitaires, suivant une nomenclature spécifique. Par exemple :

D(n) -> display: none

Étrangement Atomizer n’a pas rencontré une grande notoriété et il a fallu attendre l’arrivée de son successeur, Tailwind, pour faire émerger la pratique du CSS atomique.

Tailwind

L’arrivée dans la deuxième décennie 2000 de Tailwind, est un changement de paradigme (Tailwind n’est pas pas framework au sens strict, il est ”utility first”) et aussi technique, puisqu’il ne se base pas sur Sass mais sur Postcss (Tailwind a depuis abandonné Postcss au profit de LightningCss).

Avantages:

  • Plus souple, modulaire et extensible que Bootstrap. Ce n’est d’ailleurs pas un framework en soi, mais plus une “utility library” sur laquelle est venue se greffer de nombreuses bibliothèques de composants (comme DaisyUI ou Shadcn)
  • Plus de code mort
  • Rapidité : Tailwind intègre son propre postprocesseur et s’appuie sur LightningCss pour le “transpiling” CSS (notamment pour la prise en charge du “nesting” des blocs de déclarations)
  • Plus besoin de trouver des noms de classes dogmatiquement
  • DX (expérience de développement) facilitée par l’utilisation dans les éditeurs de code de plugins IntelliSense très efficaces (comme Tailwind CSS IntelliSense et Prettier Tailwind dans le cas de VSCode). Ces derniers assurant autocomplétion, coloration syntaxique et linting

Inconvénients:

  • S’engager sur une librairie comme Tailwind, c’est se couper de la couche native CSS en ajoutant un degré d’abstraction
  • HTML pollué par de nombreuses classes CSS. C’est la critique principale que font les contempteurs de Tailwind. Cet inconvénient majeur peut être atténué par l’utilisation de plugins IntelliSense adaptés (voir ci-dessus)
  • Maintenabilité complexe. Les noms de classes peuvent se suivre sur des dizaines de lignes, sans aucune sémantique : le debugging devient alors difficile
  • Temps d’apprentissage, car Tailwind est une sur-couche à CSS. Notamment concernant flexbox
  • Media queries difficiles à lire/maintenir
  • La définition de pseudos-éléments est assez lourde. Tailwind déconseille d’ailleurs l’utilisation des pseudos-elements

Par son design même, Tailwind ne permet pas la réutilisation, ce qui emmène tout de suite à des copier/coller. Cependant, en toute bonne foi, on rétorquera que l’utilisation d’une librairie UI, comme React, permet de pallier à ce problème.

Une note sur les conventions de nommage CSS

Un argument souvent mis en avant en faveur de Tailwind est que ce “framework” dispense le développeur de chercher des noms de classes CSS “sémantiquement” justes.

Personnellement, je trouve cet argument fallacieux, car la réflexion autour du nommage des classes est un travail indispensable, puisqu’il pousse le développeur à se pencher sur la pertinence de la balise choisie et de l’arborescence HTML dans laquelle elle se positionne. Plus généralement cela amène à challenger le design, ce qui ne peut pas être une mauvaise chose.

Avant 2015, le développement web était contraint par la nature globale de CSS : une règle de style CSS injectée dans une page en ciblait son entièreté. Des conventions de nommage furent établies afin de limiter l’empreinte globale de CSS : BEM, SMACSS ou encore OOCSS.

A l’époque, l’apport de de ces conventions à la clarté du code CSS fut réel. Néanmoins, en l’absence de documentation détaillant ces conventions, il était courant de voir des projets commencés en BEM ou SMACSS qui s’étaient petit à petit éloignés des choix faits initialement, ce qui rendait d’autant plus complexe leur compréhension et, in fine, leur maintenabilité.

En introduisant la notion de “local scope” en 2015, Webpack (le “build tool” de référence à l’époque) fut le premier à permettre l’encapsulation de styles CSS dans n’importe quel code JS. Ouvrant ainsi la voie aux librairies comme Angular ou React qui mirent en pratique ce concept de CSS “scopé” au sein de composants.

En 2025, l’utilisation de convention de nommage peut paraître désuète. Il n’en reste pas moins que nous sommes toujours confrontés au même choix de nomenclature, à l’échelle de nos composants, et que dans cette perspective, SMACSS ou OOCSS peuvent nous inspirer.

Je ne recommanderais plus BEM, qui, de part mon expérience, entraîne trop de couplage entre les éléments HTML et est difficilement extensible.

Frameworks “CSS in JS”

Le développement frontend impliquant nécessairement l’utilisation de librairies JS (comme React ou Svelte), on a vu se développer, en parallèle des frameworks CSS décrit ci-dessus, des solutions de “CSS in JS”.

Cette famille d’outils peut être divisée en deux générations successives, la première, qualifiée de CSS in JS au “runtime”, la deuxième de CSS in JS au “buildtime”.

J’évoquerais rapidement la première génération, qui ne doit plus être utilisée, avant d’aborder la seconde en m’appuyant sur un cas concret d’utilisation.

Première génération, “CSS in JS” au runtime

Ces librairies permettent de coupler le CSS aux composants JS. Le CSS est alors “scopé” au sein de chaque composant et il est possible de le variabiliser (theming, media queries …). Les blocs de déclarations CSS sont codés dans des templates strings.

  • Styled-components : le précurseur du CSS in JS. Ne fonctionne qu’avec React
  • Emotion : inspiré de styled-components, il est “framework agnostic”

Pourquoi il faut arrêter de les utiliser

Le code CSS étant inclus dans le bundle JS envoyé au client, c’est le moteur JS du navigateur qui a la charge de parser le CSS et de l’injecter dans le DOM via des balises de styles. Ce qui peut se traduire par de mauvaises performances (voir cette explication détaillée https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b ).

On assiste à un abandon de ces solutions, même venant de leurs “maintainers” :

L’un des arguments les plus à charge contre ces solutions est que le CSS natif est désormais très mature, il permet de dynamiser, variabiliser et scoper les règles CSS.

Pour finir, l’apparition de React Server Component en 2020 (et surtout son implémentation dans NextJS en 2023) casse le paradigme “client side only”.

Seconde génération, “CSS in JS” au buildtime

Afin d’adresser les problèmes posés par les solutions “runtime”, des solutions “buildtime” ont été proposées. Le code CSS est généré en amont et peut donc être servi au client indépendamment du JS.

Le futur de l’intégration CSS passe donc maintenant par ces librairies “build time”. Les plus matures aujourd’hui sont :

  • Linaria
  • Tailwind v4

PandaCSS et UnoCSS sont également intéressantes et méritent d’être abordées dans un autre article 🙂

J’ai monté 2 projets de démonstration qui m’ont permis de tester ces outils, ils sont disponibles sur les repositories Github de SwoodPartners :

Pour chaque “projet”, l’utilisateur a la possibilité de changer de thème CSS par l’appui sur un bouton. 3 thèmes sont disponibles : Finance, Gaming et News (chacun ayant une couleur représentative). En outre le passage en mode sombre ou clair se traduit par une variation dans la luminosité des couleurs. Pour lancer chaque projet :

yarn; yarn dev

Pour chaque librairie je donne d’abord le détail de mon implémentation puis j’indique quels sont les bons et les mauvais points de la solution. Si vous n’avez pas trop de temps devant vous, vous pouvez sauter directement aux conclusions 🙃

Get SWOOD Partners’s stories in your inbox

Join Medium for free to get updates from this writer.

Afin de pouvoir les départager, j’ai défini les critères de comparaison suivants :

  • Facilité de migrer un projet déjà développé en styled-components vers la nouvelle librairie. En effet, la grande majorité des projets sur lesquels je travaille actuellement reposent sur styled-components ou emotion
  • Prise en charge d’un “design system”. C’est à dire que l’on doit pouvoir déclarer des “core tokens” de style, qui seront eux mêmes référencés par des “design/semantic tokens”.
  • Concrètement, pour chaque projet j’ai défini un core token color-accent qui est une couleur de mise en valeur d’un élément UI. Ce token n’a pas vocation à être utilisé directement. A la place il est référencé par un design token color-accent-background-default qui lui peut-être utilisé en tant que couleur de fond sur des composants (par exemple sur le composant Button dans les projets de démonstration)
  • Ce design system peut-être typé, c’est à dire que le CSS répond à une contrainte de “type safety”
  • Pouvoir définir une librairie de composants UI qui héritent de ce design system
  • Ces composants doivent présenter une API permettant de configurer leurs styles. J’ai choisi comme cas de test le fait de pouvoir définir un composant React Button qui accepterait des props de style comme size ou variant.
  • Ces dernières années, un concept a fait ses preuves dans la définition d’API de styles fortement typé, il s’agit de la variance. Le concept a été popularisé par le package npm Class Variance Authority. J’ai donc choisi de juger les 2 solutions à l’aune de leur capacité à mettre en place plus ou moins facilement le concept de variance
  • Possibilité de définir des thèmes
  • Prise en charge des modes dark/light du navigateur
  • “Developper Experience” ou DX : Intellisense dans l’éditeur de code (autocomplétion, linting, coloration syntaxique) et débugging facile dans le navigateur

Linaria

Implémentation:

On peut voir que le point d’entrée de l’application est le fichier main.tsx. Il contient un composant App (contenant le code de l’application proprement dite) et un composant ThemeProvider, qui est un wrapper autour de l’App :

https://github.com/swoodpartners/linaria-css/blob/main/src/main.tsx


import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import { ThemeProvider } from "./ThemeProvider.tsx";

createRoot(document.getElementById("root")!).render(
<StrictMode>
<ThemeProvider>
<App />
</ThemeProvider>
</StrictMode>
);

Chaque thème est un ensemble de variables CSS, chacune de ces variables représentant un token (core ou design). Concrètement, l’ensemble des core tokens est déclaré dans le fichier theme-base.ts, pour être ensuite exposé via des design tokens par chacun des thèmes.

https://github.com/swoodpartners/linaria-css/blob/main/src/theme-base.ts

import { css } from "@linaria/atomic";

export const coreTokens = css`
--purple50: oklch(0.97 0.02 318.4);
--purple800: oklch(0.3 0.15 308.05);
--red50: oklch(0.97 0.014 254.604);
--red800: oklch(0.56 0.15 46.79);
--yellow50: oklch(0.99 0.02 106.68);
--yellow800: oklch(0.86 0.19 110.76);
`;

Par exemple, dans le fichier theme-finance.ts, ce sont les core tokens --purple50 et --purple800 qui sont exposés à travers les design tokens --accent et --accentBackgroundDefault.

https://github.com/swoodpartners/linaria-css/blob/main/src/theme-finance.ts

import { css, cx } from '@linaria/atomic';

import { coreTokens } from './theme-base';

export const financeLightTheme = cx(
coreTokens,
css`
--accent: var(--purple50);
--accentBackgroundDefault: var(--accent);
`
);

export const financeDarkTheme = cx(
coreTokens,
css`
--accent: var(--purple800);
--accentBackgroundDefault: var(--accent);
`
);

C’est la fonction css()(de type “tagged template”) de l’API Linaria qui permet d’englober ces tokens dans des constantes Typescript. Ces variables TS seront ensuite prises en charge par le ThemeProvider (fichier ThemeProvider.tsx). Ce Provider a la responsabilité de récupérer le thème choisi par l’utilisateur (via appui sur le bouton correspondant) et de détecter un changement sur le thème système (sombre ou clair). En fonction de ses contraintes, la fonction getTheme sélectionne la constante TS adéquate, qui est injectée en tant que className dans la div racine du Provider. Ce faisant, les constantes CSS (et donc le thème) sont diffusées dans la page HTML.

https://github.com/swoodpartners/linaria-css/blob/main/src/ThemeProvider.tsx

import type { ReactNode } from "react";
import { useState } from "react";
import { financeDarkTheme, financeLightTheme } from "./theme-finance";
import { gamingDarkTheme, gamingLightTheme } from "./theme-gaming";
import { newsDarkTheme, newsLightTheme } from "./theme-news";
import { useSystemTheme, type SystemTheme } from "./useSystemTheme";

interface ThemeProviderProps {
children: ReactNode;
}

function getTheme(universe: string, systemTheme: SystemTheme) {
switch (universe) {
case "finance":
return systemTheme === "light" ? financeLightTheme : financeDarkTheme;
case "gaming":
return systemTheme === "light" ? gamingLightTheme : gamingDarkTheme;
default:
case "news ":
return systemTheme === "light" ? newsLightTheme : newsDarkTheme;
}
}

export function ThemeProvider({ children }: ThemeProviderProps) {
const [universe, setUniverse] = useState<string>("finance");
const { systemTheme } = useSystemTheme();

const theme = getTheme(universe, systemTheme);

const handleUniverseChange = (universe: string) => {
setUniverse(universe);
};

return (
<div className={theme}>
{children}
<div>
<div>
<button onClick={() => handleUniverseChange("gaming")}>
Gaming Theme
</button>
<button onClick={() => handleUniverseChange("news")}>
News Theme
</button>
<button onClick={() => handleUniverseChange("finance")}>
Finance Theme
</button>
</div>
</div>
</div>
);
}

Sur le plan des composants UI, il faut jeter un oeil du côté du composant Button. La définition de son style est faite dans le fichier Button.style.ts, contenu dans un objet JS littéral, buttonStyles, qui est un agrégat de blocs de styles encapsulés via la fonction css de Linaria.

https://github.com/swoodpartners/linaria-css/blob/main/src/Button/Button.style.ts

import type { ReactNode } from "react";
import { useState } from "react";
import { financeDarkTheme, financeLightTheme } from "./theme-finance";
import { gamingDarkTheme, gamingLightTheme } from "./theme-gaming";
import { newsDarkTheme, newsLightTheme } from "./theme-news";
import { useSystemTheme, type SystemTheme } from "./useSystemTheme";

interface ThemeProviderProps {
children: ReactNode;
}

function getTheme(universe: string, systemTheme: SystemTheme) {
switch (universe) {
case "finance":
return systemTheme === "light" ? financeLightTheme : financeDarkTheme;
case "gaming":
return systemTheme === "light" ? gamingLightTheme : gamingDarkTheme;
default:
case "news ":
return systemTheme === "light" ? newsLightTheme : newsDarkTheme;
}
}

export function ThemeProvider({ children }: ThemeProviderProps) {
const [universe, setUniverse] = useState<string>("finance");
const { systemTheme } = useSystemTheme();

const theme = getTheme(universe, systemTheme);

const handleUniverseChange = (universe: string) => {
setUniverse(universe);
};

return (
<div className={theme}>
{children}
<div>
<div>
<button onClick={() => handleUniverseChange("gaming")}>
Gaming Theme
</button>
<button onClick={() => handleUniverseChange("news")}>
News Theme
</button>
<button onClick={() => handleUniverseChange("finance")}>
Finance Theme
</button>
</div>
</div>
</div>
);
}

Ces blocs sont ensuite couplés pour former une API de variance via la méthode factory cva() du package class-variance-authority. C’est cette API qui est le point d’entrée permettant de configurer le style CSS du Button, via l’attribut className de ce dernier (dans le fichier Button.tsx).

Les propriétés disponibles dans l’API de variance sont exposées à travers les props du composant React : variant et size.

https://github.com/swoodpartners/linaria-css/blob/main/src/Button/Button.tsx

import React from "react";

import { buttonCVA } from "./Button.style";

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* Button contents
*/
children: React.ReactNode;
/**
* Optional click handler
*/
onClick?: () => void;
/**
* Optional className for additional styling
*/
className?: string;
/**
* Button type
*/
type?: "button" | "submit" | "reset";
/**
* Button variant style
*/
variant?: "accent" | "link";
/**
* Button size
*/
size?: "sm" | "lg";
disabled?: boolean;
}

export const Button: React.FC<ButtonProps> = ({
children,
onClick,
type = "button",
className,
variant = "accent",
size = "sm",
disabled,
...props
}) => {
return (
<button
type={type}
className={buttonCVA({ variant, size, className })}
onClick={onClick}
disabled={disabled}
{...props}
>
{children}
</button>
);
};

Avantages:

  • Le CSS de Linaria repose sur des template strings, migrer un projet depuis styled-components est donc très facile. Mais cet avantage est aussi un inconvénient majeur (voir le paragraphe Inconvénients ci-dessous)
  • La mise en place d’un design system (c’est à dire des core/design tokens associés) se fait facilement, via des variables CSS inclues dans des templates strings
  • Appliquée à la création de composants UI, la fonction css atomique de Linaria permet de facilement cloisonner les états de style (accent, link, small …). Couplée à la librairie CVA, la définition d’API de style devient une formalité
  • Linting possible via le package stylelint
  • Projet soutenu par Callstack, qui est un acteur important du monde React/React Native

Inconvénients:

  • Linaria ne peut être utilisé que sur les projets React
  • La prise en charge d’un thème dynamique demande l’implémentation d’un ThemeProvider “custom”. Néanmoins, cette configuration est assez simple et n’a pas d’impact fort sur la complexité de l’application dans son ensemble
  • Grosses limitations sur le plan de la “DX” :
  • Les templates strings empêchent tout typage et toute autocomplétion, la DX est donc considérablement dégradée. Il existe bien une extension VSCode pour activer l’IntelliSense et la coloration syntaxique sur des templates strings mais elle a été archivée : https://github.com/styled-components/vscode-styled-components
  • Les noms de classes atomiques générées sont « encodées », par exemple dans le cas d’un bouton :
<button type="button" class="atm_9s_116y0ak atm_h_1h6ojuz ...">bouton linaria</button>
  • Cela rend fastidieux le débugging dans l’inspecteur du navigateur.

Conclusion sur Linaria:

Bien que permettant de créer et de maintenir assez facilement des thèmes, la faiblesse de Linaria réside dans son utilisation des templates strings, ce qui rend tout typage impossible.

Si le typage n’est pas une prérogative pour vous et que les templates strings ne constituent pas une contrainte, Linaria est un choix mature pour vos projets React web. N’oubliez pas que la librairie permet aussi de migrer presque sans effort depuis styled-components ou emotion.

Tailwind 4

Implémentation:

Dans sa nouvelle mouture, Tailwind privilégie la convention sur la configuration, puisque l’ensemble des variables de thème doivent être définies dans des fichiers CSS, contenues dans une directive CSS propre à Tailwind :@theme. Dans le projet de démo j’ai défini un thème par défaut index.css qui définit 2 variables de theme, --color-accent et --color-accent-background-default. J’ai considéré que --color-accent est un “core token” donc n’a pas vocation à être utilisé directement dans les composants. En revanche, --color-accent-background-default, qui hérite de --color-accent, a vocation à être utilisé, c’est un “design token”.

https://github.com/swoodpartners/tailwind-css/blob/main/src/index.css

@import "tailwindcss";

/* theme news by default */

@theme {
--color-accent: var(--color-green-50);
--color-accent-background-default: var(--color-accent);
}

@media (prefers-color-scheme: dark) {
:root {
--color-accent: var(--color-green-800);
}
}

Dans le nom de variable --color-accent-background-default, le mot clé réservé “color” étant employé, Tailwind va automatiquement générer les classes utilitaires de couleurs associées. Par exemple, pour les besoins de la démo j’utilise la classe CSS bg-accent-background-default (qui permet de spécifier le background color sur un élément). La contrainte est que les variables de thèmes doivent respecter les “namespaces” suivants : https://tailwindcss.com/docs/theme#theme-variable-namespaces.

Le lecteur attentif remarquera la présence d’une autre variable, --color-green-50, qui est une des nombreuses variables exposées “out of the box” par Tailwind.

Le thème par défaut est celui des news, il est défini dans le index.css. Le surchargement des thèmes se fait en important ceux-ci à sa suite. Si on observe le contenu du thème “Gaming” (theme-gaming.css), on peut voir que les 2 variables de style Tailwind décrites précédemment sont surchargées au niveau du root scope, à la condition qu’une balise parente de l’arbre HTML contienne un attribut data-theme="gaming".

https://github.com/swoodpartners/tailwind-css/blob/main/src/assets/theme-gaming.css

[data-theme="gaming"] {
--color-accent: var(--color-red-50);
--color-accent-background-default: var(--color-accent);
}

@media (prefers-color-scheme: dark) {
[data-theme="gaming"] {
--color-accent: var(--color-red-800);
}
}

Cet ajout dynamique d’un “data attribute” data-theme sur la balise <html> est géré en JS dans l’App.tsx de façon très triviale.

Côté composants, j’ai fait le choix de reposer entièrement sur la librairie Tailwind Variants. Comme son nom l’indique, cette librairie permet de spécifier un nombre déterminé de variantes, en couplant les classes utilitaires tailwind (comme text-sm ou bg-accent-background-default). De manière identique à CVA, Tailwind Variants (via sa method tv()), permet :

  • de spécifier un style de base
  • de définir des variantes, comme color, size
  • de faire de la composition de variantes

L’objet retourné par tv() est une “factory method” (dans notre exemple buttonStyle) qui prend en paramètre un objet de configuration respectant l’API des variantes. API qui sera typée via l’utilitaire VariantProps proposé par Tailwind Variant. Dans la terminologie de Tailwind Variant, ces méthodes sont appelées des “components”.

https://github.com/swoodpartners/tailwind-css/blob/main/src/Button/Button.style.ts

import { tv, type VariantProps } from "tailwind-variants";

export const buttonStyle = tv({
base: "font-medium bg-blue-500 text-white rounded-full active:opacity-80",
variants: {
color: {
primary: "bg-blue-500 text-white",
secondary: "bg-purple-500 text-white",
},
size: {
sm: "text-sm",
md: "text-base",
lg: "px-4 py-3 text-lg",
},
variant: {
accent: "bg-accent-background-default",
},
},
compoundVariants: [
{
size: ["sm", "md"],
class: "px-3 py-1",
},
],
defaultVariants: {
size: "md",
color: "primary",
},
});

export type ButtonVariants = VariantProps<typeof buttonStyle>;

A’ l’utilisation, le choix des variantes se fait en passant l’objet de configuration désiré à la props className du composant Button.

https://github.com/swoodpartners/tailwind-css/blob/main/src/Button/Button.tsx

import React from "react";
import { buttonStyle, type ButtonVariants } from "./Button.style";

export interface ButtonProps {
/**
* Button contents
*/
children: React.ReactNode;
/**
* Optional click handler
*/
onClick?: () => void;
/**
* Optional className for additional styling
*/
className?: ButtonVariants;
/**
* Button type
*/
type?: "button" | "submit" | "reset";
disabled?: boolean;
}

/**
* Primary UI component for user interaction
*/
export const Button: React.FC<ButtonProps> = ({
children,
onClick,
type = "button",
className,
disabled,
...props
}) => {
return (
<button
type={type}
className={buttonStyle(className)}
onClick={onClick}
disabled={disabled}
{...props}
>
{children}
</button>
);
};

Note: concernant les avantages/inconvénients propres à Tailwind je renvoie le lecteur à la section dédiée plus haut dans cet article. Les points abordés ci-dessous sont spécifiques aux contraintes posées par mon projet de test.

Avantages :

  • Solution la plus répandue actuellement, avec une grande communauté de développeurs
  • Fonctionne pour n’importe quel framework JS
  • Dans sa version 3, Tailwind reposait sur un fichier de configuration Javascript pour la customisation des variables et la mise en place d’un design system. Avec sa nouvelle version, Tailwind ne demande plus aucune configuration JS. A la place il suffit de s’appuyer sur des variables CSS dont le nommage suit une convention spécifique. Ce choix de privilégier la convention sur la configuration est un gros avantage en terme de maintenabilité du code
  • La détection des modes dark/light se fait nativement en CSS, via la media query prefers-color-scheme
  • Tailwind est livré avec un ensemble de variables préconfigurées, comme --color-green-50, qui permet de commencer tout de suite l’intégration et de surcharger progressivement les styles
  • Concernant la contrainte de pouvoir créer (et maintenir facilement) des composants UI, Tailwind Variants offre des fonctionnalités supplémentaires par rapport à CVA, qui sont présentées sur leur site : https://www.tailwind-variants.org/docs/comparison. On retiendra la possibilité d’organiser les styles de composants UI complexes en “slots” et d’étendre des “components” TV déjà définis
  • Du point de vue de la DX :
  • Les noms de classes atomiques générées sont « non encodées », ce qui rend possible l’édition de ces classes à la volée dans le “dev tool” du navigateur
  • Tailwind Variants s’intègre “out of the box” avec les plugins Tailwind pour VSCode

Inconvénients:

  • Faire migrer un projet de styled-components vers Tailwind est une tâche complexe et fastidieuse, l’implémentation du CSS étant radicalement différente entre les deux outils. Cependant l’usage d’un agent IA peut grandement faciliter cette migration
  • La mise en place d’un design system et des différents thèmes se faisant via des variables CSS au sein de fichiers CSS, tout typage est impossible
  • On l’a vu, les core/design tokens sont représentés par des variables de thèmes Tailwind, qui doivent obligatoirement obéir à des namespaces spécifiques (comme --color-* ou --breakpoint-*). Or, dans la pratique, le design system et ses tokens (qui sont fournis par les équipes de designers travaillant dans Figma) respectent une nommenclature plus générique qui ne correspond pas aux namespaces Tailwind. Une phase de “mapping” entre les tokens en sortie de Figma et les variables Tailwind est donc nécessaire

Conclusion sur Tailwind 4:

Choisir Tailwind 4 pour son futur projet, c’est choisir la solution CSS in JS ayant la DX la plus aboutie du marché. Reposant sur un système de theming 100% CSS, elle permet de développer rapidement une librarie de composants UI respectant un design system.

En revanche, la migration d’un projet développé en styled-components vers Tailwind demandera beaucoup d’efforts. La mise en place d’une stratégie de non-régression reposant sur des outils de testing visuel comme https://main.vitest.dev/guide/browser/visual-regression-testing est alors primordiale.

Sources et références

https://www.aleksandrhovhannisyan.com/blog/why-i-dont-like-tailwind-css/#why-tailwind-isnt-worth-your-time

https://www.youtube.com/watch?v=lIUp8bdKiq4

https://www.youtube.com/watch?v=lHZwlzOUOZ4

https://2024.stateofcss.com/en-US/tools/#css_frameworks

https://prismic.io/blog/best-css-frameworks

https://w3techs.com/

https://www.alsacreations.com/article/lire/1884-deux-annees-avec-tailwind-quel-bilan.html

https://dev.to/srmagura/why-were-breaking-up-wiht-css-in-js-4g9b

https://dev.to/worldoftheweb/is-css-the-new-sass-heres-what-you-need-to-know-in-2025-4254

https://www.smashingmagazine.com/2013/10/challenging-css-best-practices-atomic-approach/

https://medium.com/@wmukhtar/master-css-naming-conventions-in-2025-bem-oocss-smacss-suit-css-and-beyond-c3afe583c92b

https://medium.com/seek-blog/the-end-of-global-css-90d2a4a06284

--

--

SWOOD Partners
SWOOD Partners

Written by SWOOD Partners

SWOOD Partners est la première société de conseil à réserver la moitié de son capital pour ses salariés. swood.fr