npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vanillify

v1.1.0

Published

Tailwind CSS to vanilla CSS converter powered by Tailwind CSS and oxc-parser

Downloads

36

Readme


What is Vanillify?

Vanillify converts Tailwind utility classes in JSX/TSX files to vanilla CSS. It uses Tailwind v4's native compile().build() API for real CSS output and oxc-parser for proper AST-based class extraction.

No regex. No lookup tables. The CSS you get is generated by Tailwind's own compilation pipeline — the same engine that powers production builds, not a hand-maintained mapping that drifts from reality.

Before / After

Input - button.tsx

import { component$ } from "@qwik.dev/core";

export const Button = component$(() => {
  return (
    <button class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600">
      Click me
    </button>
  );
});

Output - button.vanilla.tsx

import { component$ } from "@qwik.dev/core";
import "./button.vanilla.css";

export const Button = component$(() => {
  return <button class="node0">Click me</button>;
});

Output - button.vanilla.css

.node0 {
  border-radius: 0.5rem;
  background-color: rgb(59, 130, 246);
  padding-left: 1rem;
  padding-right: 1rem;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  color: rgb(255, 255, 255);
}
.node0:hover {
  background-color: rgb(37, 99, 235);
}

Each element gets an indexed class name (.node0, .node1, ...) with its full CSS isolated per node.

Installation

npm install vanillify

Use Cases

  • Framework examples - Generate vanilla CSS versions of your components for documentation, tutorials, or sharing across Qwik, React, Preact, Solid and other frameworks without requiring Tailwind as a dependency
  • Migrate away from Tailwind - Convert an entire project from Tailwind to vanilla CSS, producing portable stylesheets you can take anywhere, no build tool lock-in
  • Audit your styles - See exactly what CSS your Tailwind classes produce, useful for debugging specificity issues or understanding generated output
  • Ship framework-agnostic libraries - Strip Tailwind from your component library before publishing so consumers don't need Tailwind in their stack

Semantic Naming with LLMs

Right now vanillify uses indexed class names (.node0, .node1, ...). The intention is to support hooking into LLM models to generate semantic class names automatically, so .node0 becomes .primary-button or .card-header based on the component context.

This would let you go from Tailwind utility classes to clean, human-readable CSS without manually naming anything.

Local models, such as Gemma 4B can be used in workflows at no cost.

Usage - Programmatic API

import { convert } from "vanillify";

const source = `import { component$ } from "@qwik.dev/core";

export const Card = component$(() => {
  return <div class="flex p-4 bg-white rounded-lg">Hello</div>
});`;

const { component, css, warnings } = await convert(source, "Card.tsx");

| Return field | Description | | ------------ | ----------------------------------------------------------------------- | | component | Rewritten source with indexed class names and a CSS import | | css | Generated vanilla CSS | | themeCss | Extracted :root CSS variables from theme layer | | warnings | Array of { type, message, location } for dynamic or unmatched classes |

convert() is a pure async function - no file I/O, no side effects.

Options

Pass any Tailwind CSS directives via the css option — Tailwind handles @theme, @custom-variant, and all other directives natively:

const result = await convert(source, "Card.tsx", {
  css: `
    @theme { --color-brand: #ff0000; }
    @custom-variant ui-open (&[data-open]);
    @custom-variant ui-checked (&[data-checked]);
  `,
});

CSS Modules

const result = await convert(source, "Card.tsx", {
  outputFormat: "css-modules",
});

// result.component uses {styles.node0} instead of "node0"
// result.css is identical (use as .module.css)
// result.classMap maps indexed names

Usage - CLI

# Convert a single file
npx vanillify "src/Button.tsx"

# Convert with glob patterns
npx vanillify "src/components/*.tsx"

# Specify output directory
npx vanillify "src/Button.tsx" -o dist

# Pass CSS with @theme, @custom-variant, etc.
npx vanillify "src/**/*.tsx" -c tailwind.css

# CSS Modules output
npx vanillify "src/**/*.tsx" -f css-modules

| Flag | Alias | Description | | ---------- | ----- | -------------------------------------------------------------- | | --outDir | -o | Output directory (default: alongside input files) | | --css | -c | Path to CSS file with @theme, @custom-variant, etc. | | --format | -f | Output format: vanilla (default) or css-modules |

The CLI outputs .vanilla.css and .vanilla.tsx (or .vanilla.jsx) files, preserving the original file extension. With --format css-modules, it outputs .module.css and .module.tsx instead.

How It Works

Vanillify runs a four-step pipeline:

  1. Parse - JSX/TSX source is parsed into an AST using oxc-parser (Rust-powered, the same parser behind Rolldown)
  2. Extract - className and class attribute values are extracted by walking the AST with oxc-walker. Dynamic expressions with string literals are rewritten; unresolvable parts emit warnings.
  3. Generate - Extracted class names are fed to Tailwind v4's native compile().build() API to produce real CSS
  4. Rewrite - The original source is rewritten with indexed class names (.node0, .node1, ...) and a CSS import is prepended

Each step is a pure function. The full pipeline is a single await convert(source, filename) call.

Dynamic Patterns

Vanillify v1.1 rewrites string literals inside dynamic class expressions. Each recognized string literal gets a scoped class name and CSS — the rest of the expression structure is preserved unchanged.

Supported Patterns

Ternary expressions — both branches are rewritten:

class={active ? "flex gap-4" : "hidden"}
// becomes: class={active ? "node0" : "node1"}

Logical AND expressions — the string literal on the right-hand side is rewritten:

className={isOpen && "p-4 bg-white"}
// becomes: className={isOpen && "node0"}

Function call arguments (clsx, cn, classnames, etc.) — all string literal args are rewritten:

className={clsx("flex", "gap-4")}
// becomes: className={clsx("node0", "node1")}

Object keys (quoted) — quoted string keys in function call arguments are rewritten:

className={clsx({ "flex gap-4": isActive })}
// becomes: className={clsx({ node0: isActive })}

Object keys (unquoted identifiers) — identifier keys that are valid Tailwind tokens are rewritten:

className={clsx({ hidden: cond })}
// becomes: className={clsx({ node0: cond })}

twMerge unwrappingtwMerge() calls are unwrapped and the import is removed if unused:

className={twMerge("flex gap-4")}
// becomes: className={"node0"}  (import removed if no other references)

className={twMerge("flex", "gap-4")}
// becomes: className={"node0"}  (args joined, single scoped name)

twMerge limitations:

  • Only twMerge imported from "tailwind-merge" is detected. Aliased imports (import { twMerge as tm }) work, but re-exports through your own utility file (import { tm } from "./utils") are not traced.
  • All arguments must be string literals. If any argument is a variable or expression (e.g., twMerge("flex", someVar)), the entire call is left unchanged — no partial unwrapping.
  • Only twMerge calls directly inside a className/class attribute are unwrapped. Calls assigned to variables (const cls = twMerge(...)) are not affected.
  • The tailwind-merge import is removed only when no other references to the imported name remain anywhere in the file.

CSS Modules — all patterns above work with outputFormat: "css-modules". The syntax adapts per context:

className={clsx({ "flex gap-4": isActive })}
// becomes: className={clsx({ [styles.node0]: isActive })}

Out of Scope

These patterns are not handled and will not be rewritten:

| Pattern | Reason | |---------|--------| | Variable references (className={myVar}) | Requires data-flow analysis across scopes | | Computed class tokens ("text-" + size) | Cannot determine the final Tailwind token statically | | Template literals with interpolation (`flex ${size}`) | Span replacement corrupts template delimiters | | Wrapper function tracing (cn wrapping twMerge) | Interprocedural analysis, beyond static AST | | Runtime class conflict resolution | twMerge semantics are unnecessary with vanilla CSS |

Warnings

convert() returns a warnings array. Each entry has type, message, and location.

| Warning type | When emitted | |--------------|--------------| | unmatched-class | A class string was passed to Tailwind but produced no CSS output | | dynamic-class | A className expression was only partially rewritten because it contains variable references or other unresolvable parts — includes line and column of the expression |

A fully-rewritten expression (all string literals resolved) emits no dynamic-class warning.

Why Not Regex?

Existing Tailwind-to-CSS converters use regex matching or lookup tables to map class names to CSS. This breaks on:

  • Stacked variants (hover:focus:bg-blue-500)
  • Arbitrary values (bg-[#1a1a2e], w-[calc(100%-2rem)])
  • Dynamic or conditional class expressions

Vanillify sidesteps all of this. The AST parser handles extraction correctly regardless of syntax complexity, and Tailwind's compilation engine produces the same CSS it would in a real build. If Tailwind supports a utility, vanillify converts it accurately.

Requirements

  • Node.js >= 20

Tech Stack

| Component | Technology | | ---------- | ---------------------------------------------------------------------------- | | CSS engine | tailwindcss v4 compile() API | | Parser | oxc-parser | | AST walker | oxc-walker | | CLI | citty + consola | | Bundler | tsdown | | Tests | vitest |

License

MIT