A 1:1 port of TailwindCSS 4.x to PHP. See README.md for scope and features.
- Critical Rules for LLMs
- Test System
- Development Workflow
- Project Structure
- Key Files
- Public API
- Common Patterns
- Debugging Tips
- Current Status
FORBIDDEN: Changing assertions, expected values, or normalization logic in *.test.php files to make failing tests pass.
ALLOWED: Only fix test infrastructure bugs (parsing issues, file loading) - never change expected outputs.
When a test fails, the fix belongs in the source code (src/*.php), not in the test file. The tests define the contract - we must match TailwindCSS output exactly.
TailwindCSS uses lightningcss (Rust) for CSS transformations. Since we can't use Rust in PHP, equivalent functionality goes in:
src/_tailwindphp/LightningCss.php
This includes:
- CSS nesting transformation (flattening
&selectors) @mediaquery hoistingcalc()simplification- Leading zero removal (
0.5→.5) - Transform function spacing
- Grid value normalization
src/_tailwindphp/ contains PHP-specific helpers that are NOT part of the TailwindCSS port:
LightningCss.php- CSS optimizations (lightningcss equivalent)CssMinifier.php- CSS minificationlib/- Companion library ports (clsx, tailwind-merge, cva)
Everything else in src/ should mirror TailwindCSS structure.
We include PHP ports of clsx and tailwind-merge because they are essential companion libraries in the Tailwind ecosystem:
- clsx - Conditional class name construction (used in virtually every Tailwind project)
- tailwind-merge - Intelligent class conflict resolution (
p-2 p-4→p-4) - cn() - Combines both (the pattern popularized by shadcn/ui)
By including these, TailwindPHP provides a complete Tailwind development experience without requiring Node.js for anything.
Important for LLMs: These libraries live in src/_tailwindphp/lib/ with their own namespace (TailwindPHP\Lib\*) to clearly separate them from the core TailwindCSS port. The public API functions (cn, clsx, twMerge, twJoin) are exposed in src/index.php under the main TailwindPHP namespace.
TailwindPHP includes PHP ports of official TailwindCSS plugins:
- @tailwindcss/typography - The prose class for beautiful typographic defaults
- @tailwindcss/forms - Form element reset and styling utilities
These are 1:1 ports following the exact same logic as the JavaScript originals. The plugin system architecture:
src/plugin.php # Core plugin system
├── PluginInterface # Contract for plugins (getName, __invoke, getThemeExtensions)
├── PluginAPI # API passed to plugins (addBase, addUtilities, addComponents, etc.)
└── PluginManager # Registry and execution of plugins
src/plugin/plugins/
├── typography-plugin.php # @tailwindcss/typography port
└── forms-plugin.php # @tailwindcss/forms port
test-coverage/plugins/
├── typography/ # Extracted typography tests
│ ├── summary.json
│ └── tests/*.json
└── extract-typography-tests.php # Extraction script
PluginAPI Methods (same 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 classesmatchComponents(array $components, array $options)- Add functional componentsaddVariant(string $name, string|array $variant)- Add custom variantsmatchVariant(string $name, callable $callback, array $options)- Add functional variantstheme(string $path, mixed $default)- Access theme values
Usage in CSS:
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms" {
strategy: "class";
}
@import "tailwindcss/utilities.css";Important for LLMs: The plugin implementations are PHP ports, not JavaScript execution. They follow the same logic and produce the same output, but are implemented natively in PHP. Tests are extracted from the reference JavaScript test files where available.
Creating Custom Plugins:
use TailwindPHP\Plugin\PluginInterface;
use TailwindPHP\Plugin\PluginAPI;
class MyPlugin implements PluginInterface
{
public function getName(): string
{
return 'my-plugin';
}
public function __invoke(PluginAPI $api, array $options = []): void
{
// Add static utilities
$api->addUtilities([
'.btn' => ['padding' => '0.5rem 1rem', 'border-radius' => '0.25rem'],
]);
// Add functional utilities (generates .tab-1, .tab-2, .tab-4, .tab-8)
$api->matchUtilities(
['tab' => fn($value) => ['tab-size' => $value]],
['values' => ['1' => '1', '2' => '2', '4' => '4', '8' => '8']]
);
// Add components
$api->addComponents(['.card' => ['background' => 'white', 'padding' => '1rem']]);
// Add variants
$api->addVariant('hocus', '&:hover, &:focus');
// Access theme
$color = $api->theme('colors.blue.500', '#3b82f6');
}
public function getThemeExtensions(array $options = []): array
{
return []; // Theme additions
}
}
// Register and use
registerPlugin(new MyPlugin());
$css = tw::generate($html, '@plugin "my-plugin"; @import "tailwindcss/utilities.css";');While this is a 1:1 port focused on correctness, PHP-specific optimizations are applied where they improve performance without changing output. These are marked with @port-deviation:performance in file headers.
Current optimizations:
ast.phptoCss(): Array accumulation + implode instead of string concatenation, pre-computed indent strings, standalone function instead of closure (~50% faster)css-parser.phpparse(): Direct character comparison instead of ord() calls, tracked buffer lengths instead of strlen() calls (~20-30% faster)
Guidelines for performance work:
- Always preserve identical output - tests must pass
- Document with
@port-deviation:performancein file header - Run benchmarks (
composer bench) to verify improvement - Focus on hot paths identified by profiling
The project uses Pint (formatting) and PHPStan (static analysis):
composer lint # Check formatting (Pint --test)
composer format # Fix formatting (Pint)
composer analyse # Static analysis (PHPStan level 4)
composer quality # Run both lint + analyseinline0 profile note: TailwindPHP is registered in _cp as a Tailwind reproduction special case. Its colocated .test.php files and Pint workflow are intentional because they preserve upstream Tailwind traceability; do not move tests into tests/Unit / tests/Integration or replace Pint with PHPCS for profile alignment.
PHPStan Notes:
- Level 4 is used (higher levels conflict with function-based architecture)
- Some patterns are intentionally ignored (dynamic arrays, cross-file functions)
- Source files are analyzed, test files are excluded
-
Extraction-based tests (
test-coverage/directory)- Tests extracted from TailwindCSS TypeScript source using extraction scripts
- Stored as
.jsonfiles intest-coverage/*/tests/ - PHP test files load these via DataProvider at runtime
- Used for complex tests (utilities, variants, integration, css-functions, ui.spec)
- Must pass completely - no exceptions
-
Unit test ports (
src/*.test.php)- Direct PHP ports of simpler TypeScript test files
- NOT extracted - manually ported from corresponding
.test.tsfiles - Used for: AST, parsing, escaping, candidate parsing, value parsing, etc.
- Examples:
ast.test.php←ast.test.ts,escape.test.php←escape.test.ts - Test structure and assertions mirror the TypeScript originals
test-coverage/
├── utilities/tests/ # From utilities.test.ts
├── variants/tests/ # From variants.test.ts
├── index/tests/ # From index.test.ts (JSON format)
├── css-functions/tests/ # From css-functions.test.ts
├── candidate/tests/ # From candidate.test.ts
├── ui-spec/tests/ # From tests/ui.spec.ts (Playwright browser tests)
└── extract-*.php # Extraction scripts
Located in test-coverage/:
extract-run-tests.php- Extracts utilities/variants testsextract-index-tests.php- Extracts index.test.ts to JSONextract-css-functions-tests.php- Extracts css-functions testsextract-candidate-tests.php- Extracts candidate testsextract-ui-spec-tests.php- Extracts ui.spec.ts browser tests
extract-clsx-tests.php- Extracts clsx tests from reference/clsx/test/extract-tailwind-merge-tests.php- Extracts tailwind-merge tests from reference/tailwind-merge/tests/extract-cva-tests.php- Extracts CVA tests from reference/cva/packages/cva/src/verify-test-counts.php- Verifies PHP tests match reference test counts
# Extract tests from TypeScript source
composer extract
# Extract companion library tests
composer extract-libs
# Extract all tests (core + libs)
composer extract-all
# Run all tests
composer test
# Run library tests only
composer test-libs
# Extract and run tests
composer extract-and-test
# Run specific test file
./vendor/bin/phpunit src/utilities.test.php
# Run tests matching a pattern
./vendor/bin/phpunit --filter="hover"
# Check current versions of all references
composer versions
# Update TailwindCSS reference
composer update-tailwind
# Update companion library references
composer update-libs# 1. Extract latest tests from TypeScript
composer extract
# 2. Run tests
composer test
# 3. If tests fail, fix in src/*.php (NOT in test files)
# 4. When all tests pass, commit and push
git add -A && git commit -m "message" && git push- Read the failure message - understand what output differs
- Check the source - find the relevant PHP implementation
- Compare with TypeScript - reference
reference/tailwindcss/ - Fix the source code - make PHP output match TypeScript
- Re-run tests - verify the fix
- First ensure
utilities.test.phppasses (364 tests) - Then
variants.test.php(144 tests) - Then
index.test.php(integration tests) - Then remaining test files
tailwindphp/
├── bin/
│ └── tailwindphp # CLI entry point
│
├── src/
│ ├── Cli/ # CLI (1:1 port of @tailwindcss/cli)
│ │ ├── Application.php # Main CLI application
│ │ └── Console/ # Input/Output handling
│ │ ├── Input.php
│ │ └── Output.php
│ │
│ ├── _tailwindphp/ # PHP-specific (NOT part of port)
│ │ ├── LightningCss.php # CSS transformations
│ │ ├── CssMinifier.php # CSS minification
│ │ └── lib/ # Companion library ports
│ │ ├── clsx/ # clsx port (27 tests)
│ │ │ ├── clsx.php
│ │ │ └── clsx.test.php
│ │ ├── tailwind-merge/ # tailwind-merge port (52 tests)
│ │ │ ├── index.php # Main entry, cn(), twMerge(), twJoin()
│ │ │ ├── default-config.php # tailwind-merge class group config
│ │ │ ├── merger.php # ClassNameMerger logic
│ │ │ ├── lru-cache.php
│ │ │ └── tailwind_merge.test.php
│ │ └── cva/ # CVA port (50 tests)
│ │ ├── cva.php # cva(), cx(), compose(), defineConfig()
│ │ └── cva.test.php
│ │
│ ├── plugin/ # Plugin system
│ │ └── plugins/ # Built-in plugin implementations
│ │ ├── typography-plugin.php # @tailwindcss/typography port
│ │ ├── typography_plugin.test.php
│ │ └── forms-plugin.php # @tailwindcss/forms port
│ │
│ ├── utilities/ # Split from utilities.ts
│ │ ├── accessibility.php
│ │ ├── backgrounds.php
│ │ ├── borders.php
│ │ └── ... (15 files)
│ │
│ ├── utils/ # Helper functions
│ │
│ ├── index.php # Main entry + public API (cn, cva, merge, join)
│ ├── plugin.php # Plugin system (PluginInterface, PluginAPI, PluginManager)
│ ├── *.php # Core implementation
│ └── *.test.php # Unit test files
│
├── tests/
│ ├── TestHelper.php # Test utilities
│ └── ui_spec.test.php # Browser/integration tests (from tests/ui.spec.ts)
│
├── test-coverage/
│ ├── */tests/ # Extracted test data
│ ├── lib/ # Library test extraction scripts
│ │ ├── extract-clsx-tests.php
│ │ ├── extract-tailwind-merge-tests.php
│ │ ├── extract-cva-tests.php
│ │ └── verify-test-counts.php
│ └── extract-*.php # Core extraction scripts
│
├── resources/
│ ├── theme.css # Default Tailwind theme (copied from reference)
│ ├── preflight.css # CSS reset (copied from reference)
│ ├── utilities.css # Utilities directive
│ └── index.css # Main entry point
│
├── reference/
│ ├── tailwindcss/ # Git submodule - TailwindCSS source
│ ├── tailwindcss-typography/ # Git submodule - @tailwindcss/typography source
│ ├── tailwindcss-forms/ # Git submodule - @tailwindcss/forms source
│ ├── clsx/ # Git submodule - clsx source
│ ├── tailwind-merge/ # Git submodule - tailwind-merge source
│ └── cva/ # Git submodule - CVA source
│
├── scripts/
│ ├── update-tailwind.php # Update TailwindCSS reference + copy CSS files
│ └── update-libs.php # Update companion library references
│
└── CLAUDE.md # This file
| File | Purpose |
|---|---|
src/index.php |
Main entry point, compile(), cn(), variants(), etc. |
src/at-import.php |
@import resolution and substitution |
tests/EdgeCasesTest.php |
Edge case tests for complex scenarios |
src/utilities.php |
Utility registration and compilation |
src/variants.php |
Variant handling (hover, focus, etc.) |
src/compile.php |
Candidate to CSS compilation |
src/design-system.php |
Central registry |
src/theme.php |
Theme value management |
src/ast.php |
AST nodes and toCss() |
src/plugin.php |
Plugin system (PluginInterface, PluginAPI, PluginManager) |
src/plugin/plugins/typography-plugin.php |
@tailwindcss/typography port |
src/plugin/plugins/forms-plugin.php |
@tailwindcss/forms port |
src/_tailwindphp/LightningCss.php |
CSS optimizations (for test parity) |
src/_tailwindphp/CssMinifier.php |
CSS minification |
src/_tailwindphp/lib/clsx/clsx.php |
clsx implementation |
src/_tailwindphp/lib/tailwind-merge/index.php |
tailwind-merge + cn() implementation |
src/_tailwindphp/lib/cva/cva.php |
CVA (class variance authority) implementation |
tests/ImportPathsTest.php |
Import paths feature tests |
tests/TestHelper.php |
TestHelper::run() for tests |
scripts/update-libs.php |
Update companion library references |
use TailwindPHP\tw;
// Generate CSS from HTML
$css = tw::generate('<div class="flex p-4">');
// With custom CSS input
$css = tw::generate($html, '@import "tailwindcss"; @theme { --color-brand: #3b82f6; }');
// With file-based imports
$css = tw::generate([
'content' => '<div class="flex p-4 btn">',
'importPaths' => '/path/to/styles.css',
]);
// With multiple import paths (files and directories)
$css = tw::generate([
'content' => $html,
'importPaths' => [
'/path/to/base.css',
'/path/to/components/', // Directory loads all .css files
],
]);
// Combine inline CSS with file imports
$css = tw::generate([
'content' => $html,
'css' => '.custom { color: red; }',
'importPaths' => '/path/to/styles.css',
]);
// Custom resolver (for virtual file systems, databases, etc.)
$css = tw::generate([
'content' => $html,
'importPaths' => function (?string $uri, ?string $fromFile): ?string {
if ($uri === null) return '@import "tailwindcss";';
// Return CSS content for uri, or null to skip
return null;
},
]);
// Extract class candidates from content
$classes = tw::extractCandidates('<div class="flex p-4">');
// Minify during generation
$css = tw::generate([
'content' => '<div class="flex p-4">',
'minify' => true,
]);
// Or minify separately
$css = tw::generate('<div class="flex p-4">');
$minified = tw::minify($css);
// Enable caching (default temp directory)
$css = tw::generate([
'content' => '<div class="flex p-4">',
'cache' => true,
]);
// Cache to custom directory with TTL
$css = tw::generate([
'content' => '<div class="flex p-4">',
'cache' => '/path/to/cache',
'cacheTtl' => 3600, // 1 hour
]);
// Clear cache
tw::clearCache(); // Default directory
tw::clearCache('/path/to/cache'); // Custom directoryCreate a reusable compiler instance for multiple operations:
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; }');
// Extract candidates and generate CSS
$candidates = $compiler->extractCandidates('<div class="flex p-4 bg-brand">');
$css = $compiler->css($candidates);
// Or minify the output
$minified = $compiler->minify($css);Get raw or computed CSS properties for classes:
use TailwindPHP\tw;
// Raw properties (unresolved CSS variables)
tw::properties('p-4');
// ['padding' => 'calc(var(--spacing) * 4)']
tw::properties(['flex', 'items-center', 'p-4']);
// ['display' => 'flex', 'align-items' => 'center', 'padding' => 'calc(var(--spacing) * 4)']
// Computed properties (resolved values)
tw::computedProperties('p-4');
// ['padding' => '1rem']
tw::computedProperties('text-blue-500');
// ['color' => 'oklch(.546 .245 262.881)']
// Single value extraction
tw::value('p-4'); // 'calc(var(--spacing) * 4)'
tw::computedValue('p-4'); // '1rem'
// From compiler instance
$compiler = tw::compile();
$compiler->properties('p-4');
$compiler->computedProperties('p-4');
$compiler->value('p-4');
$compiler->computedValue('p-4');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";']);use function TailwindPHP\cn;
use function TailwindPHP\merge;
use function TailwindPHP\join;
// cn() - Recommended: conditional classes + conflict resolution
cn('px-2 py-1', 'px-4'); // => 'py-1 px-4'
cn('btn', ['btn-primary' => $active]); // => 'btn btn-primary' (if $active)
// merge() - Conflict resolution only
merge('px-2 py-1', 'px-4'); // => 'py-1 px-4'
merge('hover:bg-red-500', 'hover:bg-blue-500'); // => 'hover:bg-blue-500'
// join() - Simple joining without conflict resolution
join('foo', 'bar', null); // => 'foo bar'
// React-style component using cn()
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']);use function TailwindPHP\variants;
use function TailwindPHP\compose;
// variants() - Create component variants (CVA port)
$button = variants([
'base' => 'btn font-semibold border rounded',
'variants' => [
'intent' => [
'primary' => 'bg-blue-500 text-white',
'secondary' => 'bg-gray-200 text-gray-800',
],
'size' => [
'sm' => 'text-sm px-2 py-1',
'md' => 'text-base px-4 py-2',
],
],
'defaultVariants' => [
'intent' => 'primary',
'size' => 'md',
],
]);
// Usage (React-style single props object)
$button(); // => 'btn font-semibold ... bg-blue-500 text-white text-base px-4 py-2'
$button(['intent' => 'secondary']); // => 'btn font-semibold ... bg-gray-200 text-gray-800 text-base px-4 py-2'
$button(['size' => 'sm', 'class' => 'my-class']); // Appends custom classes
// Example component using variants() + cn() for 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-primary-foreground 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'],
]);
// 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']);
// compose() - Merge multiple variant components
$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'When tests fail because of CSS transformation differences:
// In src/_tailwindphp/LightningCss.php
public static function someTransformation(string $value): string
{
// Implement the transformation that lightningcss does
return $transformedValue;
}Then call it from the appropriate place in the pipeline.
Tests that load from test-coverage/ follow this pattern:
class SomeTest extends TestCase
{
private static array $testCases = [];
private static function loadTestCases(): void
{
// Load from test-coverage/*/tests/
}
public static function dataProvider(): array
{
self::loadTestCases();
return self::$testCases;
}
#[DataProvider('dataProvider')]
public function test_case(array $test): void
{
// Run test and compare output
}
}When comparing CSS output:
- Normalize whitespace
- Normalize leading zeros
- Handle selector escaping differences
- Don't break pseudo-selectors (
:hover,:root)
When clsx or tailwind-merge releases a new version:
# Check current versions
composer versions
# Update all libraries to latest
composer update-libs
# Update specific library
php scripts/update-libs.php clsx
php scripts/update-libs.php tailwind-mergeThe update script will:
- Fetch the latest tag from the reference repo
- Checkout that tag
- Re-extract tests from the new source
- Update README badges automatically
- Run tests to verify compatibility
If tests fail after updating, it means the library made breaking changes that require updating the PHP implementation.
Tests for companion libraries are extracted from their original JavaScript/TypeScript test files:
clsx: Tests extracted from reference/clsx/test/index.js and classnames.js
- Parser handles JavaScript function call syntax
- Converts JS truthiness semantics to PHP (empty arrays are truthy in JS)
- Strips JS comments before parsing
tailwind-merge: Tests extracted from reference/tailwind-merge/tests/*.test.ts
- Parser handles TypeScript test syntax (
test(),expect().toBe()) - Processes escape sequences in string literals
- Some tests N/A (require custom config, extendTailwindMerge)
# The original TailwindCSS source is in:
reference/tailwindcss/packages/tailwindcss/src/./vendor/bin/phpunit --filter="test_name_here"./vendor/bin/phpunit --filter="test" --testdox// In test file
fwrite(STDERR, "Debug: " . print_r($value, true) . "\n");Total: 4,427 tests (all passing)
Reference: TailwindCSS v4.3.3 (see composer versions).
The extractors handle describe()-nested tests and every run() call shape
of the v4.3.3 test helper (inline arrays, Prettier-multiline calls, css
template second arguments, template-variable bindings, and the
{ polyfills: Polyfills.None } options argument).
| Test File | Status | Tests |
|---|---|---|
utilities.test.php |
✅ | 706 |
variants.test.php |
✅ | 164 |
index.test.php |
✅ | 148 (file-system/JS-runtime cases N/A) |
css_functions.test.php |
✅ | 64 (7 N/A for JS tooling) |
ui_spec.test.php |
✅ | 68 |
| Test File | Status | Tests |
|---|---|---|
plugin.test.php |
✅ | 9 (core plugin functionality) |
typography_plugin.test.php |
✅ | 16 (13 N/A - v3-specific behavior) |
forms_plugin.test.php |
✅ | 29 |
Plugin System:
plugin.php- Core plugin system (PluginInterface, PluginAPI, PluginManager)typography-plugin.php- @tailwindcss/typography portforms-plugin.php- @tailwindcss/forms port
Typography tests are extracted from reference/tailwindcss-typography/src/index.test.js. Tests marked N/A use v3-specific behavior (dark mode via .dark selector, responsive variants) that differs in v4.
PHP ports of utility libraries with tests extracted from their reference implementations:
| Library | Test File | Status | Tests | Reference |
|---|---|---|---|---|
| clsx | clsx.test.php |
✅ | 27 | 27/27 (100%) |
| tailwind-merge | tailwind_merge.test.php |
✅ | 52 | 52/67 applicable (78%) |
| cva | cva.test.php |
✅ | 50 | 50/60 extracted |
clsx - Class name string builder (TailwindPHP\Lib\Clsx\clsx)
- Reference: https://github.com/lukeed/clsx
- All 27 tests ported from index.js (12) and classnames.js (15)
tailwind-merge - Tailwind class conflict resolver (TailwindPHP\Lib\TailwindMerge\twMerge)
- Reference: https://github.com/dcastil/tailwind-merge
- 52 tests from 14 applicable test files
- 32 tests N/A (require custom config, extendTailwindMerge, etc.)
- Includes
cn()function that combines clsx + twMerge
cva - Class Variance Authority (TailwindPHP\Lib\Cva\cva)
- Reference: https://github.com/joe-bell/cva
- 50 tests for core functionality (cx, cva, compose, defineConfig)
- Declarative component variant management with base, variants, compoundVariants, defaultVariants
| Test File | Status | Tests |
|---|---|---|
CssMinifier.test.php |
✅ | 17 |
Tests cover comment removal, whitespace collapsing, hex color shortening, zero unit removal, font-weight shortening, empty rule removal, and public API integration.
| Test File | Status | Tests |
|---|---|---|
UtilitiesTest.php |
✅ | 904 |
ModifiersTest.php |
✅ | 338 |
VariantsTest.php |
✅ | 282 |
DirectivesTest.php |
✅ | 189 |
PluginsTest.php |
✅ | 61 |
ApiTest.php |
✅ | 140 |
These tests provide exhaustive coverage of the TailwindPHP public API including:
- All utility classes with various values and modifiers
- Color opacity combinations (5-95 in increments of 5)
- All responsive, state, and pseudo-class variants
- Deep variant stacking (3-4 levels)
- Arbitrary values for all utility types
- Container queries, aria/data attributes
- @apply, @theme, @utility, @layer directives
- @import virtual modules (tailwindcss/preflight, tailwindcss/theme, tailwindcss/utilities)
- @layer order declarations and layer() modifiers
- preflight option for easy CSS reset inclusion
- @plugin directive and plugin system (typography, forms)
- tw::generate(), tw::compile(), tw::properties(), tw::computedProperties(), tw::value(), tw::computedValue()
- TailwindCompiler instance methods for reusable compilation
- All input formats (string, string+css, array with content/css keys)
| Test File | Status | Tests |
|---|---|---|
css_parser.test.php |
✅ | 70 |
candidate.test.php |
✅ | 66 |
decode_arbitrary_value.test.php |
✅ | 60 |
constant_fold_declaration.test.php |
✅ | 63 |
selector_parser.test.php |
✅ | 40 |
attribute_selector_parser.test.php |
✅ | 20 |
value_parser.test.php |
✅ | 19 |
ast.test.php |
✅ | 18 |
walk.test.php |
✅ | 16 |
compare.test.php |
✅ | 14 |
brace_expansion.test.php |
✅ | 13 |
segment.test.php |
✅ | 12 |
replace_shadow_colors.test.php |
✅ | 14 |
escape.test.php |
✅ | 10 |
prefix.test.php |
✅ | 9 |
expand_declaration.test.php |
✅ | 4 |
important.test.php |
✅ | 4 |
Tests for PHP-specific implementations and helpers (not direct TypeScript ports):
| Test File | Status | Tests |
|---|---|---|
_tailwindphp/LightningCss.test.php |
✅ | 70 |
theme_unit.test.php |
✅ | 46 |
utils/infer_data_type.test.php |
✅ | 45 |
utils/is_color.test.php |
✅ | 33 |
utils/math_operators.test.php |
✅ | 22 |
utils/is_valid_arbitrary.test.php |
✅ | 17 |
property_order.test.php |
✅ | 15 |
design_system.test.php |
✅ | 13 |
utils/dimensions.test.php |
✅ | 10 |
utils/default_map.test.php |
✅ | 12 |
utils/compare_breakpoints.test.php |
✅ | 9 |
utils/topological_sort.test.php |
✅ | 9 |
| Test File | Status | Tests |
|---|---|---|
ImportPathsTest.php |
✅ | 42 |
Tests for the importPaths feature including:
- Single file and directory imports
- Array of import paths
- Nested
@importresolution - Circular import protection via deduplication
- Custom resolver functions
- Virtual module deduplication (tailwindcss, tailwindcss/preflight, etc.)
- Multiple @theme blocks and overrides
- @utility from imported files
- All CSS @import modifiers:
layer(),supports(), and media queries - Anonymous and named layer imports
- Combined modifiers (layer + supports + media)
| Test File | Status | Tests |
|---|---|---|
EdgeCasesTest.php |
✅ | 57 |
Tests for edge cases and complex scenarios:
- Deep variant stacking (3-4 levels)
- Arbitrary values with special characters
- Container queries with arbitrary breakpoints
- @apply with complex selectors
- nth-child, has(), aria, and data selectors
- Important modifier combinations
- Opacity modifier edge cases
- Print, motion, and contrast media variants
- Dark mode with variant combinations
- Pseudo-element variants with modifiers
| Test File | Status | Tests |
|---|---|---|
PropertyRegistrationTest.php |
✅ | 98 |
ContainerUtilityTest.php |
✅ | 3 |
NestedApplyRegressionTest.php |
✅ | 3 |
PropertyRegistrationTest.php—@propertyregistration of composed--tw-*variable groups (translate, scale, transform, filters, backdrop-filters, transitions, typography, font-variant-numeric, contain, touch-action, scroll-snap, border-spacing), including registration details, the@layer propertiesfallback, and the negative cases that must register nothingContainerUtilityTest.php— thecontainerlayout component (per-breakpoint media queries, unit-then-value breakpoint sorting, custom theme breakpoints)NestedApplyRegressionTest.php— nested rules surviving a parent@applythat expands to multiple declarations
| Test File | Status | Tests |
|---|---|---|
TwAnimateCssTest.php |
✅ | 23 |
Tests for tw-animate-css integration:
- Import via
@import "tw-animate-css" - animate-in/animate-out utilities
- fade-in/fade-out with opacity values
- zoom-in/zoom-out with scale values
- spin-in/spin-out with rotation values
- slide-in/slide-out directional animations
- animation-duration, delay, repeat, direction, fill-mode utilities
- play-state (running/paused) utilities
- blur-in/blur-out utilities
- accordion-down/accordion-up animations
- collapsible-down/collapsible-up animations
- caret-blink animation
- RTL/LTR-aware start/end directional slides
- Common shadcn/ui patterns (dialogs, dropdowns)
| Test File | Status | Tests |
|---|---|---|
CacheTest.php |
✅ | 14 |
StateCacheTest.php |
✅ | 6 |
Tests for file-based caching and the persistent construction-state cache:
- Cache to default and custom directories
- Cache key generation from content, CSS, and minify option
- Cache TTL (time-to-live) expiration
- clearCache() function and tw::clearCache() static method
- Cache directory auto-creation
| Test File | Status | Tests |
|---|---|---|
CliTest.php |
✅ | 25 |
CliInputTest.php |
✅ | 35 |
CliOutputTest.php |
✅ | 24 |
Tests for the CLI application (1:1 port of @tailwindcss/cli):
- Input argument and option parsing (-i, -o, -w, -m, --cwd, etc.)
- Short and long option formats
- Build with input/output files
- @source directive for content scanning
- Minify and optimize options
- Output directory creation
- Error handling (missing files, invalid paths)
- Application help and version output
- CLI Input class (argument parsing, options, commands)
- CLI Output class (colors, formatting, quiet/verbose modes)
| Test File | Status | Tests |
|---|---|---|
SourceDirectiveTest.php |
✅ | 24 |
Tests for the @source directive:
- Basic file/directory patterns (
@source "./templates") - Glob patterns (
@source "./src/**/*.html") - Negated patterns (
@source not "./ignored") - Inline candidates (
@source inline("flex p-4")) - Ignored candidates (
@source not inline("legacy")) - Brace expansion in inline patterns
- Validation (no body, no nesting, quoted paths)
- Quote styles (single and double quotes)
- CLI integration (sources array returned)
These PHP test files exist but contain no tests because the TypeScript originals test features outside the scope of this port (JS runtime, IDE tooling):
| PHP Test File | TypeScript Original | Reason |
|---|---|---|
canonicalize_candidates.test.php |
canonicalize-candidates.test.ts |
IDE tooling - Prettier plugin |
intellisense.test.php |
intellisense.test.ts |
IDE tooling - VS Code extension |
sort.test.php |
sort.test.ts |
IDE tooling - class sorting |
to_key_path.test.php |
to-key-path.test.ts |
Not needed |
Other TypeScript test files not ported: config.test.ts, resolve-config.test.ts, container-config.test.ts, screens-config.test.ts, flatten-color-palette.test.ts, apply-config-to-theme.test.ts, apply-keyframes-to-theme.test.ts, legacy-utilities.test.ts, source-map.test.ts, line-table.test.ts, translation-map.test.ts.
- ✅ All utility classes (364 utilities)
- ✅ All variants (hover, focus, responsive, dark mode, container queries, etc.)
- ✅
@applydirective with nested selectors - ✅
@themecustomization with namespace clearing - ✅
@utilitycustom utilities - ✅
@custom-variantsupport - ✅
@importresolution for virtual modules (tailwindcss,tailwindcss/preflight,tw-animate-css, etc.) - ✅
@importresolution for file paths viaimportPathsoption - ✅ Nested
@importresolution (files importing other files) - ✅ Import deduplication (virtual modules and files)
- ✅ Custom import resolvers (callable for virtual file systems)
- ✅
theme()function with dot notation (colors.red.500) - ✅
--theme()function with initial fallback handling - ✅
--spacing()and--alpha()functions - ✅
color-mix()tooklab()conversion (LightningCSS equivalent) - ✅ Stacking opacity in
@themedefinitions - ✅ Prefix support (
tw:) - ✅ Shadow/ring stacking with
--tw-*variables - ✅
@propertyrules with@layer propertiesfallback - ✅ Vendor prefixes (autoprefixer equivalent)
- ✅ Keyframe handling and hoisting
- ✅ Built-in plugins (
@tailwindcss/typography,@tailwindcss/forms) - ✅ Invalid
theme()candidates filtered out - ✅ File-based caching with TTL support
- ✅ CLI (1:1 port of @tailwindcss/cli with -i, -o, -w, -m, --optimize, --cwd options)
- ✅
@sourcedirective with full support (not,inline(), brace expansion)
All 44 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 significant deviations |
@port-deviation:async |
PHP uses synchronous code instead of async/await |
@port-deviation:storage |
Different data structures (PHP array vs JS Map/Set) |
@port-deviation:types |
PHPDoc annotations instead of TypeScript types |
@port-deviation:sourcemaps |
Source map tracking omitted |
@port-deviation:enum |
PHP constants instead of TypeScript enums |
@port-deviation:caching |
Different caching strategy |
@port-deviation:errors |
Different error handling approach |
@port-deviation:stub |
Placeholder for unneeded functionality |
@port-deviation:replacement |
PHP implementation replacing external library (e.g., lightningcss) |
@port-deviation:helper |
PHP-specific helper not in original |
@port-deviation:omitted |
Entire module not ported (not needed for PHP) |
File header deviation:
/**
* Port of: packages/tailwindcss/src/compile.ts
*
* @port-deviation:bigint TypeScript uses BigInt for variant order bitmask.
* PHP uses regular integers (sufficient for current variant count).
*
* @port-deviation:sorting TypeScript uses Map<AstNode, ...> for nodeSorting.
* PHP embeds sorting info directly in nodes via '__sorting' key.
*/Inline deviation:
// @port-deviation: PHP regex syntax differs from JS
$pattern = '/.../';