Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Tailwind Merge Php Laravel Package

gehrisandro/tailwind-merge-php

TailwindMerge for PHP merges multiple Tailwind CSS classes and resolves conflicts automatically, keeping only the class defined later. A PHP port of tailwind-merge supporting Tailwind v3.0 through v3.4, requiring PHP 8.1+, with caching and configurable options.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the two hard prerequisites before anything else. The package requires PHP 8.1+ and targets Tailwind CSS v3.0 through v3.4. If the estate is on Tailwind v4 or is planning the move, stop here — this is a decision gate, not a footnote.

composer require gehrisandro/tailwind-merge-php

Where to look first. The README's Usage section is the real specification — it enumerates exactly which conflict shapes are handled (breakpoints, dark mode, hover:/focus: states, the ! important modifier, arbitrary values, arbitrary variants). Read the changelog next: v1.1.1 and v1.1.2 are both bug-fix releases around arbitrary values and border color/width, which tells you where the sharp edges are. For configuration semantics, the PHP README delegates to the upstream JS docs (dcastil/tailwind-merge v1.14.0) — treat those as canonical, since the PHP config keys mirror them.

First real use case: a Blade button component whose defaults must be overridable. This is the day-one win. $attributes->merge() concatenates classes but cannot resolve conflicts — a caller passing px-6 to a component that hardcodes px-4 gets both, and CSS specificity becomes a coin flip. Swap in the merger:

use TailwindMerge\TailwindMerge;

$tw = TailwindMerge::instance();

$tw->merge('block inline');                 // 'inline'
$tw->merge('pl-4 px-6');                    // 'px-6'
$tw->merge('h-10 lg:h-12 lg:h-20');         // 'h-10 lg:h-20'
$tw->merge('non-tailwind-class block inline'); // 'non-tailwind-class inline'

Input is flexible — string, array, or variadic mix — which matches how Laravel developers already accumulate classes:

$tw->merge(['h-10', 'h-20'], 'h-30', ['h-40']); // 'h-40'

Implementation Patterns

Resolve the instance once, not per call. TailwindMerge::instance() gives you a default-configured object, but as soon as you need a prefix or custom class groups, bind a configured instance in a service provider and inject it. Configuration is the expensive part; merging should be cheap.

// app/Providers/AppServiceProvider.php
use TailwindMerge\TailwindMerge;

$this->app->singleton(TailwindMerge::class, fn () => TailwindMerge::factory()
    ->withConfiguration([
        'prefix' => 'tw-',
        'classGroups' => [
            'font-size' => [
                ['text' => ['very-large', 'tiny']],
            ],
        ],
    ])
    ->withCache(cache()->store())
    ->make());

Blade component pattern. Don't use $attributes->merge(['class' => ...]) for the class attribute; compute the final string yourself and strip class from the bag.

@props(['variant' => 'primary'])

@php
    $base     = 'inline-flex items-center rounded-md px-4 py-2 font-medium';
    $variants = [
        'primary' => 'bg-blue-600 text-white hover:bg-blue-700',
        'danger'  => 'bg-red-600 text-white hover:bg-red-700',
    ];
    $classes = app(\TailwindMerge\TailwindMerge::class)
        ->merge($base, $variants[$variant], $attributes->get('class', ''));
@endphp

<button {{ $attributes->except('class') }} class="{{ $classes }}">

The caller can now pass class="bg-emerald-600 px-8" and get a correctly resolved result instead of a specificity fight. Add a tw() helper alongside your existing helpers if app(...) in templates feels noisy.

Conditional arrays. The variadic array form is what makes this feel native to Laravel:

$classes = $tw->merge(
    'rounded-lg border p-4',
    ['bg-white dark:bg-gray-900', $isError ? 'border-red-500' : 'border-gray-200'],
    $compact ? 'p-2 text-sm' : null,
);

Variants are scoped, not global. lg:h-12 lg:h-20 collapses correctly because conflicts are evaluated per variant prefix. This means you generally do not need to hand-curate responsive overrides — you can list every breakpoint's intent and let the merger keep the last one. Same for dark: and state variants.

Order is the contract. Later arguments always win. The discipline that keeps this maintainable is: base → variant → size/state modifiers → caller-supplied classes, with caller classes strictly last. Every component that violates this ordering creates a bug where user overrides silently vanish.

Cache deliberately. The merger is regex-heavy; wrapping it in a PSR-16 cache (Laravel's cache repository satisfies this) turns repeated identical merges into lookups. This matters most in queued mail/PDF rendering and in Octane/Swoole workers where the same component classes are merged thousands of times. In a plain request lifecycle with a handful of components, it's optional.

Test your real class strings. Add a PHPUnit data provider with fixtures lifted from your actual components — not toy examples:

public static function merges(): array
{
    return [
        'px over p' => ['p-4 px-6', 'p-4 px-6'],
        'caller wins' => ['px-4 px-8', 'px-8'],
        'dark scoped' => ['text-black dark:text-white dark:text-gray-700', 'text-black dark:text-gray-700'],
        'arbitrary' => ['z-10 z-[999]', 'z-[999]'],
    ];
}

This suite is your insurance policy against config drift and against upstream regressions.

Gotchas and Tips

Tailwind v4 is a dead end until ported. Support stops at v3.4. v4's CSS-first configuration changes utility semantics fundamentally, so a faithful port of the v1.x upstream config model does not map onto it. If v4 is on the 12-month roadmap, budget for a fork or a replacement rather than an upgrade.

Config drift is a silent-correctness hazard, not a loud failure. The merger only resolves conflicts it knows about. Any custom spacing scale, renamed color, plugin-added utility, or prefix in tailwind.config.js must be mirrored in PHP via theme, classGroups, and conflictingClassGroups. When it's out of sync, merge() still returns a string and nothing errors — you just get wrong CSS. Assign explicit ownership of keeping the two in sync, or generate the PHP config from the JS config.

Clear the cache whenever configuration changes. Cache keys are the input class strings, not a hash of the configuration. A config change with a warm cache gives you stale merges that are nearly impossible to diagnose. Clear it on deploy, or include a config version in the cache namespace.

Non-Tailwind classes are preserved but not deduplicated. merge('foo block foo') leaves both foo tokens in place — unknown classes are opaque. Don't rely on this package as a general class deduplicator; it's a conflict resolver for known Tailwind groups only.

Arbitrary values are where the bugs live. v1.1.1 fixed arbitrary shadow with inset and arbitrary border color vs. width; v1.1.2 fixed merging arbitrary variables. If your design system leans on z-[999], [&>*]:underline, or shadow-[inset_0_2px_4px_rgba(0,0,0,0.1)], pin the version and cover those exact strings in your fixture suite. Upgrade deliberately, not automatically.

Don't mutate the singleton. instance() returns a shared object. Any per-request tweaking of configuration on it will leak across requests in Octane. Configure once in a provider; treat the resolved instance as immutable.

The ! important modifier is part of the class identity. !font-medium !font-bold correctly resolves to !font-bold, but font-medium !font-bold does not conflict — they're different groups. Don't mix important and non-important forms of the same utility and expect a collapse.

Prefixed setups need symmetric configuration. If you use prefix: 'tw-', classes written without the prefix won't be recognized as Tailwind classes at all and will pass through untouched. Configure the prefix to match production exactly.

Consider wrapping behind your own interface. TailwindMerge\TailwindMerge is a small surface — bind it behind an internal ClassMerger contract. That insulates call sites from a single-maintainer dependency (140 stars, one maintainer, MIT) and makes a fork or swap a one-file change rather than a codebase-wide grep.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin