Skip to content

tinyfroggy/twlint

Repository files navigation

twlinter

A fully open source Tailwind CSS diagnostic CLI for project-wide checks. It is designed to scan a full codebase from the terminal, similar to the react-doctor workflow, and currently supports Tailwind v4 projects.

We were inspired by react-doctor and Tailwind CSS IntelliSense.

Anyone can contribute to it, fork it, or build their own variant on top of it.

Features

  • Zero-config default scan with every rule enabled.
  • Tailwind v4 diagnostics through Tailwind's own language service.
  • Custom Tailwind rules for duplicate utilities, dependency mistakes, accessibility issues, shorthand opportunities, and design-system preferences.
  • Component-aware dependency checks for shadcn/ui-style components such as DialogFooter, Button, and CardFooter.
  • Conservative unknown-component behavior: unknown components are skipped unless strict mode is enabled.
  • Responsive-scope aware checks: base flex satisfies sm:flex-row, but sm:flex does not satisfy base flex-col.
  • Monorepo-friendly input resolution for package paths such as apps/web.
  • Built-in ignores for generated output from Next.js, Remix, TanStack Start, Vite, Astro, SvelteKit, Nuxt, Vinxi, Nitro, Storybook, Vercel, Netlify, and common test/build tools.
  • Grouped terminal output designed for humans and agents: stable rule ids, file/line locations, deterministic sorting, and non-zero exit code when diagnostics exist.
  • Programmatic API via lintProject().

Quick Start

npx -y twlinter@latest .
npx -y twlinter@latest apps/web
npx -y twlinter@latest "src/**/*.{tsx,html}"

The default rule profile runs every rule without requiring a .twlinter.json file. The terminal reporter groups noisy style suggestions so one-command scans stay readable.

Use It Like react-doctor

Once published, developers should be able to run the package directly with npx:

npx -y twlinter@latest . --verbose

The CLI also supports the explicit lint command:

npx -y twlinter@latest lint .
npx -y twlinter@latest lint "src/**/*.{tsx,html}" --verbose

The npm package and installed CLI command are both twlinter.

Local Development

git clone <repo-url>
cd twlint
npm install

Running on a test project

Use the built-in fixture to verify the CLI works:

# List all rules
npm run dev -- --help

# Scan the test fixture
npm run dev -- tests/fixtures/tw-v4-app/src

# Or scan the whole repo (excluding node_modules)
npm run dev -- .

# Run only selected rules while developing a rule
npm run dev -- . --rules canonical-classes,shorthand-classes

Running against your own project

# Point to your project directory
npm run dev -- /path/to/your/project

# With verbose output and custom rules
npm run dev -- /path/to/your/project --verbose --rules canonical-classes,shorthand-classes

# Override CSS entry point if auto-detection fails
npm run dev -- /path/to/your/project --css-entry /path/to/your/project/src/globals.css

Building and running the compiled CLI

npm run build
node dist/cli.js tests/fixtures/tw-v4-app/src --verbose

Development scripts

Command Description
npm run dev -- ... Run the CLI directly via tsx (no build needed)
npm run build Compile TypeScript to dist/
npm run test Run all tests (Vitest)
npm run test:watch Run tests in watch mode
npm run typecheck TypeScript type checking (tsc --noEmit)
npm run lint Lint source code (oxlint)
npm run format Format source code (oxfmt)
npm run format:check Check formatting without writing
npm run check Run all checks: typecheck + lint + format + test

Commands

twlinter [patterns...]

When run without a subcommand, twlinter scans the provided paths or glob patterns.

twlinter lint [patterns...]

Lint files for canonical Tailwind classes. This is the primary command.

Alias behavior: twlinter can be run with or without the lint subcommand.

Option Default Description
--verbose Off Show per-file details and project info
-c, --config <path> Auto Path to config file (default: .twlinter.json in project root)
--rules <rules> All rules Comma-separated rule ids. Overrides config rules.
--cssEntry <path> Auto Tailwind CSS entry file. Overrides auto-discovery.

CSS Entrypoint Discovery

