Skip to content

Instantly share code, notes, and snippets.

@Eptagone
Last active October 21, 2025 21:28
Show Gist options
  • Select an option

  • Save Eptagone/a18be9adabaf8ecc54d1c4e6337c95b2 to your computer and use it in GitHub Desktop.

Select an option

Save Eptagone/a18be9adabaf8ecc54d1c4e6337c95b2 to your computer and use it in GitHub Desktop.

Revisions

  1. Eptagone revised this gist Apr 3, 2025. 1 changed file with 22 additions and 16 deletions.
    38 changes: 22 additions & 16 deletions tailwind.polyfill.ts
    Original 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(() => {
    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;
    }
    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;
    }
    },
    };
    }
    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)}`);
  2. Eptagone created this gist Mar 28, 2025.
    38 changes: 38 additions & 0 deletions README.md
    Original 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()],
    },
    });
    ```
    178 changes: 178 additions & 0 deletions tailwind.polyfill.ts
    Original 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;