Pourquoi color-mix() change la façon de penser les couleurs
Dans beaucoup de projets web, la palette CSS finit par ressembler à un inventaire difficile à maintenir : --blue-100, --blue-200, --blue-300, --button-hover, --button-active, --card-[border](/articles/css-style-page-web), --card-background-muted, puis les mêmes variantes pour le thème sombre. Ce modèle fonctionne, mais il devient vite lourd dès qu’une interface grandit.
La fonction CSS color-mix() permet de calculer une couleur à partir de deux autres couleurs, directement dans le navigateur. Concrètement, au lieu de déclarer manuellement toutes les nuances d’une couleur de marque, on peut définir quelques couleurs de base, puis dériver les états, arrière-plans, bordures et survols avec une règle explicite.
Ce sujet complète naturellement une approche basée sur les design tokens en CSS et TypeScript, mais il va plus loin sur un point précis : la génération de variantes visuelles à partir d’une intention, plutôt que l’accumulation de valeurs figées.
La syntaxe de base de color-mix()
La syntaxe la plus courante consiste à indiquer un espace colorimétrique, puis deux couleurs à mélanger :
.badge {
background: color-mix(in oklch, var(--color-primary) 12%, white);
color: var(--color-primary);
}
Ici, le navigateur mélange --color-primary avec white. Le pourcentage indique le poids de la première couleur. Plus ce pourcentage est faible, plus le résultat sera proche du blanc.
L’espace oklch est généralement un bon choix pour les interfaces modernes, car il produit des variations souvent plus perceptuellement régulières que des mélanges en srgb. Pour des besoins simples, srgb reste compréhensible, mais oklch donne souvent des dégradés et variantes plus équilibrés.
:root {
--color-primary: oklch(58% 0.18 255);
--color-surface: white;
--color-text: oklch(22% 0.02 255);
--primary-bg-subtle: color-mix(in oklch, var(--color-primary) 10%, var(--color-surface));
--primary-border-subtle: color-mix(in oklch, var(--color-primary) 28%, var(--color-surface));
--primary-hover: color-mix(in oklch, var(--color-primary) 88%, black);
}
On obtient ainsi une petite grammaire visuelle : une couleur principale, une surface, un texte, puis des dérivés sémantiques.
Remplacer les variantes manuelles par des tokens calculés
Dans un design system, il est préférable d’éviter les noms trop liés à l’apparence brute. Un token comme --blue-200 décrit une couleur, mais pas son usage. Un token comme --button-primary-background-hover décrit une intention d’interface.
Avec color-mix(), on peut combiner les deux niveaux : une base stable et des alias sémantiques calculés.
:root {
--brand: oklch(61% 0.2 260);
--surface: oklch(99% 0.01 260);
--text: oklch(20% 0.03 260);
--button-bg: var(--brand);
--button-bg-hover: color-mix(in oklch, var(--brand) 90%, black);
--button-bg-active: color-mix(in oklch, var(--brand) 78%, black);
--button-border: color-mix(in oklch, var(--brand) 80%, black);
--button-focus-ring: color-mix(in oklch, var(--brand) 45%, var(--surface));
}
.button {
border: 1px solid var(--button-border);
border-radius: 0.75rem;
background: var(--button-bg);
color: white;
padding: 0.75rem 1rem;
font-weight: 700;
}
.button:hover {
background: var(--button-bg-hover);
}
.button:active {
background: var(--button-bg-active);
}
.button:focus-visible {
outline: 3px solid var(--button-focus-ring);
outline-offset: 3px;
}
L’intérêt n’est pas seulement esthétique. Le code rend explicite la relation entre les couleurs. Lorsqu’on change --brand, les variantes suivent automatiquement. Cette logique s’intègre bien avec des architectures CSS plus structurées, notamment celles qui utilisent les cascade layers CSS pour séparer tokens, composants et utilitaires.
Construire un thème sombre sans tout redéclarer
Le thème sombre est un bon cas d’usage. Sans color-mix(), on duplique souvent une grande partie de la palette. Avec des couleurs calculées, on peut changer les fondations et laisser les dérivés se recalculer.
:root {
color-scheme: light;
--brand: oklch(60% 0.18 250);
--surface: oklch(99% 0.01 250);
--surface-elevated: color-mix(in oklch, var(--brand) 4%, var(--surface));
--text: oklch(22% 0.025 250);
--border: color-mix(in oklch, var(--text) 16%, var(--surface));
}
[data-theme="dark"] {
color-scheme: dark;
--surface: oklch(18% 0.025 250);
--surface-elevated: color-mix(in oklch, var(--brand) 10%, var(--surface));
--text: oklch(94% 0.015 250);
--border: color-mix(in oklch, var(--text) 18%, var(--surface));
}
.card {
background: var(--surface-elevated);
border: 1px solid var(--border);
color: var(--text);
border-radius: 1rem;
padding: 1.25rem;
}
On ne cherche pas à faire disparaître tous les tokens. Un thème robuste a toujours besoin de décisions explicites. Mais color-mix() limite les valeurs arbitraires et facilite les ajustements progressifs.
Générer des variantes de composants
Prenons un composant d’alerte. Il peut exister en plusieurs variantes : information, succès, avertissement, erreur. Sans calcul CSS, on déclare souvent trois ou quatre valeurs par variante. Avec color-mix(), une seule couleur d’accent peut suffire.
.notice {
--notice-accent: oklch(58% 0.18 250);
border: 1px solid color-mix(in oklch, var(--notice-accent) 35%, var(--surface));
background: color-mix(in oklch, var(--notice-accent) 12%, var(--surface));
color: var(--text);
border-radius: 0.875rem;
padding: 1rem;
}
.notice[data-variant="success"] {
--notice-accent: oklch(62% 0.16 150);
}
.notice[data-variant="warning"] {
--notice-accent: oklch(72% 0.17 80);
}
.notice[data-variant="danger"] {
--notice-accent: oklch(58% 0.2 30);
}
.notice__title {
color: color-mix(in oklch, var(--notice-accent) 80%, var(--text));
font-weight: 700;
}
Le composant devient plus simple à étendre : ajouter une variante revient à définir --notice-accent. C’est particulièrement utile dans des bibliothèques de composants, des applications SaaS ou des interfaces administrables.
Exemple avec React et TypeScript
Dans une application React, on peut exposer certaines couleurs sous forme de variables CSS locales. Cette approche évite de générer des classes dynamiques complexes, tout en gardant la logique de rendu lisible.
type MetricCardProps = {
title: string;
value: string;
accent: string;
};
export function MetricCard({ title, value, accent }: MetricCardProps) {
return (
<article
className="metric-card"
style={{ "--metric-accent": accent } as React.CSSProperties}
>
<p className="metric-card__title">{title}</p>
<strong className="metric-card__value">{value}</strong>
</article>
);
}
.metric-card {
border: 1px solid color-mix(in oklch, var(--metric-accent) 30%, var(--surface));
background: linear-gradient(
135deg,
color-mix(in oklch, var(--metric-accent) 14%, var(--surface)),
var(--surface)
);
border-radius: 1rem;
color: var(--text);
padding: 1rem;
}
.metric-card__title {
color: color-mix(in oklch, var(--metric-accent) 65%, var(--text));
margin: 0 0 0.5rem;
}
.metric-card__value {
font-size: 2rem;
}
Cette technique reste compatible avec une architecture React classique. Elle peut aussi cohabiter avec des composants serveur, à condition de garder en tête les frontières entre rendu serveur et comportement client, comme expliqué dans l’article sur les React Server Components.
Attention au contraste et à l’accessibilité
color-mix() ne garantit pas automatiquement un contraste suffisant. Une couleur calculée peut être élégante mais illisible, notamment sur des arrière-plans subtils ou en thème sombre.
Il faut donc tester les combinaisons importantes : texte principal, texte secondaire, bouton, focus ring, états d’erreur. Pour les interfaces de formulaire, cette vigilance rejoint les principes abordés dans les formulaires accessibles : la couleur peut renforcer une information, mais elle ne doit pas être le seul moyen de la comprendre.
Une règle simple : utilisez color-mix() pour générer des surfaces, bordures, arrière-plans et états visuels, mais gardez des tokens explicites pour les couleurs de texte critiques.
:root {
--danger: oklch(58% 0.22 30);
--danger-bg: color-mix(in oklch, var(--danger) 12%, white);
--danger-border: color-mix(in oklch, var(--danger) 35%, white);
--danger-text: oklch(36% 0.18 30);
}
.error-message {
background: var(--danger-bg);
border-left: 4px solid var(--danger-border);
color: var(--danger-text);
padding: 0.75rem 1rem;
}
Prévoir un fallback raisonnable
Pour une base de compatibilité large, vous pouvez déclarer une couleur de secours avant la valeur calculée. Les navigateurs qui comprennent color-mix() utiliseront la seconde déclaration ; les autres garderont la première.
.tag {
background: #eef2ff;
background: color-mix(in oklch, var(--brand) 12%, white);
border-color: #c7d2fe;
border-color: color-mix(in oklch, var(--brand) 30%, white);
}
Ce principe s’inscrit dans une logique de progressive enhancement : fournir une expérience correcte par défaut, puis améliorer l’interface lorsque le navigateur dispose des capacités nécessaires.
Bonnes pratiques à retenir
Utilisez color-mix() pour exprimer des relations entre couleurs, pas pour masquer une palette désorganisée. Une bonne stratégie consiste à définir peu de couleurs sources, à nommer clairement les tokens sémantiques, puis à calculer les états secondaires.
Évitez aussi les mélanges trop profonds comme color-mix() dans color-mix() dans color-mix(). Le code devient difficile à lire et les effets de bord visuels se multiplient. Si une couleur est centrale dans votre interface, donnez-lui un nom.
Enfin, documentez vos conventions : espace colorimétrique choisi, pourcentages recommandés pour les hover states, surfaces, bordures et focus rings. C’est ce qui transforme une astuce CSS en véritable outil d’architecture frontend.
Conclusion
color-mix() ne remplace pas le travail de direction artistique ni les tests d’accessibilité, mais elle rend les systèmes de couleurs plus cohérents, plus expressifs et plus faciles à maintenir. Pour un site, une application métier ou un design system, c’est un excellent moyen de réduire la duplication sans perdre le contrôle.
En pratique, commencez petit : une couleur de marque, une surface, deux ou trois variantes calculées. Puis observez si vos composants deviennent plus simples à faire évoluer. C’est souvent là que color-mix() montre sa vraie valeur : moins de valeurs magiques, plus d’intentions lisibles.