twlinter auto-discovers your CSS entry by searching for files that import tailwindcss. It prefers conventional paths like src/app.css, app/globals.css, and styles/globals.css. If multiple candidates are found, it exits with an error.

Output

Diagnostics are grouped by rule-friendly summary with severity icons:

  ⚠ The class `h-[350px]` can be written as `h-87.5` (2)
    app.tsx:12
    app.tsx:15

Found 2 warning(s). Scanned 18 file(s) in 45ms.

With --verbose, shows full file paths and project configuration details. Long style suggestions, such as class reorder lists, are summarized by default and expanded with --verbose.

Developer And Agent Experience

The CLI is intentionally friendly to both humans and coding agents:

  • One command works for most projects: twlinter ..
  • Default rules run every check; the reporter groups and summarizes noisy style suggestions.
  • Diagnostics include stable rule ids in the programmatic API.
  • Output is grouped by message to make large scans readable.
  • Results are sorted by file, line, and column for deterministic diffs.
  • Exit code is 1 when diagnostics are found and 0 when clean.
  • --rules makes experiments reproducible without editing config files.
  • Generated framework outputs are ignored by default, including TanStack Start and Vinxi output.

Exit Codes

Exit code 1 if any diagnostics are found, 0 otherwise.

Monorepo Support

twlinter detects monorepo layouts (npm, pnpm, yarn, nx) and handles them correctly. Run from a package root to scope the scan:

twlinter lint apps/web

Programmatic API

import { lintProject } from "twlinter";

const result = await lintProject(["."]);

console.log(result.diagnostics);
console.log(
  `Scanned ${result.scannedFiles} files in ${result.elapsedMilliseconds}ms`,
);

LintResult

Field Type Description
matchedFiles number Files matched by glob patterns
scannedFiles number Files passed through relevance filter
elapsedMilliseconds number Total scan duration
diagnostics Diagnostic[] All findings
project ProjectInfo Discovered project metadata

Diagnostic

Field Type Description
file string File path
line number Line number
column number Column number
rule string Rule identifier
severity "warning" | "error" Severity level
message string Human-readable message
source string Diagnostic source

LintOptions

Field Type Default Description
verbose boolean false Show per-file details
config string Path to config file
ignorePatterns string[] Additional glob patterns to skip (merged with defaults)
classIgnorePatterns string[] Regex patterns for class names to suppress
maxFileSize number 524288 Maximum file size in bytes to scan
rules string[] All rules Rules to enable. Defaults to every known rule.
cssEntry string Auto Override the CSS entry point detection
strict boolean false Emit low-confidence dependency warnings for unknown components
components object Built-ins Component base class map merged over built-in presets

Rules

Default rules run every known check so twlinter . catches everything without configuration. Use --rules or .twlinter.json only when you intentionally want a smaller rule set.

Default rule ids:

class-conflicts
canonical-classes
recommended-variant-order
used-blocklisted-class
shorthand-classes
no-duplicate-utilities
canonical-class-order
prefer-truncate-shorthand
no-important-abuse
no-sr-only-display-conflict
consistent-negative-arbitrary-values
prefer-logical-properties
require-motion-reduce-for-animation
no-orphan-layout-utilities
require-flex-for-flex-utilities
require-grid-for-grid-utilities
warn-ineffective-z-index
require-display-for-sizing
warn-hover-on-disabled
require-focus-visible-for-interactive
warn-incomplete-dark-color-pair
prefer-theme-scale
no-magic-spacing
detect-conflicts-in-template-literals
prefer-design-tokens

Run a smaller rule set with --rules or .twlinter.json:

