Skip to content

IvanRodriCalleja/tailwind-typescript-plugin

Repository files navigation

Tailwind TypeScript Plugin

CI npm version License: MIT TypeScript PRs Welcome

A TypeScript Language Service plugin that catches typos and invalid Tailwind CSS class names in your JSX/TSX files. When you write a class name that doesn't exist in Tailwind, it won't apply any styles—this plugin detects those mistakes and shows errors directly in your editor before you ship broken styles.

What does this plugin do?

Ever written className="flex itms-center" instead of "flex items-center"? That typo silently fails—Tailwind ignores invalid classes and your component looks broken. This plugin prevents that by analyzing your JSX/TSX code and validating that all Tailwind classes used in className attributes actually exist in your Tailwind CSS configuration. It provides real-time feedback by showing TypeScript errors for invalid or misspelled Tailwind classes, catching styling mistakes before they reach production.

Table of Contents

Features

  • Real-time validation: Get instant feedback on invalid Tailwind classes while you code
  • Editor integration: Works with any editor that supports TypeScript Language Service (VS Code, WebStorm, etc.)
  • Supports Tailwind variants: Validates responsive (md:, lg:), state (hover:, focus:), and other variants
  • Arbitrary values: Correctly handles Tailwind arbitrary values like h-[50vh] or bg-[#ff0000]
  • Variant library support:
    • tailwind-variants: Validates classes in tv() function calls including base, variants, compoundVariants, slots, and class/className override properties
    • class-variance-authority: Validates classes in cva() function calls including base classes, variants, compoundVariants, and class/className override properties

Installation

Install the plugin as a dependency:

npm install tailwind-typescript-plugin
# or
yarn add tailwind-typescript-plugin
# or
pnpm add tailwind-typescript-plugin

Configuration

1. Add the plugin to your tsconfig.json

Add the plugin to the compilerOptions.plugins array in your tsconfig.json:

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "tailwind-typescript-plugin",
        "globalCss": "./src/global.css",
        "utilityFunctions": ["clsx", "cn", "classnames"],
        "allowedClasses": ["custom-button", "app-header", "project-card"],
        "variants": {
          "tailwindVariants": true,
          "classVarianceAuthority": true
        }
      }
    ]
  }
}

Configuration options:

  • globalCss (required): Path to your global CSS file that imports Tailwind CSS. This can be relative to your project root.

  • allowedClasses (optional): Array of custom class names that should be treated as valid alongside Tailwind classes. Useful for project-specific or third-party utility classes that aren't part of Tailwind.

    • Default: [] (no custom classes allowed)
    • Example:
      {
        "allowedClasses": ["custom-button", "app-header", "project-card"]
      }
    • Classes in this list will be considered valid and won't trigger validation errors
    • Works with all extraction patterns (literals, expressions, functions, arrays, etc.)
    • Combines with Tailwind classes - both are validated independently
  • variants (optional): Configure which variant library extractors to enable. This is useful for performance optimization when you only use one library.

    • Default behavior (no config): Both tailwind-variants and class-variance-authority are enabled
    • Selective enabling: If you specify ANY variant config, only those explicitly set to true are enabled
    • Example configurations:
      // Enable only tailwind-variants
      {
        "variants": {
          "tailwindVariants": true
        }
      }
      
      // Enable only class-variance-authority
      {
        "variants": {
          "classVarianceAuthority": true
        }
      }
      
      // Enable both explicitly
      {
        "variants": {
          "tailwindVariants": true,
          "classVarianceAuthority": true
        }
      }
      
      // No config = both enabled by default
      {
        // variants not specified - both libraries validated
      }
    • Performance impact: Disabling unused extractors skips TypeChecker operations and symbol resolution for that library, providing faster validation
  • utilityFunctions (optional): Array of additional function names to validate. These will be merged with the defaults, so you don't lose the common ones.

    • Defaults (always included): ['clsx', 'cn', 'classnames', 'classNames', 'cx', 'cva', 'twMerge', 'tv']

    • Add your own: Provide custom function names that will be added to the defaults

    • Example config:

      {
        "utilityFunctions": ["myCustomFn", "buildClasses"]
      }

      This will validate: clsx, cn, classnames, classNames, cx, cva, twMerge, tv, myCustomFn, buildClasses

    • Supported patterns:

      // Simple calls (validated by default):
      className={clsx('flex', 'items-center')}
      className={cn('flex', 'items-center')}
      
      // Member expressions (nested property access):
      className={utils.cn('flex', 'items-center')}
      className={lib.clsx('flex', 'items-center')}
      
      // Custom functions (add via config):
      className={myCustomFn('flex', 'items-center')}
      className={buildClasses('flex', 'items-center')}
      
      // Dynamic calls (ignored, won't throw errors):
      className={functions['cn']('flex', 'items-center')}

