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 Laravel Laravel Package

gehrisandro/tailwind-merge-laravel

TailwindMerge for Laravel merges Tailwind CSS classes and resolves conflicts so later classes override earlier ones, making it easy to customize Blade components. A PHP port supporting Tailwind v3.0 through v3.3.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the version fit first. This is a presentation-layer utility with no runtime I/O, so onboarding is one command:

composer require gehrisandro/tailwind-merge-laravel

Before you commit to it, check two version boundaries, because they are the only real adoption risk:

  • Laravel: README states Laravel 10+; releases v1.2.0–v1.4.0 add Laravel 11, 12, and preparatory 13 support. Pin a recent release if you're on 11+.
  • Tailwind: the README claims v3.0–v3.3, but changelog v1.1.0 added v3.4. There is no Tailwind v4 support in this package line — if your app is on v4, stop here and evaluate the upstream JS tailwind-merge via a Node step instead.

Publish the config only if you have a custom theme. A stock Tailwind config needs nothing:

php artisan vendor:publish --provider="TailwindMerge\Laravel\TailwindMergeServiceProvider"

That writes config/tailwind-merge.php containing blade_directive (default twMerge), prefix (env TAILWIND_MERGE_PREFIX), and an empty classGroups array.

Where to look first. Read the README "Usage" section, then jump to the underlying gehrisandro/tailwind-merge-php repo for edge cases (the Laravel package is a thin adapter — nearly all merge semantics live there). For classGroups configuration syntax, the authoritative reference is dcastil's original tailwind-merge v1.14 configuration docs, which the README links.

Your first use case: one overridable component. The canonical win is a default-styled Blade component whose classes a caller can override without !important or string surgery.

{{-- resources/views/components/circle.blade.php --}}
<div {{ $attributes->twMerge('w-10 h-10 rounded-full bg-red-500') }}></div>
<x-circle class="bg-blue-500" />
{{-- <div class="w-10 h-10 rounded-full bg-blue-500"></div> --}}

Verify the resolver behaves as you expect before wiring it through templates — php artisan tinker is the fastest feedback loop:

twMerge('pl-4 px-6');                    // px-6
twMerge('h-10 lg:h-12 lg:h-20');         // h-10 lg:h-20
twMerge('text-black dark:text-white dark:text-gray-700'); // text-black dark:text-gray-700

If those resolve correctly, your defaults are compatible with the package. If they don't, fix config before touching Blade files.

Implementation Patterns

Blade components are the primary surface — treat twMerge as the replacement for merge. The rule for your team: in component templates, $attributes->twMerge($defaults) is the only way classes enter the root element. Every component you author gets this treatment, and callers get override-by-append semantics for free.

<button {{ $attributes->twMerge('inline-flex items-center px-4 py-2 rounded-md bg-gray-900 text-white font-medium') }}>
    {{ $slot }}
</button>

Build variant maps in PHP, not in template ternaries. The package resolves conflicts, it doesn't pick variants. Keep that logic in a @php block or, better, a small class so it's testable:

@php
    $base = 'inline-flex items-center font-medium rounded-md transition';
    $sizes = ['sm' => 'px-2.5 py-1.5 text-sm', 'md' => 'px-4 py-2 text-base'];
    $variants = ['primary' => 'bg-blue-600 text-white', 'danger' => 'bg-red-600 text-white'];
    $classes = implode(' ', [$base, $sizes[$size] ?? $sizes['md'], $variants[$variant] ?? $variants['primary']]);
@endphp

<button {{ $attributes->twMerge($classes) }}>
    {{ $slot }}
</button>

This is where the library pays for itself: $attributes->twMerge($classes) lets a caller write <x-button variant="primary" class="bg-emerald-600"> and get exactly one background color, with no fighting over specificity.

Multi-element components use twMergeFor + withoutTwMergeClasses. A button with an icon has two class-bearing elements, and Laravel's attribute bag only natively supports one merge point. The pattern:

{{-- button.blade.php --}}
<button {{ $attributes->withoutTwMergeClasses()->twMerge('p-2 bg-gray-900 text-white') }}>
    <svg {{ $attributes->twMergeFor('icon', 'h-4 w-4 text-gray-500') }} viewBox="0 0 448 512">…</svg>
    {{ $slot }}
</button>
<x-button class="bg-blue-900" class:icon="text-blue-500">Click Me</x-button>

The withoutTwMergeClasses() call on the root bag is mandatory — omit it and every class:icon attribute leaks into the rendered output of the button itself.

Use @twMerge for one-off compositions in plain Blade. Especially in loops where classes are computed per iteration (a table row's active state, a nav link's current state):

<a class="@twMerge('px-3 py-2 rounded-md text-gray-700', $active ? 'bg-gray-100 text-gray-900' : 'hover:bg-gray-50')">

It accepts multiple arguments, so you can pass fragments rather than pre-joining strings. Rename or disable it via blade_directive in the config if twMerge collides with something in your codebase.

Use the Facade or helper outside Blade, aggressively. Any place you produce a class string in PHP is a candidate: view composers, ViewModel/DTO classes, Livewire computed properties, mail and notification templates, Filament-style admin panels, and class strings returned from enums.

