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

eslint-plugin-tailwind-classify

v0.2.2

Published

ESLint plugin that splits Tailwind class lists onto multiple lines, grouped by semantic category (layout, spacing, typography, ...) with nested variants.

Readme

eslint-plugin-tailwind-classify

npm version license

Splits long Tailwind class lists onto multiple lines, grouped by semantic category (layout, spacing, typography, …), with variants nested inside each group.

Why

The official prettier-plugin-tailwindcss only sorts classes into a single line, and Prettier itself can't format the class/className attribute across multiple lines (it collapses whitespace). So this is an ESLint rule with autofix instead.

Other ESLint multi-line plugins wrap by print width and group by variant. classify is different: it groups by semantic category, so each line answers one question — "what are the spacing classes? the typography classes?".

// before
<div className="flex flex-col items-center justify-center px-5 py-15 text-sm text-neutral-100" />

// after
<div className="
  flex flex-col items-center justify-center
  px-5 py-15
  text-sm text-neutral-100
" />

Short lists are reordered on a single line instead of wrapped:

// before                         // after
<div className="text-sm flex p-4" />   →   <div className="flex p-4 text-sm" />

Why an ESLint rule, and not a Prettier plugin?

Because Prettier can't do this. Prettier deliberately collapses any whitespace inside the class / className attribute and renders it on a single line — there is no hook to lay an attribute value out across multiple lines. The request to support it has been open and declined for years (prettier/prettier#7863; see also #10918, #7550). So prettier-plugin-tailwindcss can only sort classes into that one line; a multi-line, category-grouped layout simply isn't expressible as a Prettier plugin.

ESLint, by contrast, exposes the attribute's source range and lets a rule rewrite it with an autofix — which is exactly what's needed here. It also composes cleanly with Prettier: this rule only changes whitespace inside the attribute value, which Prettier leaves untouched, so you can run both (see Using with Prettier).

Install

npm i -D eslint-plugin-tailwind-classify

Requires ESLint 8 or newer — tested on 8, 9, and 10. (On ESLint 8.0–8.39 the rule falls back to getSourceCode(); context.sourceCode is used from 8.40 on.)

Usage

Flat config (eslint.config.js) — recommended:

import tailwindClassify from "eslint-plugin-tailwind-classify";

export default [
  tailwindClassify.configs.recommended,
];

Or wire it manually (e.g. to pass options or scope it to certain files):

import tailwindClassify from "eslint-plugin-tailwind-classify";

export default [
  {
    files: ["**/*.{js,jsx,ts,tsx}"],
    plugins: { "tailwind-classify": tailwindClassify },
    rules: {
      "tailwind-classify/multiline": ["error", { printWidth: 100 }],
    },
  },
];

Run eslint --fix to apply the grouping.

Integration by syntax

The rule runs wherever your ESLint config can parse the file. It always handles JS-level usage (className, helper calls, tagged templates); for component frameworks it also handles the template class attribute when the matching ESLint parser is configured.

  • JS / TS / JSX / TSX — works out of the box. The default parser handles JSX with parserOptions.ecmaFeatures.jsx; use @typescript-eslint/parser for .ts/.tsx.
  • Svelte — supported natively (see below). class="…" in .svelte markup is grouped/wrapped; class={…} and class:foo directives are left alone.
  • Vue — supported natively (see below). class="…" in .vue <template> is grouped/wrapped; :class / v-bind:class directives are left alone.
  • Astro — supported natively (see below). class="…" in .astro markup is grouped/wrapped; class={…} expressions are left alone.
  • Preact / Solid — these JSX dialects use class; it's handled like className, no extra setup.
  • Plain HTML — no ESLint parser; use the programmatic formatMarkup.

Svelte

Svelte files are parsed by svelte-eslint-parser (bundled with eslint-plugin-svelte). Add the rule for .svelte files:

import svelte from "eslint-plugin-svelte";
import tailwindClassify from "eslint-plugin-tailwind-classify";

export default [
  ...svelte.configs.recommended, // sets svelte-eslint-parser for *.svelte
  tailwindClassify.configs.recommended,
];

tailwind-classify/multiline then fixes class="…" in .svelte markup and any clsx/cva/tw usage in <script>.

Vue