2. Ensure your CSS file imports Tailwind

Your global CSS file (referenced in globalCss) should import Tailwind CSS:

@import "tailwindcss";

Or using the traditional approach:

@tailwind base;
@tailwind components;
@tailwind utilities;

3. Enable the plugin in your editor

VS Code

The plugin should work automatically if you have the TypeScript version from your workspace selected. To ensure this:

  1. Open a TypeScript or TSX file
  2. Open the command palette (Cmd+Shift+P on Mac, Ctrl+Shift+P on Windows/Linux)
  3. Type "TypeScript: Select TypeScript Version"
  4. Choose "Use Workspace Version"

You may need to restart the TypeScript server:

  • Open command palette
  • Type "TypeScript: Restart TS Server"

Other Editors

Most editors that support TypeScript Language Service plugins should work automatically. Refer to your editor's documentation for TypeScript plugin configuration.

What it validates

Valid classes:

// ✅ Standard Tailwind classes
<div className="flex items-center justify-center">
  <p className="text-lg font-bold text-blue-500">Hello World</p>
</div>

// ✅ Arbitrary values
<div className="h-[50vh] w-[100px] bg-[#ff0000]">
  <p className="p-[20px] text-[14px]">Custom values</p>
</div>

// ✅ CSS variables (Tailwind v3.1+)
<div
  className="
    [--card-bg:#1e293b]
    [--card-radius:16px]
    bg-[var(--card-bg)]
    rounded-[var(--card-radius)]
    p-4
  "
>
  CSS custom properties
</div>

// ✅ Variants (responsive, state, etc.)
<div className="hover:bg-blue-500 md:flex lg:grid-cols-3 dark:text-white">
  Responsive and state variants
</div>

// ✅ Parenthesized expressions
<div className={(isError ? 'bg-red-500' : 'bg-blue-500')}>
  Parenthesized ternary
</div>

// ✅ Type assertions
<div className={('flex items-center' as string)}>
  Type assertion
</div>

// ✅ Non-null assertions
<div className={className!}>Non-null assertion</div>

// ✅ Variable references (resolved to their values)
const validClass = 'flex items-center';
<div className={validClass}>Variable reference</div>

// ✅ Variables in arrays
const baseClass = 'flex';
<div className={[baseClass, 'items-center']}>Array with variable</div>

// ✅ Variables in tv() and cva()
const buttonBase = 'font-semibold text-white';
const button = tv({ base: buttonBase });

Invalid classes are flagged:

// ❌ Invalid class name
<div className="random-class">Invalid class</div>
// Error: The class "random-class" is not a valid Tailwind class

// ❌ Mix of valid and invalid
<div className="random-class container mx-auto">Mixed classes</div>
// Error: The class "random-class" is not a valid Tailwind class

// ❌ Invalid variant
<div className="invalid-variant:bg-blue-500">Bad variant</div>
// Error: The class "invalid-variant:bg-blue-500" is not a valid Tailwind class

// ❌ Invalid class in variable (error points to declaration)
const invalidClass = 'not-a-tailwind-class';
// Error: The class "not-a-tailwind-class" is not a valid Tailwind class.
//        This value is used as className via variable "invalidClass" on line 5
<div className={invalidClass}>Variable with invalid class</div>

// ❌ Invalid class in variable used in array
const badClass = 'invalid-array-class';
// Error: The class "invalid-array-class" is not a valid Tailwind class.
//        This value is used as className via variable "badClass" on line 8
<div className={['flex', badClass]}>Array with invalid variable</div>

Custom allowed classes:

// Configuration in tsconfig.json:
// "allowedClasses": ["custom-button", "app-header", "project-card"]

// ✅ Valid: Custom allowed class
<div className="custom-button">Custom button</div>

// ✅ Valid: Custom allowed classes with Tailwind
<div className="custom-button flex items-center bg-blue-500">
  Mixed custom and Tailwind
</div>

// ✅ Valid: Multiple custom allowed classes
<div className="custom-button app-header project-card">
  Multiple custom classes
</div>

// ❌ Invalid: Custom class NOT in allowed list
<div className="not-in-config">Not configured</div>
// Error: The class "not-in-config" is not a valid Tailwind class

tailwind-variants validation:

import { tv } from 'tailwind-variants';
import { tv as myTv } from 'tailwind-variants'; // Import aliasing supported!

// ✅ Valid tv() usage
const button = tv({
  base: 'font-semibold text-white text-sm py-1 px-4 rounded-full',
  variants: {
    color: {
      primary: 'bg-blue-500 hover:bg-blue-700',
      secondary: 'bg-purple-500 hover:bg-purple-700'
    }
  }
});

// ✅ Valid: Array syntax
const buttonArray = tv({
  base: ['font-semibold', 'text-white', 'px-4', 'py-2'],
  variants: {
    color: {
      primary: ['bg-blue-500', 'hover:bg-blue-700']
    }
  }
});

// ✅ Valid: Import aliasing
const buttonAliased = myTv({
  base: 'flex items-center gap-2'
});

// ❌ Invalid class in base
const invalid = tv({
  base: 'font-semibold invalid-class text-white'
  // Error: The class "invalid-class" is not a valid Tailwind class
});

// ❌ Invalid class in variant
const invalidVariant = tv({
  base: 'font-semibold',
  variants: {
    color: {
      primary: 'bg-blue-500 wrong-class'
      // Error: The class "wrong-class" is not a valid Tailwind class
    }
  }
});

// ❌ Invalid class in array
const invalidArray = tv({
  base: ['font-semibold', 'invalid-array-class', 'text-white']
  // Error: The class "invalid-array-class" is not a valid Tailwind class
});

// ✅ Valid: class override at call site
<button className={button({ color: 'primary', class: 'bg-pink-500 hover:bg-pink-700' })}>
  Override
</button>

// ❌ Invalid: class override with invalid class
<button className={button({ color: 'primary', class: 'invalid-override-class' })}>
  // Error: The class "invalid-override-class" is not a valid Tailwind class
</button>

class-variance-authority validation:

import { cva } from 'class-variance-authority';
import { cva as myCva } from 'class-variance-authority'; // Import aliasing supported!

// ✅ Valid cva() usage
const button = cva(['font-semibold', 'border', 'rounded'], {
  variants: {
    intent: {
      primary: ['bg-blue-500', 'text-white', 'border-transparent'],
      secondary: ['bg-white', 'text-gray-800', 'border-gray-400']
    },
    size: {
      small: ['text-sm', 'py-1', 'px-2'],
      medium: ['text-base', 'py-2', 'px-4']
    }
  }
});

// ✅ Valid: String syntax for base
const buttonString = cva('font-semibold border rounded', {
  variants: {
    intent: {
      primary: 'bg-blue-500 text-white'
    }
  }
});

// ✅ Valid: Import aliasing
const buttonAliased = myCva(['flex', 'items-center', 'gap-2']);

// ❌ Invalid class in base array
const invalid = cva(['font-semibold', 'invalid-class', 'border']);
// Error: The class "invalid-class" is not a valid Tailwind class

// ❌ Invalid class in variant
const invalidVariant = cva(['font-semibold'], {
  variants: {
    intent: {
      primary: 'bg-blue-500 wrong-class'
      // Error: The class "wrong-class" is not a valid Tailwind class
    }
  }
});

// ✅ Valid: class override at call site
<button className={button({ intent: 'primary', class: 'bg-pink-500 hover:bg-pink-700' })}>
  Override
</button>

// ❌ Invalid: class override with invalid class
<button className={button({ intent: 'primary', class: 'invalid-override-class' })}>
  // Error: The class "invalid-override-class" is not a valid Tailwind class
</button>

Implemented features

Note on examples: Each feature has a corresponding test file in example/src/ following the naming pattern [context]-[pattern].tsx where:

  • Context = the container (literal, expression, template, function, array, object, tv)
  • Pattern = what's inside (static, variable, binary, ternary, mixed)
  • Literal Staticliteral-static.tsx Validates string literal className attributes Example: className="flex invalid-class"

  • Expression Staticexpression-static.tsx Validates JSX expressions with string literals Example: className={'flex invalid-class'}

  • Template Variabletemplate-variable.tsx Validates template literals with variable interpolation Example: className={flex ${someClass} invalid-class}

  • Template Ternarytemplate-ternary.tsx Validates template literals with conditional expressions Example: className={flex ${isActive ? 'invalid-class' : ''}}

  • Template Binarytemplate-binary.tsx Validates template literals with binary expressions Example: className={flex ${isError && 'invalid-class'}}

  • Function Staticfunction-static.tsx Validates function calls with static arguments Example: className={clsx('flex', 'invalid-class')}

  • Function Binaryfunction-binary.tsx Validates function calls with binary expressions Example: className={clsx('flex', isError && 'invalid-class')}

  • Function Ternaryfunction-ternary.tsx Validates function calls with conditional expressions Example: className={clsx('flex', isActive ? 'invalid-class' : 'bg-gray-500')}

  • Expression Binaryexpression-binary.tsx Validates direct binary expressions Example: className={isError && 'invalid-class'}

  • Expression Ternaryexpression-ternary.tsx Validates direct conditional expressions Example: className={isActive ? 'invalid-class' : 'bg-gray-500'}

  • Array Staticarray-static.tsx Validates array literals Example: className={cn(['flex', 'invalid-class'])}

  • Array Binaryarray-binary.tsx Validates array literals with binary expressions Example: className={cn(['flex', isError && 'invalid-class'])}

  • Array Ternaryarray-ternary.tsx Validates array literals with conditional expressions Example: className={cn(['flex', isActive ? 'invalid-class' : 'bg-gray-500'])}

  • Object Staticobject-static.tsx Validates object literal keys Example: className={clsx({ 'invalid-class': true })} or className={clsx({ 'invalid-class': isActive })}

  • Array Nestedarray-nested.tsx Validates nested arrays Example: className={cn([['flex', 'invalid-class']])} or className={cn([['flex'], [['items-center'], 'invalid-class']])}

  • Object Array Valuesobject-array-values.tsx Validates arrays as object property values Example: className={clsx({ flex: ['items-center', 'invalid-class'] })}

  • Mixed Complexmixed-complex.tsx Validates kitchen sink complex nesting with all patterns combined Example: className={clsx('flex', [1 && 'bar', { baz: ['invalid-class'] }])}

  • TV Statictv-static.tsx Validates tailwind-variants tv() function definitions Example: const styles = tv({ base: 'invalid-class', variants: { size: { sm: 'invalid-class' } } })

  • TV Class Overridetv-class-override.tsx Validates tailwind-variants class/className property overrides at call site Example: button({ color: 'primary', class: 'invalid-class' })

  • CVA Staticcva-static.tsx Validates class-variance-authority cva() function definitions Example: const button = cva(['invalid-class'], { variants: { intent: { primary: 'invalid-class' } } })

  • CVA Class Overridecva-class-override.tsx Validates class-variance-authority class/className property overrides at call site Example: button({ intent: 'primary', class: 'invalid-class' })

  • Allowed Classesallowed-classes.tsx Validates custom classes configured via allowedClasses config option Example: className="custom-button app-header" (where these are in the allowedClasses config)

  • Expression Parenthesizedexpression-parenthesized.tsx Validates parenthesized expressions and type assertions Example: className={(isError ? 'bg-red-500' : 'bg-blue-500')}, className={('flex' as string)}, className={expr!}

  • CSS Variablescss-variables.tsx Validates CSS custom properties (variables) using arbitrary property syntax Example: className="[--card-bg:#1e293b] bg-[var(--card-bg)]"

  • Expression Variableexpression-variable.tsx Validates variable references by resolving to their declared string values Example: const dynamicClass = 'invalid-class'; <div className={dynamicClass}>Dynamic</div>

  • Variable in Arraystest-variable-in-array.tsx Validates variables used inside array expressions Example: const myClass = 'invalid-class'; <div className={[myClass, 'flex']}>Array</div>

  • Variable in Objectstest-variable-in-object.tsx Validates variables used in computed object property keys Example: const myClass = 'invalid-class'; <div className={{ [myClass]: true }}>Object</div>

  • TV Variabletv-variable.tsx Validates variables in tailwind-variants tv() definitions Example: const baseClasses = 'invalid-class'; const button = tv({ base: baseClasses })

  • CVA Variablecva-variable.tsx Validates variables in class-variance-authority cva() definitions Example: const baseClasses = 'invalid-class'; const button = cva(baseClasses)

How It Works

The plugin hooks into the TypeScript Language Service and:

  1. Parses your TSX/JSX files to find className attributes, tv() calls, and cva() calls
  2. Extracts individual class names from className strings, tv() configurations, and cva() configurations
  3. Validates each class against your Tailwind CSS configuration
  4. Reports invalid classes as TypeScript errors in your editor

Performance Optimizations

The plugin is designed for minimal performance impact:

  • Import caching: Detects tailwind-variants and class-variance-authority imports once per file
  • Early bailout: Skips tv()/cva() validation for files without respective library imports
  • Configurable extractors: Disable unused variant libraries via variants config for better performance
  • Smart traversal: Only processes JSX elements and call expressions
  • Fast paths: Optimized hot paths for common patterns (string literals)
  • Lazy validation: Tailwind design system loaded on-demand
  • Symbol caching: TypeChecker results cached to avoid redundant type resolution

Typical overhead: <1ms per file for most files, ~2-3ms for files with many tv()/cva() calls

Development

This is a monorepo using Yarn workspaces:

# Install dependencies
yarn install

# Build the plugin
yarn build

# Build all packages
yarn build:all

# Set up e2e tests
yarn setup-e2e

Project Structure

├── packages/
│   ├── plugin/          # The TypeScript plugin package
│   └── e2e/             # End-to-end test examples

Publishing

This project uses an automated publishing workflow with beta releases on every commit and manual stable releases.

Beta Releases (Automatic)

Every commit to main automatically publishes a beta version to npm:

Commit to main → Auto-publishes 1.0.33-beta.1
Commit to main → Auto-publishes 1.0.33-beta.2
Commit to main → Auto-publishes 1.0.33-beta.3

Users can install beta versions:

npm install typescript-custom-plugin@beta

Stable Releases (Manual)

To publish a stable release:

  1. Go to Actions tab on GitHub
  2. Click "Stable Release" workflow
  3. Click "Run workflow" button
  4. Select version bump type:
    • patch: Bug fixes (1.0.32 → 1.0.33)
    • minor: New features (1.0.32 → 1.1.0)
    • major: Breaking changes (1.0.32 → 2.0.0)
  5. Click "Run workflow"

The workflow will:

  • Run tests and build
  • Bump version in package.json
  • Create git tag
  • Publish to npm as @latest
  • Create GitHub Release

Version Timeline Example

package.json: 1.0.32

Day 1: Commit → Publishes 1.0.33-beta.1
Day 2: Commit → Publishes 1.0.33-beta.2
Day 3: Click "Stable Release" (patch) → Publishes 1.0.33
Day 4: Commit → Publishes 1.0.34-beta.1 (starts over)

Requirements

  • NPM_TOKEN: Set in GitHub repository secrets (Settings → Secrets → Actions)
    • Create at npmjs.com → Access Tokens → Generate New Token (Automation)

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

License

MIT

Author

Ivan Rodriguez Calleja

About

TypeScript Language Service plugin that validates Tailwind CSS class names in JSX/TSX files. Catches typos and invalid classes in real-time.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages