Skip to content

Instantly share code, notes, and snippets.

@aziis98
Last active October 31, 2025 15:56
Show Gist options
  • Select an option

  • Save aziis98/b5278887db10130f45e28befba3f3a30 to your computer and use it in GitHub Desktop.

Select an option

Save aziis98/b5278887db10130f45e28befba3f3a30 to your computer and use it in GitHub Desktop.
css-extract vite js plugin

css-extract

I really don't like TailwindCSS and recently I discovered that llms are good at writing ViteJS plugins and did some experiments that came out pretty well in opinion.

Note. I know about styled-components, EmotionCSS and vanilla-extract and others but I don't like to write CSS-as-JS and some of the libraries do not support ViteJS or are too much tied to the React ecosystem and can't be used with Preact.


I really like to write CSS by hand but I get that Tailwind gives the ability to write everything directly in the same files keeping related things together. In an ideal world I think I would just ditch HTML+CSS and have the separation at components+layout and colors+styles, or just have a single language like in SwiftUI or Jetpack Compose.

This micro-library gives the best from Tailwind of having the styles directly in the code without having to learn a completely new language and syntax on top of CSS.

<button
    classList={[
      css`
        background-color: blue;
        color: white;
        padding: 10px 20px;
        border: none;
        border-radius: 5px;
        cursor: pointer;

        &:hover {
          background-color: darkblue;
        }
      `,
    ]}
  >
  Click Me
</button>

This is also very composable. Commonly used utilities like the following can also be easily moved to a separate module

const cssFontBold = css`
  font-weight: bold;
`

const cssCardShadow = css`
  box-shadow: 
    0 4px 6px -1px rgb(0 0 0 / 0.1), 
    0 2px 4px -2px rgb(0 0 0 / 0.1);
`

As these are just compiled to class names these can just be used with the classList custom attribute that simply calls directly clsx.

<button classList={[
    css`
      background-color: blue;
      color: white;
      padding: 10px 20px;
      border: none;
      border-radius: 5px;
      cursor: pointert;

      &:hover {
        background-color: darkblue;
      }
    `,
    // just an example of a conditional class
    Math.random() > 0.5 && cssFontBold
  ]}>
  Click Here
</button>

More ideas

  • Instead of hashing use a sequential counter and give a custom uuid to each css snippet

  • Due to the regex extraction technique we can't extract complex template literals with interpolations inside, this would be usefull for example to define multiple variants of a class at the same time. A possible solution could be to have a function as follows

    cosnt prova = css.variant({ 
        regular: { 
            bg: 'gray',
        }, 
        primary: {
            bg: 'royalblue',
        },
    }, ({ bg }) => css`
        font-size: 14px;
        color: #333;
        background: ${bg};
        
        &:hover {
            background: hsl(from ${bg} h s calc(l + 10));
        }
    `)

    Then the lambda inside can be extracted* and evaluated "at compile time" from the vite js plugin environment. Another idea can be to not use interpolation altogether and just generate some css variables for each constant.

    *At this point maybe not with regex extraction but with a fully fledged {j,t}sx? parser.

import type { Rollup } from "vite"
export default function cssCollectorPlugin(): Rollup.Plugin {
const virtualModuleId = "css-extract"
const resolvedVirtualModuleId = "\0" + virtualModuleId
const collectedStyles = new Map()
// Generate a unique class name from CSS content
function generateClassName(cssContent: string) {
const hash = Array.from(cssContent.replace(/\s+/g, " ")).reduce(
(s, c) => (Math.imul(31, s) + c.charCodeAt(0)) | 0,
0
)
return `css-${Math.abs(hash).toString(36)}`
}
// Process CSS and wrap with generated class name
function processCss(cssContent: string) {
const className = generateClassName(cssContent)
// Wrap the CSS with the class selector
const wrappedCss = `.${className} {\n${cssContent}\n}`
return { className, wrappedCss }
}
return {
name: "vite-plugin-css-extract",
// Resolve the virtual module
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
},
// Load the virtual module with the css function
load(id) {
if (id === resolvedVirtualModuleId) {
return `
export function css(strings, ...values) {
// Combine template literal parts
let result = strings[0];
for (let i = 0; i < values.length; i++) {
result += values[i] + strings[i + 1];
}
// Generate hash for caching
const hash = Array.from(result.replace(/\\s+/g, ' '))
.reduce((s, c) => Math.imul(31, s) + c.charCodeAt(0) | 0, 0)
.toString(36);
return 'css-' + hash;
}
`
}
},
// Transform each JS file to extract css`...` calls
transform(code, id) {
// Handle JS files for CSS extraction
if (id.match(/\.(js|jsx|ts|tsx)$/)) {
// Skip node_modules
if (id.includes("node_modules")) {
return null
}
// Regex to match css`...` template literals (including multiline)
const cssRegex = /css`([^`]*)`/gs
let match
let transformedCode = code
// Extract and process each css`` call
while ((match = cssRegex.exec(code)) !== null) {
const cssContent = match[1]
const { className, wrappedCss } = processCss(cssContent)
collectedStyles.set(className, wrappedCss)
// Replace the css`...` call with the class name string
transformedCode = transformedCode.replace(match[0], `'${className}'`)
}
return transformedCode !== code ? transformedCode : null
}
// Handle CSS files with @extracted-css directive
if (id.match(/\.css$/)) {
if (code.includes("@extracted-css")) {
const combinedStyles = Array.from(collectedStyles.values()).join("\n\n")
return code.replace("@extracted-css", combinedStyles)
}
}
return null
},
}
}
declare global {
namespace preact.JSX {
interface HTMLAttributes {
classList?: ClassValue
}
}
}
export {}
import { options } from "preact"
import { clsx } from "clsx"
const originalVnodeHook = options.vnode
// Extend Preact's options to include a "classList={...}" that maps to class={clsx(...)}
options.vnode = vnode => {
const props = vnode.props as any
if (props["classList"]) {
const classList = props["classList"]
delete props["classList"]
props.class = clsx(props.class, classList)
}
if (originalVnodeHook) {
originalVnodeHook(vnode)
}
}
declare module "css-extract" {
import type { ClassValue } from "clsx"
/**
* A template literal tag function for defining CSS styles.
* The CSS will be extracted during build and injected at the @extracted-css directive.
*
* @example
* ```ts
* import { css } from 'css-extract';
*
* const styles = css`
* .button {
* background: blue;
* color: white;
* }
* `;
* ```
*/
export function css(strings: TemplateStringsArray, ...values: any[]): string
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment