Skip to main content

Powerful control over every class, breakpoint, and state.

No more tangled className strings. Grouped by breakpoint and state, readable at a glance, and fully typed.

npm install tailess

In practice

This is the whole className

Three breakpoints, a dark mode with its own hover, a condition, a compound variant and the caller's className — one ss() call, checked by the compiler.

Card.tsx
import { ss, match } from "tailess"; export function Card({ tone, wide, disabled, className }: CardProps) {  return (    <div      className={ss(        {          base: "rounded-lg border p-4",          md: wide && "p-6",          dark: { base: "border-neutral-800", hover: "border-neutral-700" },          "focus-visible": "ring-2 ring-offset-2",        },        disabled && { base: "opacity-50 pointer-events-none" },        match(tone, { info: "bg-blue-50", danger: "bg-red-50" }),        className,      )}    />  );}

Before and after

The same className, twice

One ordinary button: three breakpoints, two states and a dark mode with its own hover. Both spellings produce the same class string at the same runtime cost — but one of them you can read, and the compiler checks every key.

One string, everything jumbled together
<button  className="flex gap-2 rounded-lg border px-3 py-1.5 text-sm sm:px-4 md:gap-3 md:rounded-xl md:px-5 md:text-base lg:px-6 hover:bg-neutral-50 focus-visible:outline-2 dark:border-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-900"/>
Grouped, readable, typed
<button  className={ss({    base: "flex gap-2 rounded-lg border px-3 py-1.5 text-sm",    sm: "px-4",    md: "gap-3 rounded-xl px-5 text-base",    lg: "px-6",    hover: "bg-neutral-50",    "focus-visible": "outline-2",    dark: {      base: "border-neutral-800 text-neutral-100",      hover: "bg-neutral-900",    },  })}/>

And one call is the whole className

Conditions, a caller's className and compound variants all go inside it. No wrapper helper, no second ss(). Keys are sorted inside each map; the arguments themselves are never reordered, which is what makes the last argument win exactly as it does in cn().

A wrapper, and ss() again for every condition
className={cn(  ss({ base: "rounded-lg border p-4", md: "p-6" }),  ss({ dark: "border-neutral-800" }),  isDisabled && ss({ base: "opacity-50", sm: "bg-red-500" }),  className,)}
One call
className={ss(  {    base: "rounded-lg border p-4",    md: "p-6",    dark: { base: "border-neutral-800", hover: "border-neutral-700" },  },  isDisabled && { base: "opacity-50", sm: "bg-red-500" },  className,)}

Quick start

Three steps, and the first one is an install command

Add one line to the config file you already have for Tailwind. That line is what lets Tailwind see the classes ss() builds at runtime — and if you forget it, tailess prints the exact fix instead of leaving you with a page that quietly lost its styles.

  1. 01

    Install

    One package, one runtime dependency. Use whichever package manager the project already has.

    npm install tailess
  2. 02

    Add the plugin — Vite

    Order in the array does not matter — the hook is registered order: "pre", so it always runs before Tailwind.

    vite.config.ts
    import { defineConfig } from "vite";import tailwindcss from "@tailwindcss/vite";import tailess from "tailess/vite"; export default defineConfig({  plugins: [tailwindcss(), tailess()],});
  3. 02

    Or PostCSS — Next.js and friends

    Listed before @tailwindcss/postcss. Works with Turbopack and webpack, in dev and in build.

    postcss.config.mjs
    const config = {  plugins: {    "tailess/postcss": {},    "@tailwindcss/postcss": {},  },}; export default config;
  4. 03

    Use it

    That is the whole setup. No config file, no CSS changes, nothing to commit.

    Button.tsx
    import { ss } from "tailess"; <button  className={ss({    base: "rounded-lg px-4 py-2",    md: "px-6",    hover: "bg-neutral-50",    "focus-visible": "ring-2",  })}/>

Your className already has structure. Make it visible.

MIT licensed, and the build plugin tells you when it is not wired up instead of leaving you with a page that quietly lost its styles.

npm install tailess