tailwind-typescript-plugin Svelte Themes

Tailwind Typescript Plugin

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

Tailwind TypeScript Plugin

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
  • Duplicate class detection: Warns when the same class appears multiple times in the same className attribute
  • Conflicting class detection: Warns when Tailwind utilities that affect the same CSS property are used together (e.g., text-left text-center)
  • 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"
      }
    ]
  }
}

Full configuration example with all options:

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "tailwind-typescript-plugin",
        "globalCss": "./src/global.css",
        "libraries": {
          "utilities": {
            "cn": "@/lib/utils",
            "merge": "tailwind-merge",
            "myFn": "*"
          },
          "variants": {
            "tailwindVariants": true,
            "classVarianceAuthority": true
          }
        },
        "validation": {
          "enabled": true,
          "severity": "error",
          "allowedClasses": ["custom-*", "app-*"]
        },
        "lint": {
          "enabled": true,
          "conflictingClasses": {
            "enabled": true,
            "severity": "warning"
          },
          "repeatedClasses": {
            "enabled": true,
            "severity": "warning"
          }
        },
        "editor": {
          "enabled": true,
          "autocomplete": {
            "enabled": true
          },
          "hover": {
            "enabled": true
          }
        }
      }
    ]
  }
}

Configuration options:

globalCss (required)

Path to your global CSS file that imports Tailwind CSS. This can be relative to your project root.


libraries (optional)

Configure utility functions and variant libraries.

libraries.utilities

Configure which utility functions to scan for class names. Keys are function names, values control import matching:

Value Description
"*" Match any import source
"off" Disable this utility function
"package-name" Only match if imported from this package

Defaults (automatically included):

{
  "cn": "*",
  "clsx": "clsx",
  "classnames": "classnames",
  "classNames": "classnames",
  "cx": "classnames",
  "twMerge": "tailwind-merge"
}

Example - Add custom utility and disable a default:

{
  "libraries": {
    "utilities": {
      "myMerge": "@/lib/utils",
      "clsx": "off"
    }
  }
}

Supported import patterns:

// Named import
import { merge } from '@/lib/utils';
className={merge('flex', 'items-center')} // ✅ Validated

// Default import
import merge from '@/lib/utils';
className={merge('flex', 'items-center')} // ✅ Validated

// Namespace import
import * as utils from '@/lib/utils';
className={utils.merge('flex', 'items-center')} // ✅ Validated

// Aliased import
import { something as merge } from '@/lib/utils';
className={merge('flex', 'items-center')} // ✅ Validated
libraries.variants

Configure which variant library extractors to enable.

Option Default Description
tailwindVariants true Enable tv() function support
classVarianceAuthority true Enable cva() function support

Example - Enable only tailwind-variants:

{
  "libraries": {
    "variants": {
      "tailwindVariants": true,
      "classVarianceAuthority": false
    }
  }
}

validation (optional)

Configure invalid class detection.

Option Default Description
enabled true Enable/disable invalid class validation
severity "error" "error" | "warning" | "suggestion" | "off"
allowedClasses [] Custom class patterns to allow
validation.allowedClasses

Array of custom class names or wildcard patterns that should be treated as valid.

Pattern Description Example Matches
prefix-* Matches classes starting with prefix custom-* custom-button, custom-card
*-suffix Matches classes ending with suffix *-icon arrow-icon, close-icon
*-contains-* Matches classes containing the string *-component-* app-component-header
exact Exact match (no wildcards) my-class Only my-class

Example:

{
  "validation": {
    "allowedClasses": ["custom-*", "*-icon", "exact-class"]
  }
}

lint (optional)

Configure lint rules for code quality issues.

Option Default Description
enabled true Master switch for all lint rules
conflictingClasses { enabled: true, severity: "warning" } Detect conflicting utilities
repeatedClasses { enabled: true, severity: "warning" } Detect duplicate classes

Example - Disable conflicting class detection:

