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.
md:, lg:), state (hover:, focus:), and other variantsh-[50vh] or bg-[#ff0000]tv() function calls including base, variants, compoundVariants, slots, and class/className override propertiescva() function calls including base classes, variants, compoundVariants, and class/className override propertiesInstall the plugin as a dependency:
npm install tailwind-typescript-plugin
# or
yarn add tailwind-typescript-plugin
# or
pnpm add tailwind-typescript-plugin
tsconfig.jsonAdd 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.
[] (no custom classes allowed){
"allowedClasses": ["custom-button", "app-header", "project-card"]
}
variants (optional): Configure which variant library extractors to enable. This is useful for performance optimization when you only use one library.
tailwind-variants and class-variance-authority are enabledtrue are enabled// 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
}
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:
You may need to restart the TypeScript 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 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>
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.tsx
Validates string literal className attributes
Example: className="flex invalid-class"
Expression Static → expression-static.tsx
Validates JSX expressions with string literals
Example: className={'flex invalid-class'}
Template Variable → template-variable.tsx
Validates template literals with variable interpolation
Example: className={flex ${someClass} invalid-class}
Template Ternary → template-ternary.tsx
Validates template literals with conditional expressions
Example: className={flex ${isActive ? 'invalid-class' : ''}}
Template Binary → template-binary.tsx
Validates template literals with binary expressions
Example: className={flex ${isError && 'invalid-class'}}
Function Static → function-static.tsx
Validates function calls with static arguments
Example: className={clsx('flex', 'invalid-class')}
Function Binary → function-binary.tsx
Validates function calls with binary expressions
Example: className={clsx('flex', isError && 'invalid-class')}
Function Ternary → function-ternary.tsx
Validates function calls with conditional expressions
Example: className={clsx('flex', isActive ? 'invalid-class' : 'bg-gray-500')}
Expression Binary → expression-binary.tsx
Validates direct binary expressions
Example: className={isError && 'invalid-class'}
Expression Ternary → expression-ternary.tsx
Validates direct conditional expressions
Example: className={isActive ? 'invalid-class' : 'bg-gray-500'}
Array Static → array-static.tsx
Validates array literals
Example: className={cn(['flex', 'invalid-class'])}
Array Binary → array-binary.tsx
Validates array literals with binary expressions
Example: className={cn(['flex', isError && 'invalid-class'])}
Array Ternary → array-ternary.tsx
Validates array literals with conditional expressions
Example: className={cn(['flex', isActive ? 'invalid-class' : 'bg-gray-500'])}
Object Static → object-static.tsx
Validates object literal keys
Example: className={clsx({ 'invalid-class': true })} or className={clsx({ 'invalid-class': isActive })}
Array Nested → array-nested.tsx
Validates nested arrays
Example: className={cn([['flex', 'invalid-class']])} or className={cn([['flex'], [['items-center'], 'invalid-class']])}
Object Array Values → object-array-values.tsx
Validates arrays as object property values
Example: className={clsx({ flex: ['items-center', 'invalid-class'] })}
Mixed Complex → mixed-complex.tsx
Validates kitchen sink complex nesting with all patterns combined
Example: className={clsx('flex', [1 && 'bar', { baz: ['invalid-class'] }])}
TV Static → tv-static.tsx
Validates tailwind-variants tv() function definitions
Example: const styles = tv({ base: 'invalid-class', variants: { size: { sm: 'invalid-class' } } })
TV Class Override → tv-class-override.tsx
Validates tailwind-variants class/className property overrides at call site
Example: button({ color: 'primary', class: 'invalid-class' })
CVA Static → cva-static.tsx
Validates class-variance-authority cva() function definitions
Example: const button = cva(['invalid-class'], { variants: { intent: { primary: 'invalid-class' } } })
CVA Class Override → cva-class-override.tsx
Validates class-variance-authority class/className property overrides at call site
Example: button({ intent: 'primary', class: 'invalid-class' })
Allowed Classes → allowed-classes.tsx
Validates custom classes configured via allowedClasses config option
Example: className="custom-button app-header" (where these are in the allowedClasses config)
Expression Parenthesized → expression-parenthesized.tsx
Validates parenthesized expressions and type assertions
Example: className={(isError ? 'bg-red-500' : 'bg-blue-500')}, className={('flex' as string)}, className={expr!}
CSS Variables → css-variables.tsx
Validates CSS custom properties (variables) using arbitrary property syntax
Example: className="[--card-bg:#1e293b] bg-[var(--card-bg)]"
Expression Variable → expression-variable.tsx
Validates 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.tsx
Validates variables used inside array expressions
Example: const myClass = 'invalid-class'; <div className={[myClass, 'flex']}>Array</div>
Variable in Objects → test-variable-in-object.tsx
Validates variables used in computed object property keys
Example: const myClass = 'invalid-class'; <div className={{ [myClass]: true }}>Object</div>
TV Variable → tv-variable.tsx
Validates variables in tailwind-variants tv() definitions
Example: const baseClasses = 'invalid-class'; const button = tv({ base: baseClasses })
CVA Variable → cva-variable.tsx
Validates 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:
className attributes, tv() calls, and cva() callsThe plugin is designed for minimal performance impact:
variants config for better performanceTypical 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@beta
To publish a stable release:
The workflow will:
package.json@latestpackage.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)
Contributions are welcome! Please feel free to submit issues or pull requests.
MIT
Ivan Rodriguez Calleja