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.
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.
- Features
- Installation
- Configuration
- What it validates
- Implemented features
- How It Works
- Performance Optimizations
- Development
- Publishing
- Contributing
- License
- 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]orbg-[#ff0000] - Variant library support:
- tailwind-variants: Validates classes in
tv()function calls includingbase,variants,compoundVariants,slots, andclass/classNameoverride properties - class-variance-authority: Validates classes in
cva()function calls including base classes,variants,compoundVariants, andclass/classNameoverride properties
- tailwind-variants: Validates classes in
Install the plugin as a dependency:
npm install tailwind-typescript-plugin
# or
yarn add tailwind-typescript-plugin
# or
pnpm add tailwind-typescript-pluginAdd 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
- Default:
-
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-variantsandclass-variance-authorityare enabled - Selective enabling: If you specify ANY variant config, only those explicitly set to
trueare 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
- Default behavior (no config): Both
-
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')}
-
Your global CSS file (referenced in globalCss) should import Tailwind CSS:
@import "tailwindcss";Or using the traditional approach:
@tailwind base;
@tailwind components;
@tailwind utilities;The plugin should work automatically if you have the TypeScript version from your workspace selected. To ensure this:
- Open a TypeScript or TSX file
- Open the command palette (Cmd+Shift+P on Mac, Ctrl+Shift+P on Windows/Linux)
- Type "TypeScript: Select TypeScript Version"
- Choose "Use Workspace Version"
You may need to restart the TypeScript server:
- Open command palette
- Type "TypeScript: Restart TS Server"
Most editors that support TypeScript Language Service plugins should work automatically. Refer to your editor's documentation for TypeScript plugin configuration.
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 classtailwind-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>Note on examples: Each feature has a corresponding test file in
example/src/following the naming pattern[context]-[pattern].tsxwhere:
- Context = the container (literal, expression, template, function, array, object, tv)
- Pattern = what's inside (static, variable, binary, ternary, mixed)
-
Literal Static →
literal-static.tsxValidates string literalclassNameattributes Example:className="flex invalid-class" -
Expression Static →
expression-static.tsxValidates JSX expressions with string literals Example:className={'flex invalid-class'} -
Template Variable →
template-variable.tsxValidates template literals with variable interpolation Example:className={flex ${someClass} invalid-class} -
Template Ternary →
template-ternary.tsxValidates template literals with conditional expressions Example:className={flex ${isActive ? 'invalid-class' : ''}} -
Template Binary →
template-binary.tsxValidates template literals with binary expressions Example:className={flex ${isError && 'invalid-class'}} -
Function Static →
function-static.tsxValidates function calls with static arguments Example:className={clsx('flex', 'invalid-class')} -
Function Binary →
function-binary.tsxValidates function calls with binary expressions Example:className={clsx('flex', isError && 'invalid-class')} -
Function Ternary →
function-ternary.tsxValidates function calls with conditional expressions Example:className={clsx('flex', isActive ? 'invalid-class' : 'bg-gray-500')} -
Expression Binary →
expression-binary.tsxValidates direct binary expressions Example:className={isError && 'invalid-class'} -
Expression Ternary →
expression-ternary.tsxValidates direct conditional expressions Example:className={isActive ? 'invalid-class' : 'bg-gray-500'} -
Array Static →
array-static.tsxValidates array literals Example:className={cn(['flex', 'invalid-class'])} -
Array Binary →
array-binary.tsxValidates array literals with binary expressions Example:className={cn(['flex', isError && 'invalid-class'])} -
Array Ternary →
array-ternary.tsxValidates array literals with conditional expressions Example:className={cn(['flex', isActive ? 'invalid-class' : 'bg-gray-500'])} -
Object Static →
object-static.tsxValidates object literal keys Example:className={clsx({ 'invalid-class': true })}orclassName={clsx({ 'invalid-class': isActive })} -
Array Nested →
array-nested.tsxValidates nested arrays Example:className={cn([['flex', 'invalid-class']])}orclassName={cn([['flex'], [['items-center'], 'invalid-class']])} -
Object Array Values →
object-array-values.tsxValidates arrays as object property values Example:className={clsx({ flex: ['items-center', 'invalid-class'] })} -
Mixed Complex →
mixed-complex.tsxValidates kitchen sink complex nesting with all patterns combined Example:className={clsx('flex', [1 && 'bar', { baz: ['invalid-class'] }])} -
TV Static →
tv-static.tsxValidatestailwind-variantstv() function definitions Example:const styles = tv({ base: 'invalid-class', variants: { size: { sm: 'invalid-class' } } }) -
TV Class Override →
tv-class-override.tsxValidatestailwind-variantsclass/className property overrides at call site Example:button({ color: 'primary', class: 'invalid-class' }) -
CVA Static →
cva-static.tsxValidatesclass-variance-authoritycva() function definitions Example:const button = cva(['invalid-class'], { variants: { intent: { primary: 'invalid-class' } } }) -
CVA Class Override →
cva-class-override.tsxValidatesclass-variance-authorityclass/className property overrides at call site Example:button({ intent: 'primary', class: 'invalid-class' }) -
Allowed Classes →
allowed-classes.tsxValidates custom classes configured viaallowedClassesconfig option Example:className="custom-button app-header"(where these are in the allowedClasses config) -
Expression Parenthesized →
expression-parenthesized.tsxValidates parenthesized expressions and type assertions Example:className={(isError ? 'bg-red-500' : 'bg-blue-500')},className={('flex' as string)},className={expr!} -
CSS Variables →
css-variables.tsxValidates CSS custom properties (variables) using arbitrary property syntax Example:className="[--card-bg:#1e293b] bg-[var(--card-bg)]" -
Expression Variable →
expression-variable.tsxValidates variable references by resolving to their declared string values Example:const dynamicClass = 'invalid-class'; <div className={dynamicClass}>Dynamic</div> -
Variable in Arrays →
test-variable-in-array.tsxValidates variables used inside array expressions Example:const myClass = 'invalid-class'; <div className={[myClass, 'flex']}>Array</div> -
Variable in Objects →
test-variable-in-object.tsxValidates variables used in computed object property keys Example:const myClass = 'invalid-class'; <div className={{ [myClass]: true }}>Object</div> -
TV Variable →
tv-variable.tsxValidates variables in tailwind-variants tv() definitions Example:const baseClasses = 'invalid-class'; const button = tv({ base: baseClasses }) -
CVA Variable →
cva-variable.tsxValidates variables in class-variance-authority cva() definitions Example:const baseClasses = 'invalid-class'; const button = cva(baseClasses)
The plugin hooks into the TypeScript Language Service and:
- Parses your TSX/JSX files to find
classNameattributes,tv()calls, andcva()calls - Extracts individual class names from className strings, tv() configurations, and cva() configurations
- Validates each class against your Tailwind CSS configuration
- Reports invalid classes as TypeScript errors in your editor
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
variantsconfig 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
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├── packages/
│ ├── plugin/ # The TypeScript plugin package
│ └── e2e/ # End-to-end test examples
This project uses an automated publishing workflow with beta releases on every commit and manual stable releases.
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@betaTo publish a stable release:
- Go to Actions tab on GitHub
- Click "Stable Release" workflow
- Click "Run workflow" button
- 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)
- 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
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)
- NPM_TOKEN: Set in GitHub repository secrets (Settings → Secrets → Actions)
- Create at npmjs.com → Access Tokens → Generate New Token (Automation)
Contributions are welcome! Please feel free to submit issues or pull requests.
MIT
Ivan Rodriguez Calleja