twlinter . --rules canonical-classes,shorthand-classes,prefer-logical-properties
Rule Default Description
canonical-classes On Suggests canonical Tailwind class names for arbitrary values
class-conflicts / no-conflicting-utilities On Detect CSS class conflicts on the same element
recommended-variant-order On Check variant ordering follows Tailwind conventions
used-blocklisted-class On Detect usage of blocklisted or legacy classes
shorthand-classes / prefer-shorthand On Suggest shorthand classes (e.g. w-10 h-10size-10)
no-duplicate-utilities On Detect repeated identical utilities (e.g. p-4 p-4)
canonical-class-order On Enforce deterministic utility ordering (base → sm → md → lg → xl)
prefer-truncate-shorthand On Suggest truncate over overflow-hidden text-ellipsis whitespace-nowrap
no-important-abuse On Warn when !important is overused (>2 per class list)
no-sr-only-display-conflict On Detect sr-only combined with display utilities
consistent-negative-arbitrary-values On Enforce -top-[5px] over top-[-5px] syntax
prefer-logical-properties On Encourage ps-*/pe-* over pl-*/pr-* for RTL support
require-motion-reduce-for-animation On Require motion-reduce:animate-none for animations
no-orphan-layout-utilities On Detect items-center/justify-center without flex/grid
require-flex-for-flex-utilities On Detect flex-col/flex-wrap without flex
require-grid-for-grid-utilities On Detect grid-cols-*/grid-rows-* without grid
warn-ineffective-z-index On Detect z-* without relative/absolute/fixed/sticky
require-display-for-sizing On Detect sizing on inline elements without inline-block
warn-hover-on-disabled On Detect hover:* on disabled elements
require-focus-visible-for-interactive On Require focus-visible:* on interactive elements with hover:*
warn-incomplete-dark-color-pair On Detect light-mode colors without dark:* counterparts
prefer-theme-scale On Prefer Tailwind design scale over arbitrary values
no-magic-spacing On Detect spacing values not aligned to the 4px design grid
detect-conflicts-in-template-literals On Detect duplicate utilities in template literal class names
prefer-design-tokens On Prefer design tokens over raw hex colors

Specify rules in .twlinter.json or programmatically via LintOptions.rules. Use comma-separated names with --rules on the CLI when you want to narrow the scan for local debugging.

Aliases: prefer-shorthandshorthand-classes, no-conflicting-utilitiesclass-conflicts. Both names work interchangeably.

Configuration

Configuration is optional. Most projects should start with one command:

twlinter .

Create a .twlinter.json file in your project root:

{
  "ignorePatterns": ["**/*.stories.*", "**/test/**"],
  "classIgnorePatterns": ["w-\\[\\d+px\\]", "h-\\[\\d+px\\]"],
  "rules": ["canonical-classes", "class-conflicts"],
  "maxFileSize": 100000,
  "cssEntry": "src/custom.css",
  "strict": false,
  "components": {
    "DialogFooter": {
      "baseClasses": "flex flex-col-reverse gap-2 px-6 sm:flex-row sm:justify-end"
    }
  }
}
Field Type Description
ignorePatterns string[] Glob patterns to skip (merged with built-in defaults)
classIgnorePatterns string[] Regex patterns matched against each diagnostic's class name
rules string[] Rules to enable
maxFileSize number Maximum file size in bytes
cssEntry string Override auto-discovered CSS entry point
strict boolean Emit low-confidence dependency warnings for unknown components
components object Component base class map used by component-aware rules

Config can also be placed in package.json under the "twlinter" key.

Built-In Ignores

twlinter ignores generated and dependency output by default, both at glob time and with a hard path-segment filter. This keeps one-command scans useful in real apps and prevents generated framework files from dominating diagnostics.

Ignored path segments include:

node_modules
dist
vendor
.next
.nuxt
.svelte-kit
.astro
.tanstack
.vinxi
.vite
.vercel
.netlify
.output
.nitro
.cache
.parcel-cache
.turbo
build
coverage
out
storybook-static
playwright-report

This covers common output from Next.js, Remix, React Router, TanStack Start, Vite, Vinxi, Nitro, Astro, SvelteKit, Nuxt, Storybook, Vercel, Netlify, and test tooling.

Component-Aware Rules

Some rules need to know whether a class can actually take effect. For example, flex-row only works when the rendered element also has flex or inline-flex.

Native elements are checked directly:

<div className="flex-row" />

Known components are checked against their effective classes, which are the component's base classes plus the user-provided classes:

