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

tailward

v1.1.0

Published

Convert regular CSS to Tailwind CSS v4 utility classes.

Readme

Version Build Downloads License

About

Tailward converts regular CSS to Tailwind CSS 4 utility classes.

Give it a declaration, a rule, or a whole stylesheet and you get back the utility classes that produce the same result, ready to drop straight into your markup.

It's handy when you're moving an existing project over to Tailwind, or turning inlined HTML back into clean, class-based templates, without redoing all that styling by hand.

Runs on Node 18+ or a modern edge runtime.

Contents

Installation

npm install tailward

Usage

Convert a stylesheet and read the classes back, grouped per selector:

import { CssToTailwind } from 'tailward'

const converter = new CssToTailwind()

const { nodes } = await converter.convert(`
  .button {
    display: inline-flex;
    padding: 0.5rem 1rem;
    background-color: #fb2c36;
    border-radius: 0.375rem;
    font-weight: 600;
  }
  .button:hover {
    background-color: #e7000b;
  }
  @media (min-width: 48rem) {
    .button { padding-left: 2rem; }
  }
`)

for (const node of nodes) {
  console.log(node.selector, '→', node.tailwindClasses.join(' '))
}
// .button → inline-flex rounded-md bg-red-500 px-4 py-2 font-semibold
// .button → hover:bg-red-600
// .button → md:pl-8

The index builds on the first convert() call and is cached after that, so construct the converter once and reuse it. For a quick one-off, there's convertCss:

import { convertCss } from 'tailward'

const { nodes } = await convertCss('.a { display: block }')

API

new CssToTailwind(options?)

Creates a converter you can reuse. The utility index builds on the first convert() call and is cached after that, so construct it once and keep it around.

converter.convert(css)

Converts a stylesheet.

  • css - the CSS to convert, as a string.

Returns a ConvertResult:

interface ConvertResult {
  nodes: {
    selector: string          // the original selector
    tailwindClasses: string[] // in Tailwind's class order
    complementary: string     // declarations that couldn't convert, as CSS
  }[]
  warnings: {
    type: 'approximate-color' | 'unconvertible'
    selector: string          // the rule it came from
    declaration: string       // "color: #a1b2c3"
    message: string           // human-readable explanation
  }[]
  summary: {
    converted: number         // declarations that produced utilities
    unconvertible: number     // declarations left as complementary CSS
    arbitrary: number         // emitted utilities using an arbitrary value or property
    coverage: number          // converted / (converted + unconvertible), 0 to 1
  }
}

With { positions: true }, each node also carries a position of { start, end, line, column }, mapping the rule back to the input for editor "convert selection" integrations. It's off by default.

warnings tells you where the conversion wasn't exact: colors matched to a near palette token instead of an exact one, and declarations it couldn't convert at all.

const { warnings } = await convertCss('.a { color: #a1b2c3 }', { colorThreshold: 0.2 })
// [{ type: 'approximate-color', selector: '.a', declaration: 'color: #a1b2c3',
//    message: 'approximated #a1b2c3 to text-mist-400 (ΔE 0.039)' }]

convertCss(css, options?)

One-shot version of the above, new CssToTailwind(options).convert(css) in a single call.

  • css - the CSS to convert, as a string
  • options - any of the Options below

Returns the same ConvertResult as convert().

convertHtml(html, options?)

De-inlines a full HTML document, turning its inline styles and <style> rules into utility classes. Also available from the tailward/html subpath.

  • html - the HTML document, as a string
  • options - the Options below, plus styleAttributes, styleRules, and keepStyleAttributes

Returns { html, warnings } - the rewritten markup and the same warnings convertCss produces.

Formatting the result

Two helpers take a ConvertResult and give you something you can drop straight into a project:

  • toApply(result) renders copy-pasteable @apply rules, one per selector, keeping any unconvertible declarations as raw CSS.
  • toClassMap(result) returns a plain { selector: 'class list' } map.
import { convertCss, toApply, toClassMap } from 'tailward'

const result = await convertCss('.a { display: flex; padding: 1rem }')
toApply(result)    // ".a { @apply flex p-4; }"
toClassMap(result) // { '.a': 'flex p-4' }

Options

Every entry point takes the same options object - the CssToTailwind constructor, convertCss, and convertHtml (which adds a few of its own on top). They're all optional.

| Option | Type | Default | Description | | --- | --- | --- | --- | | theme | string | - | Custom @theme token overrides (engine-free, edge-safe). | | css | string | - | Full CSS resolved by the Tailwind engine (Node only). | | base | string | process.cwd() | Directory for resolving @import / @plugin in css. | | remInPx | number | 16 | Pixel value of 1rem for normalizing px inputs. | | arbitrary | boolean | true | Emit arbitrary values when nothing matches; otherwise keep as raw CSS. | | colorThreshold | number | 0.02 | Max OKLab distance to match a palette color. 0 means exact only. | | canonicalize | boolean | true | Prefer named functional utilities for bare numbers (z-60 over z-[60]). | | important | boolean | false | Preserve !important as the v4 trailing bang (text-red-500!, sm:text-red-500!); otherwise dropped. | | positions | boolean | false | Attach a position ({ start, end, line, column }) to each node, mapping it back to the source. | | maxSpacingSteps | number | 96 | Largest spacing multiplier reversed to a scale utility; larger lengths stay arbitrary (600px becomes w-[600px], not w-150). Use Infinity to reverse any multiple. |

A few of them in practice, on convertCss:

import { convertCss } from 'tailward'

// colorThreshold: match only exact palette colors, near-misses stay arbitrary.
await convertCss('.a { color: #f9323d }')                        // → text-red-500
await convertCss('.a { color: #f9323d }', { colorThreshold: 0 }) // → text-[#f9323d]

// maxSpacingSteps: reverse any spacing multiple, not just the conventional range.
await convertCss('.a { width: 600px }', { maxSpacingSteps: Infinity }) // → w-150

// arbitrary: keep unmatched declarations as raw CSS instead of an arbitrary value.
await convertCss('.a { width: 33.7% }', { arbitrary: false }) // → kept in node.complementary, no class

The same options flow through convertHtml, since it runs the converter under the hood:

import { convertHtml } from 'tailward'

// colorThreshold: 0 keeps the color arbitrary; remInPx: 8 rescales the padding.
const { html } = await convertHtml(
  '<a style="color: #f9323d; padding: 16px">Go</a>',
  { colorThreshold: 0, remInPx: 8 },
)
// <a class="p-8 text-[#f9323d]">Go</a>

CLI

Installing the package gives you a tailward binary. It reads a file or stdin and prints @apply blocks. Point it at an .html file (or pass --html) to de-inline instead.

tailward styles.css                 # @apply blocks on stdout
cat styles.css | tailward --json    # raw ConvertResult as JSON
tailward email.html --important --out out.html
tailward styles.css --theme brand.css --summary
tailward styles.css --watch         # re-run on change

| Flag | Description | | --- | --- | | --html | De-inline an HTML document (auto-enabled for .html/.htm). | | --theme <file> | Convert against a custom @theme file (engine-free). | | --css <file> | Convert against full CSS via the Tailwind engine. | | --rem <n> | Pixel value of 1rem (default 16). | | --important | Preserve !important as the v4 trailing bang. | | --out <file> | Write output to a file instead of stdout. | | --json | Output the raw conversion result as JSON (CSS mode). | | --summary | Print a conversion summary to stderr. | | --watch | Re-run when the input file changes. |