{
  "lint": {
    "conflictingClasses": {
      "enabled": false
    }
  }
}

Example - Make duplicate classes an error:

{
  "lint": {
    "repeatedClasses": {
      "severity": "error"
    }
  }
}

editor (optional)

Configure editor features like autocomplete and hover.

Option Default Description
enabled true Master switch for all editor features
autocomplete.enabled true Enable Tailwind class autocomplete
hover.enabled true Enable hover information showing CSS

Example - Disable autocomplete:

{
  "editor": {
    "autocomplete": {
      "enabled": false
    }
  }
}

Backwards Compatibility

The following deprecated options are still supported but will be removed in a future version:

Deprecated New Location
utilityFunctions libraries.utilities
variants libraries.variants
allowedClasses validation.allowedClasses
enableLogging Removed (no replacement)

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>

// ✅ Spread operator in arrays
const baseClasses = ['flex', 'items-center'];
<div className={[...baseClasses, 'p-4']}>Spread in array</div>

// ✅ Spread operator in function calls
<div className={cn(...baseClasses, 'p-4')}>Spread in function</div>

// ✅ Multiple spreads
const layoutClasses = ['flex', 'items-center'];
const spacingClasses = ['p-4', 'm-2'];
<div className={cn(...layoutClasses, ...spacingClasses)}>Multiple spreads</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>

// ❌ Invalid class in spread operator
const invalidClasses = ['flex', 'invalid-spread-class'];
// Error: The class "invalid-spread-class" is not a valid Tailwind class.
<div className={[...invalidClasses, 'items-center']}>Spread with invalid</div>

// ❌ Invalid class in spread within function call
<div className={cn(...invalidClasses, 'p-4')}>Function spread with invalid</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

Duplicate class detection:

The plugin detects duplicate classes within the same className attribute and shows warnings. This helps keep your class lists clean and avoids redundancy.

// ⚠️ Warning: Duplicate class
<div className="flex flex items-center">Duplicate</div>
// Warning: Duplicate class "flex"

// ⚠️ Warning: Multiple duplicates
<div className="flex items-center flex items-center p-4">Multiple</div>
// Warning: Duplicate class "flex"
// Warning: Duplicate class "items-center"

// ⚠️ Warning: Duplicates in utility functions
<div className={clsx('flex', 'flex', 'items-center')}>In clsx</div>
// Warning: Duplicate class "flex"

// ⚠️ Warning: Duplicates across arguments
<div className={cn('flex bg-blue-500', 'items-center bg-blue-500')}>Across args</div>
// Warning: Duplicate class "bg-blue-500"

// ⚠️ Warning: Duplicates in arrays
<div className={cn(['flex', 'flex', 'items-center'])}>In array</div>
// Warning: Duplicate class "flex"

// ⚠️ Warning: Duplicates with spread operators
const baseClasses = ['flex', 'items-center'];
<div className={cn(...baseClasses, 'flex', 'items-center', 'p-4')}>Spread duplicates</div>
// Warning: Duplicate class "flex"
// Warning: Duplicate class "items-center"

// ⚠️ Warning: Duplicates between multiple spreads
const layoutClasses = ['flex', 'items-center'];
const containerClasses = ['flex', 'justify-center'];
<div className={cn(...layoutClasses, ...containerClasses)}>Multiple spread duplicates</div>
// Warning: Duplicate class "flex"

// ✅ Valid: Same class in DIFFERENT elements (not duplicates)
<div className="flex items-center">
  <span className="flex justify-center">Different elements</span>
</div>

Ternary expressions with duplicates:

The plugin intelligently handles ternary (conditional) expressions:

// ⚠️ Warning: Class at ROOT and in ternary branches = true duplicate
<div className={clsx('flex', isActive ? 'flex bg-blue-500' : 'flex bg-gray-500')}>
  Duplicate
</div>
// Warning: Duplicate class "flex" (on both branch occurrences)
// The root 'flex' is always applied, so the branch 'flex' classes are redundant

// ⚠️ Warning: Class repeated in BOTH branches (refactoring suggestion)
<div className={clsx('mt-4', isActive ? 'flex bg-blue-500' : 'flex bg-gray-500')}>
  Consider extracting
</div>
// Warning: Class "flex" is repeated in both branches. Consider moving it outside the conditional.
// This is not an error - only one branch executes at runtime - but it's a DRY improvement

// ✅ Valid: Class in only ONE branch
<div className={clsx('mt-4', isActive ? 'flex bg-blue-500' : 'bg-gray-500')}>
  Valid
</div>
// No warning - 'flex' only appears in the true branch

// ⚠️ Warning: Duplicate WITHIN the same branch
<div className={clsx('mt-4', isActive ? 'flex flex bg-blue-500' : 'bg-gray-500')}>
  Duplicate in branch
</div>
// Warning: Duplicate class "flex" (appears twice in the true branch)

Variables with conditional content:

The plugin resolves variables and detects duplicates even when variables contain ternary expressions:

// Variable with ternary content
const dynamicClasses = isActive ? 'flex bg-blue-500' : 'flex bg-gray-500';

// ⚠️ Warning: Root 'flex' + variable's ternary with 'flex' = duplicate
<div className={clsx('flex', dynamicClasses)}>Duplicate</div>
// Warning: Duplicate class "flex" (on both branch occurrences in the variable)

// ⚠️ Warning: Variable ternary with 'flex' in both branches (extractable)
<div className={clsx('mt-4', dynamicClasses)}>Consider extracting</div>
// Warning: Class "flex" is repeated in both branches. Consider moving it outside the conditional.

// Variable with ternary in only one branch
const conditionalOneBranch = isActive ? 'flex bg-blue-500' : 'bg-gray-500';

// ✅ Valid: Variable ternary with class in only ONE branch
<div className={clsx('mt-4', conditionalOneBranch)}>Valid</div>
// No warning - 'flex' only appears in the true branch of the variable

Conflicting class detection:

The plugin detects when you use multiple Tailwind utilities that affect the same CSS property. This catches common mistakes where conflicting styles cancel each other out.

// ⚠️ Warning: Text alignment conflict (both affect text-align)
<div className="text-left text-center">Conflicting alignment</div>
// Warning: Class "text-left" conflicts with "text-center". Both affect the text-align property.
// Warning: Class "text-center" conflicts with "text-left". Both affect the text-align property.

// ⚠️ Warning: Display conflict (both affect display)
<div className="flex block">Conflicting display</div>
// Warning: Class "flex" conflicts with "block". Both affect the display property.
// Warning: Class "block" conflicts with "flex". Both affect the display property.

// ⚠️ Warning: Position conflict
<div className="absolute relative">Conflicting position</div>
// Warning: Class "absolute" conflicts with "relative". Both affect the position property.

// ⚠️ Warning: Flex direction conflict
<div className="flex-row flex-col">Conflicting direction</div>
// Warning: Class "flex-row" conflicts with "flex-col". Both affect the flex-direction property.

// ⚠️ Warning: Justify content conflict
<div className="justify-start justify-center">Conflicting justify</div>
// Warning: Class "justify-start" conflicts with "justify-center". Both affect the justify-content property.

// ⚠️ Warning: Align items conflict
<div className="items-start items-center">Conflicting alignment</div>
// Warning: Class "items-start" conflicts with "items-center". Both affect the align-items property.

// ✅ Valid: Different utilities (no conflict)
<div className="flex items-center justify-between">No conflict</div>

// ✅ Valid: Same class in different elements
<div className="text-left">
  <span className="text-center">Different elements</span>
</div>

Responsive and state variants:

The plugin correctly handles responsive and state variants - conflicts are only flagged when the same variant prefix is used:

// ⚠️ Warning: Same variant prefix = conflict
<div className="md:text-left md:text-center">Conflict at md breakpoint</div>
// Warning: Both classes affect text-align at the md: breakpoint