<DialogFooter className="flex-row" />

If DialogFooter is configured or included in the built-in shadcn/ui preset with flex, this does not warn.

Unknown components are skipped by default because twlinter cannot know what they render internally. Enable strict to emit low-confidence warnings instead.

{
  "strict": true,
  "components": {
    "Toolbar": {
      "baseClasses": "inline-flex items-center gap-2"
    },
    "Dialog.Footer": {
      "baseClasses": "flex flex-col-reverse sm:flex-row sm:justify-end"
    }
  }
}

Component-aware dependency rules also understand responsive scope. A base flex satisfies sm:flex-row, but sm:flex does not satisfy a base flex-col.

Ignoring files

ignorePatterns are glob patterns merged into the built-in ignores:

{
  "ignorePatterns": ["**/*.stories.*", "**/generated/**", "**/test/**"]
}

Ignoring specific class patterns

classIgnorePatterns are regex patterns tested against each diagnostic's class name. Diagnostics whose class matches any pattern are silently dropped:

{
  "classIgnorePatterns": [
    "w-\\[\\d+px\\]",
    "h-\\[\\d+px\\]",
    "min-h-\\[\\d+px\\]",
    "max-w-\\[\\d+(?:px|rem)\\]"
  ]
}

This suppresses all suggestions for w-[100px], h-[350px], sm:w-[260px], max-w-[12rem], etc. The patterns support variants — sm:w-[260px] matches w-\[\d+px\] because the class name contains the token.

Why We Use These Dependencies

This CLI is not a separate Tailwind parser with a completely different rule engine. It reuses Tailwind's existing language tooling and adapts it for terminal-based project scans.

@tailwindcss/language-service

This package provides the Tailwind analysis engine. It is the part that understands Tailwind syntax, project configuration, and class diagnostics.

vscode-languageserver-textdocument

This package provides the document model that the language service expects as input. In the editor, Tailwind diagnostics run against open text documents. In this CLI, we create those documents ourselves from files on disk and pass them into the Tailwind language service.

Why both are needed

They solve different problems:

  1. @tailwindcss/language-service does the Tailwind-aware analysis.
  2. vscode-languageserver-textdocument represents file contents in the language-server format.

So this project is effectively a CLI wrapper around the Tailwind language tooling, not a second unrelated linter.

Architecture

src/
  cli.ts              CLI entry (commander)
  index.ts            Programmatic API
  types.ts            Shared types
  constants.ts        Default globs and patterns
  formatter.ts        Line-level diagnostic formatting
  config/
    types.ts          Config type definitions
    load-config.ts    Config file loader (.twlinter.json / package.json)
  core/
    lint-project.ts   Scan orchestration
    validation-worker.ts  Worker thread for parallel validation
    normalize-diagnostic.ts
    rules.ts          Rule registry
    shorthand-classes.ts  Shorthand detection via canonicalizeCandidates
  custom-rules/
    context.ts        Rule context and component-aware class resolution
    index.ts          Custom static-analysis rules
    utils.ts          Shared class extraction, variant/element parsing
  discovery/
    discover-project.ts
    resolve-css-entry.ts
    resolve-inputs.ts
    detect-monorepo.ts
    file-relevance.ts
  adapters/
    tailwind-language-service.ts
    tailwind-design-system.ts
  reporters/
    pretty.ts

License

MIT

Publishing Checklist

Before the first npm publish:

npm run check
npm pack

Recommended final package metadata before publishing:

  1. Confirm the npm package name in package.json.
  2. Add repository, homepage, and bugs URLs in package.json.
  3. Publish with npm publish.

Contributing

See CONTRIBUTING.md for local setup, validation commands, and the preferred pull request flow.

Open Source

This repository is intended to be easy to contribute to:

  1. Standard npm-based setup.
  2. CI runs the same checks contributors run locally.
  3. Husky can run checks before commit.
  4. The codebase stays small and focused so new contributors can understand it quickly.

About

Open source Tailwind CSS CLI for full-project diagnostics and canonical class suggestions.

Resources

License

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors