Last active
October 21, 2025 21:28
-
-
Save Eptagone/a18be9adabaf8ecc54d1c4e6337c95b2 to your computer and use it in GitHub Desktop.
Revisions
-
Eptagone revised this gist
Apr 3, 2025 . 1 changed file with 22 additions and 16 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -30,24 +30,22 @@ function transformFunctionIntoColor(tokenOrValue: TokenOrValue & { type: "functi /** * Fix oklch colors which are detected as functions instead of colors. */ const FixOklchColorsVisitor = defineVisitor({ Declaration(declaration): ReturnedDeclaration | ReturnedDeclaration[] | void { let needsUpdate = false; if (declaration.property === "custom") { for (let index = 0; index < declaration.value.value.length; index++) { const tokenOrValue = declaration.value.value[index]; if (tokenOrValue?.type === "function" && tokenOrValue.value.name === "oklch") { declaration.value.value[index] = transformFunctionIntoColor(tokenOrValue); needsUpdate = true; } } } if (needsUpdate) { return declaration; } }, }); /** @@ -76,6 +74,14 @@ const ReplacePropertyRulesVisitor = defineVisitor(() => { }]; case "token-list": return component.value; case "percentage": return [{ type: "token", value: { type: "percentage", value: component.value, }, }]; } throw new Error(`Unexpected component type: ${component.type}.\nValue: ${JSON.stringify(component, undefined, 2)}`); -
Eptagone created this gist
Mar 28, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,38 @@ # TailwindCSS v4 Polyfill with LightningCSS This is a custom polyfill created with LightningCSS to use TailwindCSS V4 with older browsers. > This solution is not perfect yet and still have some issues. ## What does this do? This polyfill provides custom **lightningcss** transformers to do the following: - Replace all @property rules with css variables. - Fix all oklch colors being incorrectly detected as functions so lightningcss can process them. See [#809](https://github.com/parcel-bundler/lightningcss/issues/809) - Replace all color variables inside `color-mix` functions with the actual color so lightningcss can transpile it as [described in docs](https://lightningcss.dev/transpilation.html#color-mix). \(The transpilation still fails. Waiting a solution in [#943](https://github.com/parcel-bundler/lightningcss/issues/943)\) ## Usage Example (Astro) ```typescript import solidJs from "@astrojs/solid-js"; import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "astro/config"; import browserslist from "browserslist"; import { browserslistToTargets } from "lightningcss"; import TailwindPolyfillVisitor from "./tailwind.polyfill"; export default defineConfig({ // ... vite: { css: { transformer: "lightningcss", lightningcss: { targets: browserslistToTargets(browserslist(">= 0.01%")), // You can change this according to your needs. visitor: TailwindPolyfillVisitor, // The polyfill visitor goes here }, }, plugins: [tailwindcss()], }, }); ``` This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,178 @@ import { composeVisitors, type CustomAtRules, type Function, type ParsedComponent, type ReturnedDeclaration, type ReturnedRule, type TokenOrValue, type Visitor, } from "lightningcss"; function defineVisitor(visitor: Visitor<CustomAtRules> | (() => Visitor<CustomAtRules>)): Visitor<CustomAtRules> { return typeof visitor === "function" ? visitor() : visitor; } function transformFunctionIntoColor(tokenOrValue: TokenOrValue & { type: "function"; value: Function }): TokenOrValue { // lightness, chroma, hue let [l, c, h, alpha] = tokenOrValue.value.arguments .filter((arg): arg is TokenOrValue & { value: { type: "number"; value: number } } => arg.type === "token" && arg.value.type === "number") .map(arg => arg.value.value); l ??= 0; c ??= 0; h ??= 0; alpha ??= 1; const oklchColor: TokenOrValue = { type: "color", value: { type: "oklch", l, c, h, alpha, }, }; return oklchColor; } /** * Fix oklch colors which are detected as functions instead of colors. */ const FixOklchColorsVisitor = defineVisitor(() => { return { Declaration(declaration): ReturnedDeclaration | ReturnedDeclaration[] | void { let needsUpdate = false; if (declaration.property === "custom") { for (let index = 0; index < declaration.value.value.length; index++) { const tokenOrValue = declaration.value.value[index]; if (tokenOrValue?.type === "function" && tokenOrValue.value.name === "oklch") { declaration.value.value[index] = transformFunctionIntoColor(tokenOrValue); needsUpdate = true; } } } if (needsUpdate) { return declaration; } }, }; }); /** * Replaces all \@property rules with css variables. */ const ReplacePropertyRulesVisitor = defineVisitor(() => { function transformComponentIntoTokensOrValues(component: ParsedComponent): TokenOrValue[] { switch (component.type) { case "color": return [component]; case "length": if (component.value.type !== "value") { throw new Error(`Cannot map component of type: ${component.type}.\nValue: ${JSON.stringify(component, undefined, 2)}`); } return [{ type: "length", value: component.value.value, }]; case "length-percentage": if (component.value.type !== "percentage") { throw new Error(`Cannot map component of type: ${component.type}.\nValue: ${JSON.stringify(component, undefined, 2)}`); } return [{ type: "token", value: component.value, }]; case "token-list": return component.value; } throw new Error(`Unexpected component type: ${component.type}.\nValue: ${JSON.stringify(component, undefined, 2)}`); } let legacyCssVariables: Record<string, TokenOrValue[]> = {}; return { StyleSheet(stylesheet) { const propertyRules = stylesheet.rules.filter(rule => rule.type === "property"); for (const rule of propertyRules) { if (rule.value.initialValue) { legacyCssVariables[rule.value.name] = transformComponentIntoTokensOrValues(rule.value.initialValue); } } }, Rule(rule): ReturnedRule | ReturnedRule[] | void { if (rule.type === "property") { return []; } if (rule.type === "style") { const selectors = rule.value.selectors.flatMap(selector => selector); for (const selector of selectors) { if (selector.type === "pseudo-class" && selector.kind === "root") { for (const [name, value] of Object.entries(legacyCssVariables)) { rule.value.declarations.declarations.push({ property: "custom", value: { name, value }, }); } legacyCssVariables = {}; return rule; } } } }, }; }); /** * Replaces all var(--color-*) with css variables in each color-mix function. */ const ReplaceColorMixVariablesVisitor = defineVisitor(() => { const colorVariables: Record<string, TokenOrValue> = {}; return { Declaration(declaration): ReturnedDeclaration | ReturnedDeclaration[] | void { if (declaration.property === "custom") { if (declaration.value.name.startsWith("--color-")) { for (let index = 0; index < declaration.value.value.length; index++) { const tokenOrValue = declaration.value.value[index]; if (tokenOrValue?.type === "color") { colorVariables[declaration.value.name] = tokenOrValue; } if (tokenOrValue?.type === "function" && tokenOrValue.value.name === "oklch") { colorVariables[declaration.value.name] = transformFunctionIntoColor(tokenOrValue); } } } } }, Function(fun): TokenOrValue | TokenOrValue[] | void { let needsUpdate = false; if (fun.name === "color-mix") { for (let index = 0; index < fun.arguments.length; index++) { const arg = fun.arguments[index]; if (arg?.type === "var") { const value = colorVariables[arg.value.name.ident]; if (value) { needsUpdate = true; // Replace the argument with the color value. fun.arguments[index] = value; // If the next argument is a percentage, add a white-space between them. const nextArg = fun.arguments[index + 1]; if (nextArg?.type === "token" && nextArg.value.type === "percentage") { fun.arguments.splice(index + 1, 0, { type: "token", value: { type: "white-space", value: " " } }); } } } } } if (needsUpdate) { return { type: "function", value: fun, }; } }, }; }); /** * Custom polyfill for TailwindCSS v4. */ const TailwindPolyfillVisitor: Visitor<CustomAtRules> = composeVisitors([ FixOklchColorsVisitor, ReplacePropertyRulesVisitor, ReplaceColorMixVariablesVisitor, ]); export default TailwindPolyfillVisitor;