gehrisandro/tailwind-merge-php
Merge Tailwind CSS class strings in PHP with automatic conflict resolution (later classes win). PHP port of dcastil/tailwind-merge, compatible with Tailwind v3.0–v3.4 and PHP 8.1+. Create instances and customize configuration as needed.
composer require gehrisandro/tailwind-merge-php
use TailwindMerge\TailwindMerge;
$merged = TailwindMerge::instance()->merge('text-red-500 hover:text-blue-500', 'hover:text-green-500');
// Output: 'text-red-500 hover:text-green-500'
app/Helpers/tailwind.php:
if (!function_exists('twMerge')) {
function twMerge(...$classes) {
return TailwindMerge::instance()->merge(...$classes);
}
}
Use in Blade:
<div class="{{ twMerge('p-4', 'px-6') }}">...</div>
Merge conflicting classes in a Livewire component or Blade partial:
// Livewire component
public function render()
{
$baseClasses = 'bg-gray-100 hover:bg-gray-200';
$dynamicClasses = 'hover:bg-blue-500';
return view('livewire.example', [
'classes' => TailwindMerge::instance()->merge($baseClasses, $dynamicClasses),
]);
}
<div class="{{ $classes }}">Hover me</div>
Create a custom Blade directive (app/Providers/BladeServiceProvider.php):
Blade::directive('merge', function ($expression) {
return "<?php echo TailwindMerge::instance()->merge({$expression}); ?>";
});
Usage:
<div class="@merge(['p-4', 'px-6', 'lg:px-8'])">...</div>
Extend a service to handle dynamic class merging:
class EmailService {
public function generateButtonClasses(array $modifiers): string
{
$base = 'px-4 py-2 rounded text-white';
$dynamic = collect($modifiers)->map(fn($mod) => "bg-{$mod}-500 hover:bg-{$mod}-600")->implode(' ');
return TailwindMerge::instance()->merge($base, $dynamic);
}
}
Merge classes reactively in Livewire:
public $size = 'md';
public function updatedSize()
{
$this->classes = TailwindMerge::instance()->merge(
'p-4',
"text-{$this->size}-font"
);
}
Configure caching in a service container binding:
// config/app.php
'bindings' => [
TailwindMerge::class => function ($app) {
return TailwindMerge::factory()
->withCache($app->make(\Illuminate\Cache\CacheManager::class))
->make();
},
],
Usage:
$cachedMerge = app(TailwindMerge::class)->merge(...);
Extend for custom Tailwind classes (e.g., tw- prefix):
$merge = TailwindMerge::factory()
->withConfiguration([
'prefix' => 'tw-',
'classGroups' => [
'font-size' => [['text' => ['very-large']]],
],
])
->make();
Sanitize user-provided classes before merging:
public function mergeUserClasses(array $userClasses): string
{
$validClasses = collect($userClasses)
->filter(fn($class) => preg_match('/^(?!.*\s*!.*\s*).*$/', $class)) // Reject !important
->values()
->toArray();
return TailwindMerge::instance()->merge($validClasses);
}
Arbitrary Value Conflicts:
z-10 and z-[999] works, but z-[10] and z-10 may not resolve as expected.z-[1000]) for critical cases.Dark Mode State Overrides:
dark:text-gray-700) override earlier ones (dark:text-white).$merged = TailwindMerge::instance()->merge(
'text-white dark:text-gray-500',
'dark:text-gray-700'
); // 'text-white dark:text-gray-700'
Caching Invalidation:
classGroups configuration:
$merge->getCache()->clear();
Non-Tailwind Classes:
custom-class) are preserved but may cause unexpected behavior if they conflict with Tailwind’s internal logic.nc-custom) and exclude them from merging.PHP 8.1+ Requirement:
Performance with Large Inputs:
$cacheKey = md5(serialize($classes));
return $merge->getCache()->remember($cacheKey, 3600, fn() => $merge->merge($classes));
Inspect Merged Classes:
$instance = TailwindMerge::factory()->withDebug(true)->make();
$instance->merge(...); // Logs each step
Validate Tailwind Config:
tailwind.config.js with the supported defaults.Test Edge Cases:
// Test arbitrary values
$merge->merge('z-[10]', 'z-[20]'); // 'z-[20]'
// Test hover states
$merge->merge('hover:bg-red-500', 'hover:bg-blue-500'); // 'hover:bg-blue-500'
Custom Validators:
TailwindMerge\Validators\ValidatorInterface to handle project-specific classes:
class CustomValidator implements ValidatorInterface {
public function validate(string $class): bool { ... }
}
$merge = TailwindMerge::factory()
->withValidators([new CustomValidator()])
->make();
Plugin System:
interface MergePlugin {
public function handle(array $classes): array;
}
!important to specific classes:
class ImportantPlugin implements MergePlugin {
public function handle(array $classes): array {
return array_map(fn($class) => str_ends_with($class, '-focus') ? "!{$class}" : $class, $classes);
}
}
Laravel Service Provider:
public function register() {
$this->app->singleton(TailwindMerge::class, function ($app) {
return TailwindMerge::factory()
->withConfiguration(config('tailwind.merge'))
->withCache($app->make('cache'))
->make();
});
}
Prefix Handling:
prefix config (e.g., tw-) must match exactly. Partial matches (e.g., tw) won’t work.Class Group Priorities:
classGroups override earlier ones. Structure groups logically:
'classGroups' => [
'spacing' => [['p', 'px', 'py']],
'colors' => [['bg', 'text']], // Overrides spacing if conflicts exist
]
Arbitrary Value Support:
// tailwind.config.js
module.exports = {
corePlugins: {
preflight: false,
arbitraryValues: true
How can I help you explore Laravel packages today?