Vue SFCs are parsed by vue-eslint-parser. Set it as the parser for .vue files (eslint-plugin-vue's configs do this):

import vue from "eslint-plugin-vue";
import tailwindClassify from "eslint-plugin-tailwind-classify";

export default [
  ...vue.configs["flat/recommended"], // sets vue-eslint-parser for *.vue
  tailwindClassify.configs.recommended,
];

class="…" in the <template> is fixed (the rule reads the template AST via vue-eslint-parser), as is clsx/cva/tw usage in <script>.

Astro

Astro files are parsed by astro-eslint-parser (bundled with eslint-plugin-astro):

import astro from "eslint-plugin-astro";
import tailwindClassify from "eslint-plugin-tailwind-classify";

export default [
  ...astro.configs.recommended, // sets astro-eslint-parser for *.astro
  tailwindClassify.configs.recommended,
];

class="…" in .astro markup is fixed, as is clsx/cva/tw in the frontmatter.

Programmatic (HTML / scripts)

import { formatMarkup } from "eslint-plugin-tailwind-classify";

const out = formatMarkup('<div class="text-sm flex p-4"></div>');
// → '<div class="flex p-4 text-sm"></div>'

formatMarkup(source, options) accepts the same options as the rule. formatClassValue, extractClassAttributes, groupByCategory, and toLines are also exported for finer-grained use.

What it formats

  • JSX className — string literals and no-substitution template literals.
  • HTML / Vue / Svelte / Astro class — static attributes. Dynamic bindings (:class, v-bind:class, [class], class:list, class={…}) are left alone.
  • Class helpersclsx / classnames / cn / cx / cva / ctl / twMerge / twJoin / tw, including string, array, object, and conditional arguments. cva variant values and clsx-style object keys are both handled.
  • Tagged templatestw`…`.

Wrapping requires a context that allows newlines (JSX attributes, template literals, HTML). Ordinary JS string literals (e.g. clsx("…")) can't contain raw newlines, so there they are only regrouped on a single line, never wrapped.

Categories

Classes are grouped into these categories, in this default order; unknown / custom classes go on a leading line.

layout · flexbox-grid · spacing · sizing · typography · backgrounds · borders · effects · filters · tables · transitions-animation · transforms · interactivity · svg · accessibility

Note: the display utilities flex / inline-flex / grid / inline-grid are grouped under flexbox-grid (not layout) so flex flex-col items-center stay on one line.

Options

"tailwind-classify/multiline": ["error", { printWidth: 100, group: "category-variant" }]

| Option | Type | Default | Description | |---|---|---|---| | group | "category" \| "variant" \| "category-variant" | "category-variant" | Line layout: one line per category, per variant chain, or per category with variants nested. | | categoryOrder | string[] | docs order | Override the category order. Listed categories lead; the rest follow in the default order. | | printWidth | number | 80 | Wrap once the single-line form would exceed this column. | | maxClassesPerLine | number | — | Wrap once the single line would hold more than this many classes. | | indentStep | string | " " (2 spaces) | Indentation added for each wrapped class line. | | quotesOnNewLine | boolean | true | Put the quotes on their own lines when wrapping (vs. hugging the classes). | | preserveUnknownClasses | boolean | true | Place unknown/custom classes on a leading line (true) or a trailing line (false). They are always kept. | | callees | string[] | clsx, classnames, cn, cx, cva, ctl, twMerge, twJoin, tw | Function names whose class arguments are formatted. | | tags | string[] | ["tw"] | Tagged-template names to format. | | tailwindConfig | string | auto | Path to a Tailwind v3 config; its getClassOrder drives intra-category order. | | entryPoint | string | auto | Path to a Tailwind v4 CSS entry point. (Accepted; v4 loading is async and not wired yet — currently falls back.) |

Class ordering

The category of each class comes from a built-in prefix map. The order within a category comes from Tailwind's official getClassOrder() when a tailwindConfig is provided, so overriding utilities stay later. Without it, classes keep their source order within each category.

Before / after

<!-- HTML — before -->
<div class="text-sm flex p-4"></div>
<!-- after -->
<div class="flex p-4 text-sm"></div>
// clsx — before
clsx("text-sm flex p-4", cond && "bg-red-500 p-2");
// after
clsx("flex p-4 text-sm", cond && "p-2 bg-red-500");

Using with Prettier

The rule only changes whitespace inside a class attribute value, which Prettier leaves untouched — so the two don't fight, and order (eslint --fix vs prettier) doesn't matter. eslint-config-prettier doesn't disable this rule (it isn't a stylistic rule Prettier can own), so you can keep it enabled alongside your Prettier setup.

Safety invariant

Reordering and regrouping classes in markup is cosmetic — the cascade is decided by the generated CSS, not by class position in the attribute. This plugin therefore:

  • only reorders and wraps classes;
  • removes exact duplicates only (p-4 p-4), never conflicting pairs (p-4 p-2, block flex);
  • never merges conflicting utilities (that is tailwind-merge's job);
  • preserves unknown / arbitrary (bg-[#fff], [mask:url(#x)]) / !important classes verbatim.

Changelog

See CHANGELOG.md, generated by changesets.

Roadmap

See PLAN.md. Each stage lives on its own branch; each checklist item is a commit.

License

MIT