// ✅ Valid: Different breakpoints = no conflict
<div className="sm:text-left md:text-center lg:text-right">No conflict</div>
// Each breakpoint has its own text-align value

// ⚠️ Warning: Same state variant = conflict
<div className="hover:flex hover:block">Conflict on hover</div>
// Warning: Both classes affect display on hover

// ✅ Valid: Different state variants = no conflict
<div className="hover:flex focus:block">No conflict</div>
// Different states trigger different display values

Ternary conditional expressions:

The plugin intelligently handles ternary expressions - conflicts are NOT flagged between mutually exclusive branches:

// ✅ Valid: Different ternary branches = no conflict
<div className={clsx(isActive ? 'text-left' : 'text-center')}>No conflict</div>
// Only one branch executes at runtime

// ⚠️ Warning: Root conflicts with branch
<div className={clsx('text-left', isActive ? 'text-center' : 'bg-gray-500')}>Conflict</div>
// Warning: 'text-left' at root conflicts with 'text-center' in true branch

// ⚠️ Warning: Conflict within same branch
<div className={clsx(isActive ? 'text-left text-center' : 'bg-gray-500')}>Conflict</div>
// Warning: Conflict within the true branch

Conflicts in utility functions:

// ⚠️ Warning: Conflicts detected in clsx/cn arguments
<div className={clsx('flex', 'block', 'items-center')}>Conflict</div>
// Warning: Class "flex" conflicts with "block". Both affect the display property.

<div className={cn('text-left', 'text-right')}>Conflict</div>
// Warning: Both classes affect text-align

Conflicts with spread operators:

// ⚠️ Warning: Conflicts with spread operator
const baseClasses = ['flex', 'p-4'];
<div className={cn(...baseClasses, 'p-2', 'items-center')}>Spread conflict</div>
// Warning: Class "p-4" conflicts with "p-2". Both affect the padding property.

// ⚠️ Warning: Conflicts between multiple spreads
const smallText = ['text-sm', 'font-medium'];
const largeText = ['text-lg', 'font-bold'];
<div className={cn(...smallText, ...largeText)}>Multiple spread conflicts</div>
// Warning: Class "text-sm" conflicts with "text-lg". Both affect the font-size property.
// Warning: Class "font-medium" conflicts with "font-bold". Both affect the font-weight property.

Conflicts in tv() and cva():

The plugin detects conflicts within tv() and cva() base properties, but intelligently skips conflicts between base and variants since variants are designed to override base styles:

import { tv } from 'tailwind-variants';
import { cva } from 'class-variance-authority';

// ⚠️ Warning: Conflict in tv() base
const button = tv({
  base: 'flex block items-center'
  // Warning: "flex" conflicts with "block"
});

// ✅ Valid: Base vs variant = NO conflict (intentional override)
const button2 = tv({
  base: 'text-left items-center',
  variants: {
    align: {
      center: 'text-center'  // This is designed to override base
    }
  }
});
// No warning - variants intentionally override base styles

// ⚠️ Warning: Conflict in cva() base
const card = cva(['flex', 'grid', 'items-center']);
// Warning: "flex" conflicts with "grid"

// ✅ Valid: Base vs variant = NO conflict
const card2 = cva(['justify-start'], {
  variants: {
    centered: {
      true: ['justify-center']  // Designed to override
    }
  }
});
// No warning - variant overrides base

// ✅ Valid: Different tv()/cva() calls = no conflict
const buttonA = tv({ base: 'text-left' });
const buttonB = tv({ base: 'text-center' });
// No warning - each call has its own scope

Detected conflict groups:

The plugin detects conflicts in these CSS property groups:

CSS Property Conflicting Classes
text-align text-left, text-center, text-right, text-justify, text-start, text-end
display block, inline-block, inline, flex, inline-flex, grid, inline-grid, hidden, table, contents, flow-root, list-item
position static, relative, absolute, fixed, sticky
flex-direction flex-row, flex-row-reverse, flex-col, flex-col-reverse
justify-content justify-start, justify-end, justify-center, justify-between, justify-around, justify-evenly, justify-stretch, justify-normal
align-items items-start, items-end, items-center, items-baseline, items-stretch
visibility visible, invisible, collapse
overflow overflow-auto, overflow-hidden, overflow-clip, overflow-visible, overflow-scroll
font-style italic, not-italic
text-transform uppercase, lowercase, capitalize, normal-case
whitespace whitespace-normal, whitespace-nowrap, whitespace-pre, whitespace-pre-line, whitespace-pre-wrap, whitespace-break-spaces
text-wrap text-wrap, text-nowrap, text-balance, text-pretty
cursor cursor-auto, cursor-default, cursor-pointer, cursor-wait, cursor-text, cursor-move, cursor-help, cursor-not-allowed, cursor-none, cursor-context-menu, cursor-progress, cursor-cell, cursor-crosshair, cursor-vertical-text, cursor-alias, cursor-copy, cursor-no-drop, cursor-grab, cursor-grabbing, cursor-all-scroll, cursor-col-resize, cursor-row-resize, cursor-n-resize, cursor-e-resize, cursor-s-resize, cursor-w-resize, cursor-ne-resize, cursor-nw-resize, cursor-se-resize, cursor-sw-resize, cursor-ew-resize, cursor-ns-resize, cursor-nesw-resize, cursor-nwse-resize, cursor-zoom-in, cursor-zoom-out
And more... flex-wrap, align-content, align-self, justify-items, justify-self, float, clear, box-sizing, isolation, object-fit, pointer-events, resize, user-select, table-layout, border-collapse, caption-side

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>

Duplicate detection in tv() and cva():

The plugin detects duplicate classes within a single tv() or cva() call. Classes are scoped per call, so the same class in different calls is NOT a duplicate.

import { tv } from 'tailwind-variants';
import { cva } from 'class-variance-authority';

// ⚠️ Warning: Duplicate in tv() base
const button = tv({
  base: 'flex flex items-center'
  // Warning: Duplicate class "flex"
});

// ⚠️ Warning: Duplicate across tv() base and variants
const button2 = tv({
  base: 'flex items-center',
  variants: {
    size: {
      sm: 'flex text-sm'
      // Warning: Duplicate class "flex" (already in base)
    }
  }
});

// ⚠️ Warning: Duplicate in tv() slots
const card = tv({
  slots: {
    base: 'flex items-center',
    icon: 'flex mr-2'
    // Warning: Duplicate class "flex" (all slots share scope)
  }
});

// ⚠️ Warning: Duplicate in cva() base array
const button3 = cva(['flex', 'flex', 'items-center']);
// Warning: Duplicate class "flex"

// ⚠️ Warning: Duplicate across cva() base and variants
const button4 = cva(['flex', 'items-center'], {
  variants: {
    intent: {
      primary: ['flex', 'bg-blue-500']
      // Warning: Duplicate class "flex" (already in base)
    }
  }
});

