Transparente Farben für alte Browser – ein kleines PostCSS-Plugin
Tailwind v4 baut Transparenzen mit color-mix(). Ältere Browser bekommen dann die volle Farbe, und aus dezenten Flächen werden schwarze Blöcke. So habe ich das repariert.
#css#tailwind#kompatibilität
Diese Seite ist bewusst monochrom: Schwarz, Weiß und viele Abstufungen dazwischen. Die Abstufungen entstehen fast alle über Transparenz – bg-foreground/5 für eine kaum sichtbare Fläche, border-foreground/10 für eine feine Linie.
In einem aktuellen Browser sieht das genau so aus wie gedacht. Auf einem älteren iPhone dagegen wurden aus diesen dezenten Flächen tiefschwarze Blöcke.
Was Tailwind v4 daraus macht
Tailwind v4 baut Farben mit Transparenz über color-mix(). Aus bg-foreground/10 wird (vereinfacht) das hier:
.bg-foreground\/10 {
background-color: var(--foreground);
}
@supports (color: color-mix(in lab, red, red)) {
.bg-foreground\/10 {
background-color: color-mix(in oklab, var(--foreground) 10%, transparent);
}
}Die Idee: Wer color-mix() kennt, bekommt die Transparenz. Alle anderen bekommen einen Fallback – und dieser Fallback ist die volle Farbe. Bei einer Akzentfarbe ist das verschmerzbar. Bei --foreground heißt es: 100 % Schwarz statt 10 %.
Betroffen sind alle Browser ohne color-mix(): Safari vor 16.2 (also auch iPhones mit iOS 15), Chrome vor 111 und Firefox vor 113.
Die Idee: Farbkanäle statt fertiger Farben
Transparenz ohne color-mix() geht seit Jahren mit rgb() und Schrägstrich – vorausgesetzt, man kennt die einzelnen Farbkanäle. Deshalb gibt es zu jeder Grundfarbe im Design-System eine zweite Variable nur mit den Kanälen:
:root {
--foreground: #0a0a0a;
--foreground-rgb: 10 10 10;
}
html.dark {
--foreground: #fafafa;
--foreground-rgb: 250 250 250;
}Damit lässt sich jeder Fallback umschreiben:
/* vorher */ background-color: var(--foreground);
/* nachher */ background-color: rgb(var(--foreground-rgb) / 10%);Das funktioniert in allem, was ich unterstützen will – und schaltet beim Theme-Wechsel automatisch mit um.
Das Plugin
Von Hand geht das natürlich nicht: Tailwind erzeugt diese Regeln für jede Kombination, die im Code vorkommt. Also läuft nach Tailwind ein kleines eigenes PostCSS-Plugin. Es sucht jeden @supports-Block mit color-mix(), liest daraus Variable und Prozentwert und schreibt die Fallback-Regel davor um:
const MIX = /color-mix\(\s*in\s+[a-z-]+\s*,\s*var\((--[\w-]+)\)\s*([\d.]+)%\s*,\s*transparent\s*\)/g
const toRgb = (value) => value.replace(MIX, (_, name, percent) => `rgb(var(${name}-rgb) / ${percent}%)`)
const legacyColors = () => ({
postcssPlugin: "legacy-color-mix-fallback",
OnceExit(root) {
root.walkAtRules("supports", (atRule) => {
if (!atRule.params.includes("color-mix")) return
atRule.walkDecls((decl) => {
if (!MIX.test(decl.value)) return
MIX.lastIndex = 0
// Tailwinds Fallback-Regel steht direkt vor dem @supports-Block …
// … und bekommt die rgb()-Variante statt der vollen Farbe
})
})
},
})Der vollständige Code hat noch zwei Feinheiten:
- Zusammengefasste Regeln. Tailwind fasst Selektoren mit gleichem Inhalt zusammen, etwa
.bg-foreground, .bg-foreground\/10. Dort darf der Wert natürlich nicht für beide geändert werden. Das Plugin löst den betroffenen Selektor heraus und gibt ihm eine eigene Regel. - Eigenes CSS. Wo ich selbst
color-mix()schreibe, setzt das Plugin automatisch eine Fallback-Deklaration davor.
Fehlt zu einer Farbe die Kanal-Variable, wird die Deklaration ungültig, und die Fläche bleibt einfach transparent. Schlimmer als „kein Effekt“ kann es also nie werden.
Ein Stolperstein mit Turbopack
Eingebunden wird das Plugin in der postcss.config.mjs. Ein relativer Pfad wie ./postcss-legacy-colors.mjs hat unter Turbopack nicht funktioniert – der Build brach mit einem Fehler ab. Mit einem absoluten Pfad über process.cwd() klappt es:
import { join } from "node:path"
export default {
plugins: {
"@tailwindcss/postcss": {},
[join(process.cwd(), "postcss-legacy-colors.mjs")]: {},
},
}Was sonst noch dazugehört
Transparente Farben waren das auffälligste, aber nicht das einzige Problem älterer Browser. Ein paar weitere Fallbacks, die jetzt im CSS stecken:
min-height: 100svhmit100vhdavor – ältere Browser kennen die neuen Viewport-Einheiten nicht.overflow-x: clipmithiddendavor – Safari unterstütztcliperst ab Version 16.- Glas-Effekte mit
backdrop-filternur innerhalb von@supports. Ohne Unterstützung gibt es stattdessen einen fast deckenden Hintergrund. - Eine
browserslistin derpackage.json, damit Autoprefixer weiß, für wen er Präfixe schreiben muss.
Ergebnis
Nach dem Build bleibt kein einziger Fallback mit voller Farbe übrig. Auf aktuellen Browsern ändert sich nichts. Ältere bekommen dieselben feinen Abstufungen statt schwarzer Blöcke. Und im Code schreibe ich weiterhin ganz normal bg-foreground/10.