What it converts

  • Full stylesheets. Selectors, @media to responsive variants (md:), pseudo-classes and elements to variants (hover:, before:), @supports to supports-[…]:, and @container to container-query variants (@sm:, @min-[400px]:).
  • Colors. Nearest palette match in OKLab (#fb2c36 becomes text-red-500), falling back to an arbitrary value past a threshold.
  • Spacing. Any Tailwind 0.25-step multiplier (13px becomes p-3.25, 0.875rem becomes p-3.5), plus box-shorthand decomposition (padding: 0 24px becomes px-6 py-0).
  • Named tokens. border-radius: 0.5rem becomes rounded-lg, font-weight: 700 becomes font-bold, font-size: 20px becomes text-xl.
  • Composite properties. transform, filter, and linear gradients decompose into per-function utilities: transform: translateX(10px) rotate(45deg) becomes translate-x-2.5 rotate-45, filter: blur(4px) becomes blur-xs, linear-gradient(to right, #fb2c36, #155dfc) becomes bg-linear-to-r from-red-500 to-blue-600.
  • Everything else. Emitted as an arbitrary value (p-[13.7px], bg-[#123456]) or arbitrary property ([mask-type:luminance]). Set arbitrary: false to keep unmatched declarations as raw CSS instead.

Every class it emits is checked to reproduce the exact input declaration - against the live engine on the css path, or against the pregenerated data (verified the same way) on the engine-free paths.

De-inlining HTML

convertHtml takes a whole HTML document and rewrites it, turning both inline style="" attributes and <style> rules into utility classes. It's built for de-inlining email HTML or migrating a static page.

import { convertHtml } from 'tailward' // or 'tailward/html'

const { html, warnings } = await convertHtml(input)

Given this input:

<style>
  .card { border-radius: 8px; background-color: #fff }
  .card:hover { color: #fb2c36 }
  @media (min-width: 48rem) { .card { padding: 24px } }
</style>

<div class="card" style="padding: 16px">Hello</div>

...you get html back like this:

<div class="card rounded-lg bg-white hover:text-red-500 md:p-6 p-4">Hello</div>

The style="" becomes p-4, the .card rule becomes rounded-lg bg-white, :hover and @media bake into the hover: and md: variants, and the <style> block is dropped once everything in it has converted.

Here's what it does with each part:

  • Inline style="" becomes classes merged into class, and the attribute is dropped. Anything that can't convert stays behind in style (or keep the original with keepStyleAttributes).
  • <style> rules convert with their context baked into the variant (@media becomes sm:, :hover becomes hover:, ::before becomes before:), then attach to every element the selector matches.
  • @keyframes, @font-face, @import, and any rule that can't be fully converted or matched are left in a trimmed <style> block.
  • Entities, comments, and Outlook (MSO) conditionals round-trip untouched.

It accepts every converter option, plus these:

| Option | Type | Default | Description | | --- | --- | --- | --- | | styleAttributes | boolean | true | Convert inline style="" attributes into classes. | | styleRules | 'variants' \| 'residual' \| 'drop' | 'variants' | variants: convert <style> rules to classes on matching elements, keep the rest as residual CSS. residual: leave every <style> untouched. drop: convert what maps, discard the rest. | | keepStyleAttributes | boolean | false | Keep the original style="" alongside the emitted classes. |

convertHtml is also exported from the tailward/html subpath, so bundlers can tree-shake the HTML parser out of the core entry. Its parsing deps are pure-JS and edge-safe either way.

Matching your Tailwind theme

The classes you get back depend on the theme Tailward converts against - if your project customizes colors or spacing, that's how a brand color comes back as bg-brand instead of bg-[oklch(...)]. There are three ways to tell it which theme to use:

| You pass | Runs on | Uses the engine? | Use it for | | --- | --- | --- | --- | | (nothing) | Node and edge | no | the default Tailwind theme | | { theme } | Node and edge | no | a custom @theme (token overrides) | | { css } | Node only | yes | @plugin, custom @utility, @import |

Default (stock theme)

new CssToTailwind()

Uses a pregenerated index for the stock Tailwind theme. Fully edge-compatible.

Custom @theme

Override design tokens (colors, spacing, radius, font sizes, and so on) and convert against them without the Tailwind engine, so it works per-request on the edge:

const converter = new CssToTailwind({
  theme: `@theme {
    --color-brand: oklch(55% 0.2 270);
    --spacing: 0.2rem;
    --radius-lg: 1rem;
  }`,
})

await converter.convert('.a { background-color: oklch(55% 0.2 270) }')
// → bg-brand
await converter.convert('.a { padding: 0.4rem }')   // 0.4 / 0.2
// → p-2

Only token overrides are supported here. The output is byte-identical to running the real engine with the same @theme.

Full engine (css)

For themes that need the actual compiler (@plugin, custom @utility, @import), pass raw CSS. This dynamically imports tailwindcss and reads from disk, so it runs in Node only:

new CssToTailwind({
  css: '@import "tailwindcss";\n@plugin "./my-plugin.js";',
  base: process.cwd(), // resolves @import / @plugin paths
})