// ✅ Valid: Same class in DIFFERENT tv()/cva() calls
const button5 = tv({ base: 'flex items-center' });
const card2 = tv({ base: 'flex justify-center' });
// No warning - each call has its own scope

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>

  • Spread Operatortest-spread-operator/ Validates spread operators in arrays and function calls Example: const base = ['flex', 'invalid']; <div className={[...base, 'p-4']}>Spread</div>

    • Spread in arrays: className={[...baseClasses, 'p-4']}
    • Spread in function calls: className={cn(...baseClasses, 'p-4')}
    • Multiple spreads: className={cn(...layoutClasses, ...spacingClasses)}
    • Nested spreads: Variables containing arrays with spreads are resolved
    • Detects invalid classes, duplicates, and conflicts within spread expressions
  • 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)

  • Duplicate Classesduplicate-classes/ Detects duplicate classes within the same className attribute Example: className="flex flex items-center" shows warning on both flex occurrences

  • Duplicate Classes in Ternaryduplicate-classes/ Smart detection of duplicates in ternary expressions:

    • Root + branch duplicate: clsx('flex', isActive ? 'flex' : 'flex') → Warning on all occurrences
    • Both branches same class: clsx('mt-4', isActive ? 'flex' : 'flex') → Warning (consider extracting)
    • Single branch only: clsx('mt-4', isActive ? 'flex' : '') → No warning (valid pattern)
  • Duplicate Classes in Variables with Conditionalsduplicate-classes/ Resolves variables containing ternary expressions and detects duplicates:

    • const x = isActive ? 'flex' : 'flex'; clsx('flex', x) → Warning on all occurrences
    • const x = isActive ? 'flex' : 'flex'; clsx('mt-4', x) → Warning (consider extracting from variable)
    • const x = isActive ? 'flex' : ''; clsx('mt-4', x) → No warning (single branch)
  • TV Duplicate Classestv-duplicate-classes.tsx Detects duplicate classes within a single tv() definition (base, variants, compoundVariants, slots) Example: tv({ base: 'flex flex' }) → Warning: Duplicate class "flex"

    • Duplicates detected across base, variants, compoundVariants, and slots
    • Same class in different tv() calls is NOT a duplicate (scoped per call)
  • CVA Duplicate Classescva-duplicate-classes.tsx Detects duplicate classes within a single cva() definition (base, variants, compoundVariants) Example: cva(['flex', 'flex']) → Warning: Duplicate class "flex"

    • Duplicates detected across base array/string, variants, and compoundVariants
    • Same class in different cva() calls is NOT a duplicate (scoped per call)
  • Conflicting Classesconflicting-classes.tsx Detects conflicting Tailwind utilities that affect the same CSS property Example: className="text-left text-center" → Warning on both classes

    • Detects conflicts in 50+ CSS property groups (display, position, text-align, flex-direction, etc.)
    • Handles responsive/state variants: md:text-left md:text-center = conflict, sm:text-left md:text-center = no conflict
    • Smart ternary handling: mutually exclusive branches don't conflict
    • Works with clsx, cn, tv(), cva() and other utility functions
    • tv()/cva() base vs variant: NO conflict (variants are designed to override base)
  • Utility Function Import Verificationutility-function-imports/ Validates custom utility functions with optional import source verification Example: { "name": "merge", "from": "@/lib/utils" } only validates merge() if imported from @/lib/utils

    • Supports simple string format (name-only matching) and object format (with import verification)
    • Named imports: import { merge } from '@/lib/utils'
    • Default imports: import merge from '@/lib/utils'
    • Aliased imports: import { something as merge } from '@/lib/utils'
    • Subpath matching: { "from": "my-pkg" } matches import from 'my-pkg/utils'
    • Functions from wrong import sources are skipped (prevents false positives)

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

Hot Reloading

The plugin automatically watches your global CSS file for changes. When you modify your Tailwind configuration or add custom classes in your CSS file, the plugin will:

  1. Detect changes - Watches the globalCss file specified in your tsconfig.json
  2. Reload the design system - Automatically re-parses your Tailwind configuration
  3. Clear caches - Invalidates all diagnostic caches
  4. Refresh diagnostics - Requests TypeScript to revalidate all open files

This means you can add new custom classes or modify your Tailwind theme, and the plugin will immediately recognize them without restarting your editor or TypeScript server.

/* global.css */
@import "tailwindcss";

/* Add a custom utility - plugin will recognize it after save */
@utility custom-gradient {
  background: linear-gradient(to right, #ff7e5f, #feb47b);
}
// This will be valid immediately after saving global.css
<div className="custom-gradient">Hot reloaded!</div>

Note: The file watcher uses debouncing (300ms) to avoid excessive reloads when making rapid changes.

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 tailwind-typescript-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

Top categories

Loading Svelte Themes