use TailwindMerge\Laravel\Facades\TailwindMerge;

TailwindMerge::merge(['h-10', 'h-20'], 'h-30', ['h-40']); // h-40

The variadic merge(string|array ...$args) signature means you can splat an array of accumulated fragments without flattening it yourself:

$classes = TailwindMerge::merge(...$fragments);

Centralize default class strings. Once twMerge is everywhere, the defaults become a de-facto design system. Put them in a config file, an enum, or a Support\Ui helper rather than scattering literals across 40 Blade files — otherwise a rounded-md → rounded-lg change becomes a grep exercise.

Testing pattern. Use the helper directly in Pest/PHPUnit to assert your component's merge contract, and assert rendered output for the wiring:

it('lets callers override the background', function () {
    $this->blade('<x-button class="bg-emerald-600" />')
        ->assertSee('bg-emerald-600')
        ->assertDontSee('bg-gray-900');
});

Table-driven unit tests over twMerge('...') inputs are cheap and catch config regressions when someone edits classGroups.

Gotchas and Tips

$attributes->merge(['class' => '...']) is not supported. The README calls this out explicitly — it's a limitation in Laravel's attribute bag, not something the package can fix. If you have existing components using merge, converting them is a mechanical but required step; leaving both patterns in the codebase produces inconsistent override behavior that's very confusing to debug.

Forgetting withoutTwMergeClasses() is the #1 bug. Symptom: your component renders something like class:icon="text-blue-500" as a literal attribute. Any component using twMergeFor must call withoutTwMergeClasses() on the root bag, before twMerge.

The twMergeFor key must match the class: name exactly. $attributes->twMergeFor('icon', ...) pairs with class:icon="...". A typo silently drops the caller's classes — no error, just unmerged defaults.

There's a typo in the README's twMergeFor example output (class="p-2 blue text-white" instead of bg-gray-900/bg-blue-900). Don't use it as a reference for expected behavior — verify with tinker or an actual render.

Custom Tailwind configs need mirroring in classGroups. The defaults assume: color names that don't clash with other utilities, numeric deviations only, font-family names that don't collide with font-weight, and stock config elsewhere. Break any of those and merges will be silently wrong. Add custom groups like so:

// config/tailwind-merge.php
return [
    'classGroups' => [
        'font-size' => [
            ['text' => ['very-large']],
        ],
    ],
];

The nested [['utility' => ['values']]] shape is easy to get wrong; a malformed group doesn't error, it just never matches. After editing, re-run your twMerge unit tests.

Prefixes need declaring. If your build uses tw--prefixed utilities, set TAILWIND_MERGE_PREFIX=tw- (or the prefix key). Without it, prefixed classes aren't recognized as Tailwind classes at all and pass through unmerged — a failure mode that looks like the package "not working."

Config and merge caching mean changes need a cache clear. v1.0.0 introduced internal merge-result caching, and Laravel caches config. After changing classGroups, prefix, or blade_directive, run php artisan config:clear (and restart long-running workers like Octane/queue consumers) or you'll chase a ghost.

Debugging a class that won't merge: drop into tinker and call twMerge() on the exact pair. If it doesn't resolve, the class either isn't in a known group (custom utility → add classGroups), or it's an arbitrary value the validators don't recognize. Arbitrary values are supported (z-10 z-[999] → z-[999], [&>*]:underline [&>*]:line-through → [&>*]:line-through), but ambiguous arbitrary properties are the usual failure case.

Order is the entire contract — last wins. In Blade, $attributes->twMerge($defaults) puts caller classes first in the attribute stream but the merge treats them as later, which is what you want. When composing manually in PHP, put defaults first and overrides last, and never interleave.

Non-Tailwind classes pass through untouched. twMerge('non-tailwind-class block inline') → non-tailwind-class inline. Useful for your own utility classes, but note the package won't dedupe those — two of your own conflicting classes both survive. Keep custom utilities out of the same property space as Tailwind ones.

This is not a variant engine or a class formatter. It removes conflicts; it does not sort output prettily, generate hover states, or replace a props-based variant system. If you find yourself writing five nested ternaries inside twMerge, that's a signal to move variant selection into a PHP class or consider a CVA-style helper.

It does not affect Tailwind's build. Classes still must be statically discoverable by Tailwind's content scanner. A class produced only inside a PHP implode that never appears literally in a scanned file will be purged; safelist it in tailwind.config.js or keep the literals in a scanned location (which, usefully, the variant-map pattern above does).

Performance is a non-issue, within reason. The v1.0.0 cache makes repeated merges cheap, so calling twMerge in a loop over table rows is fine. In very hot paths (thousands of rows, or inside a render() that fires on every Livewire keystroke), precompute the class string once and reuse it.

Removal is genuinely cheap. The blast radius is Blade templates and any PHP file calling twMerge()/TailwindMerge::merge(). There's no migration, no schema, no queue surface. That makes it a safe experiment: adopt it in one component library, and if it doesn't fit, composer remove plus reverting call sites is a contained afternoon.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core