A 1:1 port of TailwindCSS 4.x to PHP
Generate Tailwind CSS using pure PHP — no Node.js required.
Built with AI — This entire codebase (57,000+ lines, 4,000+ tests) was generated using Claude Code. No manual coding was done.
TailwindCSS is written in TypeScript and requires Node.js. TailwindPHP is a complete rewrite in PHP, giving you the same functionality without the Node.js dependency.
- WordPress & PHP frameworks — Ship Tailwind-powered plugins and themes without requiring users to install Node.js or run build tools. Just
composer requireand go. See TailwindWP for a boilerplate WordPress block editor integration. - Dynamic CSS — Generate utility CSS at runtime based on user input, database content, or template variables.
- Simpler deployment — No Node.js in your production stack, no build step in CI, no npm in your Docker image.
Similar to how scssphp brought SCSS compilation to PHP, TailwindPHP brings the full Tailwind 4.x feature set to any PHP environment.
Building TailwindPHP created an opportunity to unify the Tailwind ecosystem's best tools into a single package. The JavaScript world relies on several companion libraries that are essential to any serious Tailwind project — so we ported them too:
- clsx — Conditionally construct class strings (
cn('btn', isActive && 'btn-active')) - tailwind-merge — Intelligently merge classes without conflicts (
merge('p-2', 'p-4')→'p-4') - CVA — Create component variants declaratively (
variants(['size' => ['sm', 'md', 'lg']]))
- Why This Exists
- Scope
- Status
- Installation
- CLI
- Usage
- API
- Classname Utilities
- Variants (CVA Port)
- Plugin System
- How It Works
- Testing
- Development
- License
- Credits
TailwindPHP provides full CSS compilation with support for both inline CSS and file-based imports.
// Inline CSS with Tailwind directives
$css = tw::generate('<div class="bg-brand p-4">', '@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');
// File-based imports
$css = tw::generate([
'content' => '<div class="bg-brand p-4">',
'importPaths' => '/path/to/styles.css',
]);What's included:
- All CSS compilation features (utilities, variants, directives, functions)
- File-based
@importresolution viaimportPathsoption - Virtual modules (
tailwindcss,tailwindcss/preflight,tailwindcss/utilities,tw-animate-css, etc.) - Preflight CSS reset
- Plugin system with
@tailwindcss/typography,@tailwindcss/forms, and custom plugin support cn(),variants(),merge(),join()— class name utilities (no separate packages needed)- No external dependencies beyond PHP
What's NOT included:
- IDE tooling — No IntelliSense, autocomplete, or source maps (these are editor features, not CSS compilation)
✅ 4,002 tests passing — Feature complete for core TailwindCSS functionality plus utility libraries.
| Test Suite | Tests | Status |
|---|---|---|
| Core (utilities, variants, integration) | 1,322 | ✅ |
| API Coverage (utilities, modifiers, variants, directives, plugins) | 1,774 | ✅ |
| API (tw::generate, tw::compile, tw::properties, tw::computedProperties, etc.) | 140 | ✅ |
| PHP-specific unit tests (theme, design-system, utils, helpers) | 300 | ✅ |
| Import functionality | 42 | ✅ |
| Edge cases | 57 | ✅ |
| CSS Minifier | 17 | ✅ |
| Plugin system (typography, forms) | 25 | ✅ |
| clsx (from reference test suite) | 27 | ✅ |
| tailwind-merge (from reference test suite) | 52 | ✅ |
| CVA (from reference test suite) | 50 | ✅ |
| tw-animate-css | 23 | ✅ |
| Cache | 14 | ✅ |
| CLI | 26 | ✅ |
While this is a 1:1 port focused on correctness and maintainability, PHP-specific optimizations are applied where possible:
- toCss: Uses array accumulation + implode instead of string concatenation, pre-computed indent strings
- CSS Parser: Direct character comparison instead of ord() calls, tracked buffer lengths instead of strlen()
These optimizations maintain identical output while improving performance. TypeScript remains faster due to V8's JIT compilation, but this is expected for build-time CSS generation.
See benchmarks/ for detailed comparison.
composer require tailwindphp/tailwindphpTailwindPHP includes a command-line interface that is a 1:1 port of @tailwindcss/cli - same options, same behavior, no Node.js required.
# Build CSS from an input file
./vendor/bin/tailwindphp -i ./src/app.css -o ./dist/styles.css
# Watch for changes
./vendor/bin/tailwindphp -i ./src/app.css -o ./dist/styles.css --watch
# Build minified
./vendor/bin/tailwindphp -i ./src/app.css -o ./dist/styles.css --minifytailwindphp [--input input.css] [--output output.css] [--watch] [options]
Options:
-i, --input Input CSS file (default: @import "tailwindcss")
-o, --output Output file (default: stdout)
-w, --watch Watch for changes and rebuild as needed
-m, --minify Optimize and minify the output
--optimize Optimize the output without minifying
--cwd The current working directory (default: .)
-h, --help Display usage informationCreate an app.css file with your Tailwind imports and @source directive:
@import "tailwindcss";
@source "./templates"; /* Directory to scan for classes */The @source directive tells TailwindPHP where to find your template files. It supports:
- Directories:
@source "./templates"; - Glob patterns:
@source "./src/**/*.php"; - Multiple sources: Add multiple
@sourcedirectives
# Build from CSS with @source directive
tailwindphp -i ./src/app.css -o ./dist/styles.css
# Build minified for production
tailwindphp -i ./src/app.css -o ./dist/styles.css -m
# Watch mode with minification
tailwindphp -i ./src/app.css -o ./dist/styles.css -w -m
# Use a different working directory
tailwindphp -i app.css -o dist/styles.css --cwd=/path/to/projectInstall globally to use tailwindphp from anywhere:
composer global require tailwindphp/tailwindphp
# Now available globally
tailwindphp -i ./src/app.css -o ./dist/styles.cssThe simplest way to use TailwindPHP is with the generate() function:
use TailwindPHP\tw;
// Generate CSS from HTML containing Tailwind classes
$css = tw::generate('<div class="flex items-center p-4 bg-blue-500">Hello</div>');This parses the HTML, extracts class names, and generates only the CSS needed.
You can pass configuration as either a second parameter or an array:
// Option 1: String as second parameter
$css = tw::generate($html, '@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');
// Option 2: Array with 'content' and 'css' keys
$css = tw::generate([
'content' => '<div class="flex p-4 bg-brand">Hello</div>',
'css' => '
@import "tailwindcss";
@theme {
--color-brand: #3b82f6;
--font-heading: "Inter", sans-serif;
}
.btn {
@apply px-4 py-2 rounded-lg bg-brand text-white;
}
'
]);Use importPaths to load CSS from the filesystem with full @import resolution:
// Single file
$css = tw::generate([
'content' => '<div class="flex btn">Hello</div>',
'importPaths' => '/path/to/styles.css',
]);
// Directory (loads all .css files alphabetically)
$css = tw::generate([
'content' => '<div class="flex btn">Hello</div>',
'importPaths' => '/path/to/css/',
]);
// Multiple paths
$css = tw::generate([
'content' => '<div class="flex btn card">Hello</div>',
'importPaths' => [
'/path/to/base.css',
'/path/to/components/',
],
]);
// Combine with inline CSS
$css = tw::generate([
'content' => '<div class="flex btn custom">Hello</div>',
'css' => '.custom { color: red; }',
'importPaths' => '/path/to/styles.css',
]);Nested imports are resolved automatically. If your CSS file contains @import "./buttons.css", TailwindPHP resolves the path relative to the importing file.
/* /path/to/styles.css */
@import "tailwindcss";
@import "./components/buttons.css";
@import "./components/cards.css";
@theme {
--color-brand: #3b82f6;
}Deduplication: Multiple imports of the same file or virtual module (like @import "tailwindcss") are automatically deduplicated.
Custom resolver: For advanced use cases (virtual file systems, databases), provide a callable:
$css = tw::generate([
'content' => '<div class="flex virtual-class">Hello</div>',
'importPaths' => function (?string $uri, ?string $fromFile): ?string {
if ($uri === null) {
return '@import "tailwindcss"; @import "custom.css";';
}
if ($uri === 'custom.css') {
return '.virtual-class { color: purple; }';
}
return null; // Unknown imports are silently skipped
},
]);Preflight is Tailwind's opinionated set of base styles, built on top of modern-normalize. It smooths over cross-browser inconsistencies and makes it easier to work within the constraints of the design system.
For full details on what Preflight does and why, see the official Tailwind Preflight documentation.
Key resets include:
- Default margins removed — Headings, paragraphs, lists, etc. have zero margin
- Headings unstyled — All headings have the same font-size and font-weight as normal text
- Lists unstyled —
ulandolhave no bullets/numbers or padding - Images are block-level — No more phantom space below images
- Border styles reset — Empty borders so you can add borders just by setting
border-width - Buttons inherit fonts — Buttons use the parent's font family, size, and line-height
- Hidden elements stay hidden — Elements with a
hiddenattribute are invisible unless usinghidden="until-found"
// Full import (includes theme + preflight + utilities)
$css = tw::generate([
'content' => '<div class="flex p-4">Hello</div>',
'css' => '@import "tailwindcss";',
]);Add your own base styles on top of Preflight using @layer base:
$css = tw::generate([
'content' => '<article><h1>Title</h1><p>Content</p></article>',
'css' => '
@import "tailwindcss";
@layer base {
h1 { font-size: var(--text-2xl); }
h2 { font-size: var(--text-xl); }
a { color: var(--color-blue-600); text-decoration-line: underline; }
}
',
]);To disable Preflight — for example when integrating into an existing project or defining your own base styles — import only the parts of Tailwind you need.
By default, @import "tailwindcss" is equivalent to:
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);
@import "tailwindcss/utilities.css" layer(utilities);To skip Preflight, simply omit its import:
$css = tw::generate([
'content' => '<div class="flex p-4">Hello</div>',
'css' => '
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
',
]);When importing Tailwind's files individually, modifiers like prefix(), theme(), and important should go on their respective imports:
/* Prefix affects both theme variables and utilities */
@import "tailwindcss/theme.css" layer(theme) prefix(tw);
@import "tailwindcss/utilities.css" layer(utilities) prefix(tw);
/* important affects utilities */
@import "tailwindcss/utilities.css" layer(utilities) important;
/* theme(static) or theme(inline) affects theme variables */
@import "tailwindcss/theme.css" layer(theme) theme(static);If you need to extract Tailwind class names from content separately:
use TailwindPHP\tw;
$classes = tw::extractCandidates('<div class="flex p-4" className="bg-blue-500">');
// ['flex', 'p-4', 'bg-blue-500']Minify CSS output for production:
use TailwindPHP\tw;
// Option 1: Minify during generation
$css = tw::generate([
'content' => '<div class="flex p-4">Hello</div>',
'minify' => true,
]);
// Option 2: Minify separately
$css = tw::generate('<div class="flex p-4">Hello</div>');
$minified = tw::minify($css);The minifier removes comments, collapses whitespace, shortens hex colors (#ffffff to #fff), removes units from zero values (0px to 0), and other optimizations.
Enable file-based caching to avoid recompiling identical content:
use TailwindPHP\tw;
// Cache to default directory (sys_get_temp_dir()/tailwindphp)
$css = tw::generate([
'content' => '<div class="flex p-4">Hello</div>',
'cache' => true,
]);
// Cache to custom directory
$css = tw::generate([
'content' => '<div class="flex p-4">Hello</div>',
'cache' => '/path/to/cache',
]);
// With TTL (time-to-live in seconds)
$css = tw::generate([
'content' => '<div class="flex p-4">Hello</div>',
'cache' => true,
'cacheTtl' => 3600, // Expire after 1 hour
]);The cache key is computed from the content, CSS configuration, and minify option. Different inputs generate different cache files.
Clear the cache:
use TailwindPHP\tw;
use function TailwindPHP\clearCache;
// Clear default cache
tw::clearCache();
// Clear custom cache directory
tw::clearCache('/path/to/cache');
// Or use the function
clearCache('/path/to/cache');TailwindPHP provides a comprehensive API for CSS generation and inspection.
Generate CSS from HTML content containing Tailwind classes.
Returns: string - The generated CSS
use TailwindPHP\tw;
// String input
$css = tw::generate('<div class="flex p-4">');
// String with custom CSS
$css = tw::generate('<div class="flex bg-brand">', '@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');
// Array input
$css = tw::generate([
'content' => '<div class="flex p-4">',
'css' => '@import "tailwindcss";',
'minify' => true,
]);Create a reusable TailwindCompiler instance for multiple operations.
Returns: TailwindCompiler - A reusable compiler instance
use TailwindPHP\tw;
// Create compiler with default Tailwind
$compiler = tw::compile();
// Create compiler with custom CSS
$compiler = tw::compile('@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');
// Generate CSS
$css = $compiler->generate('<div class="flex p-4 bg-brand">');
// Or minify the output
$minified = $compiler->minify($css);Get raw CSS properties for a class (unresolved CSS variables).
Returns: array<string, string> - Property name to value mapping
use TailwindPHP\tw;
// Single class
tw::properties('p-4');
// ['padding' => 'calc(var(--spacing) * 4)']
// Multiple classes (pass as array)
tw::properties(['flex', 'items-center', 'p-4']);
// ['display' => 'flex', 'align-items' => 'center', 'padding' => 'calc(var(--spacing) * 4)']
// From compiler instance
$compiler = tw::compile();
$compiler->properties('p-4');Get computed CSS properties with all variables resolved.
Returns: array<string, string> - Property name to resolved value mapping
use TailwindPHP\tw;
// Static method
tw::computedProperties('p-4');
// ['padding' => '1rem']
tw::computedProperties('text-blue-500');
// ['color' => 'oklch(.546 .245 262.881)']
// Multiple classes (pass as array)
tw::computedProperties(['flex', 'items-center', 'gap-4']);
// ['display' => 'flex', 'align-items' => 'center', 'gap' => '1rem']
// From compiler instance
$compiler = tw::compile();
$compiler->computedProperties('p-4');
// ['padding' => '1rem']Get raw value for a utility class (unresolved).
Returns: ?string - The raw CSS value, or null if utility not found
use TailwindPHP\tw;
// Static method
tw::value('p-4');
// 'calc(var(--spacing) * 4)'
tw::value('text-blue-500');
// 'var(--color-blue-500)'
// From compiler instance
$compiler = tw::compile();
$compiler->value('p-4');Get computed value for a utility class (resolved).
Returns: ?string - The resolved CSS value, or null if utility not found
use TailwindPHP\tw;
// Static method
tw::computedValue('p-4');
// '1rem'
tw::computedValue('text-blue-500');
// 'oklch(.546 .245 262.881)'
tw::computedValue('gap-4');
// '1rem'
// From compiler instance
$compiler = tw::compile();
$compiler->computedValue('p-4');
// '1rem'Extract Tailwind class names from content.
Returns: array<string> - Array of extracted class names
use TailwindPHP\tw;
tw::extractCandidates('<div class="flex p-4" className="bg-blue-500">');
// ['flex', 'p-4', 'bg-blue-500']
// From compiler instance
$compiler = tw::compile();
$compiler->extractCandidates('<div class="flex p-4">');
// ['flex', 'p-4']Minify CSS output.
Returns: string - The minified CSS
use TailwindPHP\tw;
$css = tw::generate('<div class="flex p-4">');
$minified = tw::minify($css);
// From compiler instance
$compiler = tw::compile();
$css = $compiler->generate('<div class="flex p-4">');
$minified = $compiler->minify($css);Clear the CSS cache.
Returns: int - Number of files deleted
use TailwindPHP\tw;
// Clear default cache
tw::clearCache();
// Clear custom cache directory
tw::clearCache('/path/to/cache');Get all color values from the theme.
Returns: array<string, string> - Map of color name to computed value
use TailwindPHP\tw;
tw::colors();
// ['red-500' => 'oklch(63.7% 0.237 25.331)', 'blue-500' => 'oklch(62.3% 0.214 259.815)', ...]
// With custom theme
tw::colors('@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');
// [..., 'brand' => '#3b82f6']
// From compiler instance
$compiler = tw::compile();
$compiler->colors();Get all breakpoint values from the theme.
Returns: array<string, string> - Map of breakpoint name to value
use TailwindPHP\tw;
tw::breakpoints();
// ['sm' => '40rem', 'md' => '48rem', 'lg' => '64rem', 'xl' => '80rem', '2xl' => '96rem']
// With custom theme
tw::breakpoints('@import "tailwindcss"; @theme { --breakpoint-xs: 20rem; }');
// ['xs' => '20rem', 'sm' => '40rem', ...]
// From compiler instance
$compiler = tw::compile();
$compiler->breakpoints();Get custom spacing values from the theme.
Returns: array<string, string> - Map of spacing name to value
use TailwindPHP\tw;
// Note: TailwindCSS 4 uses a single --spacing base value, not --spacing-* namespace
// This returns any custom --spacing-* values defined in the theme
tw::spacing('@import "tailwindcss"; @theme { --spacing-huge: 10rem; }');
// ['huge' => '10rem']
// From compiler instance
$compiler = tw::compile();
$compiler->spacing();All static methods accept multiple input formats:
// Format 1: String only
tw::generate('<div class="flex">');
tw::properties('p-4');
// Format 2: String + CSS string
tw::generate('<div class="flex">', '@import "tailwindcss"; @theme { ... }');
tw::properties('bg-brand', '@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');
// Format 3: Array with 'content' and optional 'css'
tw::generate(['content' => '<div class="flex">', 'css' => '@import "tailwindcss";']);
tw::properties(['content' => 'p-4', 'css' => '@import "tailwindcss";']);When using tw::compile(), the returned compiler provides instance methods:
$compiler = tw::compile('@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');
// Generate CSS
$css = $compiler->generate('<div class="flex p-4 bg-brand">');
// Extract candidates
$candidates = $compiler->extractCandidates('<div class="flex p-4 bg-brand">');
// Get properties
$compiler->properties('bg-brand'); // ['background-color' => 'var(--color-brand)']
$compiler->computedProperties('bg-brand'); // ['background-color' => '#3b82f6']
// Get single values
$compiler->value('bg-brand'); // 'var(--color-brand)'
$compiler->computedValue('bg-brand'); // '#3b82f6'
// Minify CSS
$minified = $compiler->minify($css);
// Get theme values
$compiler->colors(); // All color values
$compiler->breakpoints(); // All breakpoint values
$compiler->spacing(); // Custom spacing values
// Access internals (advanced)
$compiler->getTheme(); // Theme object with resolved values
$compiler->getDesignSystem(); // Design system with utilities, variants, etc.
$compiler->getCompiled(); // Raw compiled state arrayTailwindPHP includes PHP ports of the popular Tailwind companion libraries. No additional packages required.
The recommended utility. Combines conditional class construction with intelligent conflict resolution.
use function TailwindPHP\cn;
// Basic usage - conflicts are resolved
cn('px-2 py-1', 'px-4');
// => 'py-1 px-4' (px-4 overrides px-2)
// Conditional classes
cn('btn', ['btn-primary' => true, 'btn-disabled' => false]);
// => 'btn btn-primary'
// React-style component
function Card(array $props = []): string {
$class = cn(
'rounded-lg border bg-card text-card-foreground shadow-sm',
$props['class'] ?? null
);
return "<div class=\"{$class}\">" . ($props['children'] ?? '') . "</div>";
}
Card(['class' => 'p-6', 'children' => 'Content']);Merge Tailwind classes, resolving conflicts. Later classes override earlier ones.
use function TailwindPHP\merge;
merge('px-2 py-1 bg-red-500', 'px-4 bg-blue-500');
// => 'py-1 px-4 bg-blue-500'
merge('hover:bg-red-500', 'hover:bg-blue-500');
// => 'hover:bg-blue-500'Join classes without conflict resolution. Use when you know there are no conflicts.
use function TailwindPHP\join;
join('foo', 'bar', null, 'baz');
// => 'foo bar baz'PHP port of CVA (Class Variance Authority) for creating component style variants.
Create component variants with declarative configuration.
use function TailwindPHP\variants;
// Define component styles
$button = variants([
'base' => 'inline-flex items-center justify-center rounded-md font-medium',
'variants' => [
'variant' => [
'default' => 'bg-primary text-white hover:bg-primary/90',
'outline' => 'border border-input bg-background hover:bg-accent',
'ghost' => 'hover:bg-accent hover:text-accent-foreground',
],
'size' => [
'default' => 'h-10 px-4 py-2',
'sm' => 'h-9 px-3',
'lg' => 'h-11 px-8',
],
],
'defaultVariants' => [
'variant' => 'default',
'size' => 'default',
],
]);
// Usage (React-style single props object)
$button(); // defaults applied
$button(['variant' => 'outline']); // override variant
$button(['size' => 'sm', 'class' => 'mt-4']); // override + custom class
// Use in a component function with cn() for easy class extension
function Button(array $props = []): string {
static $styles = null;
$styles ??= variants([
'base' => 'inline-flex items-center justify-center rounded-md font-medium',
'variants' => [
'variant' => [
'default' => 'bg-primary text-white hover:bg-primary/90',
'outline' => 'border border-input hover:bg-accent',
],
'size' => [
'default' => 'h-10 px-4 py-2',
'sm' => 'h-9 px-3',
],
],
'defaultVariants' => ['variant' => 'default', 'size' => 'default'],
]);
// cn() merges variant output with custom classes, resolving conflicts
$class = cn($styles($props), $props['class'] ?? null);
$text = $props['children'] ?? 'Button';
return "<button class=\"{$class}\">{$text}</button>";
}
// Custom classes override variant defaults via cn()
Button(['variant' => 'outline', 'size' => 'sm', 'class' => 'mt-4 px-8']);Merge multiple variant components into one.
use function TailwindPHP\variants;
use function TailwindPHP\compose;
$box = variants(['variants' => ['shadow' => ['sm' => 'shadow-sm', 'md' => 'shadow-md']]]);
$stack = variants(['variants' => ['gap' => ['1' => 'gap-1', '2' => 'gap-2']]]);
$card = compose($box, $stack);
$card(['shadow' => 'md', 'gap' => '2']); // => 'shadow-md gap-2'TailwindPHP includes PHP ports of official TailwindCSS plugins. These are 1:1 ports following the same logic as the JavaScript originals.
| Plugin | Description |
|---|---|
@tailwindcss/typography |
Beautiful typographic defaults for HTML content |
@tailwindcss/forms |
Form element reset and styling utilities |
Use the @plugin directive in your CSS:
$css = tw::generate([
'content' => '<article class="prose prose-lg"><h1>Hello</h1><p>Content here</p></article>',
'css' => '
@plugin "@tailwindcss/typography";
@import "tailwindcss/utilities.css";
'
]);Pass options using CSS block syntax:
// Typography with custom class name
$css = '
@plugin "@tailwindcss/typography" {
className: "markdown";
}
@import "tailwindcss/utilities.css";
';
// Forms with class strategy (no base styles)
$css = '
@plugin "@tailwindcss/forms" {
strategy: "class";
}
@import "tailwindcss/utilities.css";
';Generates the .prose class with beautiful typographic defaults:
// Basic usage
$css = tw::generate('<div class="prose">...</div>', '@plugin "@tailwindcss/typography"; @import "tailwindcss/utilities.css";');
// Available classes: prose, prose-sm, prose-lg, prose-xl, prose-2xl
// Modifiers: prose-invert (dark mode), prose-slate, prose-gray, etc.Provides form element utilities:
// Class strategy - explicit form classes
$css = tw::generate(
'<input class="form-input" /><select class="form-select">...</select>',
'@plugin "@tailwindcss/forms" { strategy: "class"; } @import "tailwindcss/utilities.css";'
);
// Available classes: form-input, form-textarea, form-select, form-multiselect,
// form-checkbox, form-radioYou can create your own plugins by implementing the PluginInterface:
use TailwindPHP\Plugin\PluginInterface;
use TailwindPHP\Plugin\PluginAPI;
class MyCustomPlugin implements PluginInterface
{
public function getName(): string
{
return 'my-custom-plugin';
}
public function __invoke(PluginAPI $api, array $options = []): void
{
// Add static utilities
$api->addUtilities([
'.btn' => [
'padding' => '0.5rem 1rem',
'border-radius' => '0.25rem',
'font-weight' => '600',
],
'.btn-primary' => [
'background-color' => 'blue',
'color' => 'white',
],
]);
// Add functional utilities with values
$api->matchUtilities(
[
'tab' => function ($value) {
return ['tab-size' => $value];
},
],
['values' => ['1' => '1', '2' => '2', '4' => '4', '8' => '8']]
);
// Add component classes
$api->addComponents([
'.card' => [
'background-color' => 'white',
'border-radius' => '0.5rem',
'padding' => '1rem',
'box-shadow' => '0 1px 3px rgba(0,0,0,0.1)',
],
]);
// Add custom variants
$api->addVariant('hocus', '&:hover, &:focus');
// Access theme values
$primary = $api->theme('colors.blue.500', '#3b82f6');
}
public function getThemeExtensions(array $options = []): array
{
return []; // Return theme additions if needed
}
}Register and use your plugin:
use TailwindPHP\tw;
use function TailwindPHP\registerPlugin;
// Register the plugin
registerPlugin(new MyCustomPlugin());
// Use it in CSS
$css = tw::generate(
'<div class="btn btn-primary card tab-4">...</div>',
'@plugin "my-custom-plugin"; @import "tailwindcss/utilities.css";'
);The plugin system follows the TailwindCSS plugin API pattern:
src/plugin.php # PluginInterface, PluginAPI, PluginManager
src/plugin/plugins/
├── typography-plugin.php # @tailwindcss/typography port
└── forms-plugin.php # @tailwindcss/forms port
PluginAPI provides the same methods as TailwindCSS:
addBase(array $css)— Add base stylesaddUtilities(array $utilities)— Add static utilitiesmatchUtilities(array $utilities, array $options)— Add functional utilities with valuesaddComponents(array $components)— Add component classesaddVariant(string $name, string|array $variant)— Add custom variantsmatchVariant(string $name, callable $callback, array $options)— Add functional variantstheme(string $path, mixed $default)— Access theme valuesconfig(string $path, mixed $default)— Access config values
The codebase mirrors TailwindCSS's structure — same file names, same organization:
src/
├── _tailwindphp/ # PHP-specific helpers (NOT part of the TailwindCSS port)
│ ├── LightningCss.php # CSS optimizations (lightningcss Rust library equivalent)
│ ├── CssMinifier.php # CSS minification
│ └── lib/ # Companion library ports
│ ├── clsx/ # clsx port (27 tests from reference)
│ ├── tailwind-merge/ # tailwind-merge port (52 tests from reference)
│ └── cva/ # CVA port (50 tests from reference)
│
├── plugin/ # Plugin system
│ └── plugins/ # Built-in plugin implementations
│ ├── typography-plugin.php # @tailwindcss/typography port
│ └── forms-plugin.php # @tailwindcss/forms port
│
├── utilities/ # Utility implementations (split from utilities.ts)
│ ├── accessibility.php # sr-only, forced-colors
│ ├── backgrounds.php # bg-*, gradient-*, from-*, via-*, to-*
│ ├── borders.php # border-*, rounded-*, divide-*, outline-*
│ ├── effects.php # shadow-*, opacity-*, mix-blend-*
│ ├── filters.php # blur-*, brightness-*, contrast-*, etc.
│ ├── flexbox.php # flex-*, grid-*, gap-*, justify-*, align-*
│ ├── interactivity.php # cursor-*, scroll-*, touch-*, select-*
│ ├── layout.php # display, position, z-*, overflow-*, etc.
│ ├── masks.php # mask-linear-*, mask-radial-*, mask-conic-*, mask-x/y/t/r/b/l-*
│ ├── sizing.php # w-*, h-*, min-*, max-*, size-*
│ ├── spacing.php # m-*, p-*, space-*
│ ├── svg.php # fill-*, stroke-*
│ ├── tables.php # border-collapse, table-layout
│ ├── transforms.php # translate-*, rotate-*, scale-*, skew-*
│ ├── transitions.php # transition-*, duration-*, ease-*, delay-*
│ └── typography.php # font-*, text-*, leading-*, tracking-*, text-shadow-*
│
├── utils/ # Helper functions (ported from utils/)
│
├── index.php # Main entry point, compile(), cn(), variants(), merge(), join()
├── ast.php # AST nodes and toCss()
├── candidate.php # Candidate parsing (class name → parts)
├── compile.php # Candidate to CSS compilation
├── css-functions.php # theme(), --theme(), --spacing(), --alpha()
├── css-parser.php # CSS parsing
├── design-system.php # Central registry for utilities/variants
├── plugin.php # Plugin system (PluginInterface, PluginAPI, PluginManager)
├── theme.php # Theme value resolution
├── utilities.php # Utility registration and lookup
├── value-parser.php # CSS value parsing
├── variants.php # Variant handling (hover, focus, responsive, etc.)
└── walk.php # AST traversal
Note: TailwindCSS's utilities.ts is 6,000+ lines. We split it into src/utilities/ (one file per category) for maintainability.
All implementation files are documented with @port-deviation markers explaining where and why the PHP implementation differs from TypeScript:
| Marker | Meaning |
|---|---|
@port-deviation:none |
Direct 1:1 port with no deviations |
@port-deviation:async |
PHP uses synchronous code (no async/await) |
@port-deviation:storage |
Different data structures (array vs Map/Set) |
@port-deviation:types |
PHPDoc instead of TypeScript types |
@port-deviation:sourcemaps |
Source map tracking omitted |
@port-deviation:omitted |
Entire module/feature not ported (outside scope) |
@port-deviation:errors |
Different error handling approach |
@port-deviation:enum |
PHP constants instead of TypeScript enums |
@port-deviation:dispatch |
Different function dispatch pattern |
@port-deviation:structure |
Different code organization |
@port-deviation:helper |
PHP-specific helper not in original |
Tests ensure the PHP port stays in sync with TailwindCSS's TypeScript implementation. We use two approaches:
-
Extraction-based tests — Tests automatically extracted from TailwindCSS's
.test.tsfiles using scripts intest-coverage/. These cover complex utilities, variants, and integration tests. -
Unit test ports — Direct PHP ports of simpler TypeScript test files (AST, parsing, escaping, etc.). These live alongside their source files as
*.test.php.
# Extract tests from TypeScript source
composer extract
# Run all tests
composer test
# Run specific test file
./vendor/bin/phpunit src/utilities.test.php
# Run tests matching a pattern
./vendor/bin/phpunit --filter="translate"
# Run library tests only
./vendor/bin/phpunit src/_tailwindphp/lib/- Extract — Scripts in
test-coverage/parse TailwindCSS's.test.tsfiles and extract test cases to JSON - Run — PHPUnit tests read extracted data and compare PHP output against expected CSS
- Verify — Any mismatch means the PHP port has drifted from TailwindCSS behavior
| Category | Tests | Source |
|---|---|---|
| Extraction-based | ||
| utilities.test.php | 547 | utilities.test.ts |
| variants.test.php | 139 | variants.test.ts |
| index.test.php | 78 | index.test.ts |
| css_functions.test.php | 60 | css-functions.test.ts |
| ui_spec.test.php | 68 | ui.spec.ts |
| Unit test ports | ||
| css_parser.test.php | 70 | css-parser.test.ts |
| candidate.test.php | 66 | candidate.test.ts |
| decode_arbitrary_value.test.php | 60 | decode-arbitrary-value.test.ts |
| ast.test.php | 18 | ast.test.ts |
| escape.test.php | 10 | escape.test.ts |
| + 12 more unit test files | ~180 | Various .test.ts files |
| Library tests | ||
| clsx.test.php | 27 | clsx/test/*.js |
| tailwind_merge.test.php | 52 | tailwind-merge/tests/*.ts |
| cva.test.php | 50 | cva/src/index.test.ts |
| API coverage tests | 1,684 | Custom exhaustive tests |
| Plugin tests | 25 | Plugin functionality |
- PHP 8.2+
- Composer
See CLAUDE.md for detailed development guide, project structure, and porting phases.
MIT
This project ports:
- TailwindCSS by Tailwind Labs
- clsx by Luke Edwards
- tailwind-merge by Dany Castillo
- CVA by Joe Bell