goodm4ven/tailwind-merge
goodm4ven/tailwind-merge is a PHP/Laravel utility for intelligently merging Tailwind CSS class strings, resolving conflicting utilities so later classes win. Ideal for Blade components, helpers, and dynamic class lists, it keeps markup clean, avoids duplicate styles, and makes conditional Tailwind c...
1. Install (check the installed version — as of v1.1.10 the package is receiving updates again, so don't treat it as frozen):
composer require goodm4ven/tailwind-merge
The service provider is auto-discovered. If you want to tune class groups or prefixes:
php artisan vendor:publish --tag=tailwind-merge-config
2. Where to look first
README.md — the exact helper/directive names (tw_merge() vs. a Blade directive) and supported Tailwind version.config/tailwind-merge.php — the class-group map (spacing, colors, positions, arbitrary values). This is what determines whether two classes are considered "conflicting."composer.lock / vendor/goodm4ven/tailwind-merge — confirm you actually have v1.1.10+. Position utilities (static, fixed, absolute, relative, sticky) only merge as a conflict group from that release onward.3. First use case: a button component that accepts caller classes
{{-- resources/views/components/button.blade.php --}}
@props(['variant' => 'primary'])
@php
$base = 'inline-flex items-center rounded-md px-3 py-2 text-sm font-medium';
$variants = [
'primary' => 'bg-blue-600 text-white hover:bg-blue-700',
'ghost' => 'bg-transparent text-blue-600 hover:bg-blue-50',
];
@endphp
<button {{ $attributes->merge(['class' => tw_merge($base, $variants[$variant], $attributes->get('class'))]) }}>
{{ $slot }}
</button>
Then in a page:
<x-button variant="primary" class="px-6 py-3">Save</x-button>
{{-- Output class: ... px-6 py-3 ... (the px-3 py-2 from $base is dropped, not duplicated) --}}
The value is immediate: without it, $attributes->merge() appends, so you get px-3 py-2 px-6 py-3 and have to rely on CSS source order — which breaks the moment you reorder component internals.
Pattern 1 — Treat tw_merge as a drop-in for manual concatenation. Anywhere you currently do "$base $caller", switch to tw_merge($base, $caller) and let the later argument win. Apply it consistently at one layer (usually the component's @props/@php block), not both parent and child.
Pattern 2 — Variant maps, then merge. Keep variants as plain arrays of Tailwind strings and merge them in a fixed order: base → variant → size → caller override. Because last-wins, the caller's class attribute must always be the final argument.
$classes = tw_merge(
'rounded-md font-medium',
$variants[$variant] ?? '',
$sizes[$size] ?? '',
$attributes->get('class'),
);
Pattern 3 — Composable primitives. x-input wraps x-field; each level merges its own defaults with $attributes. This keeps conflict resolution local: x-input only needs to know about what it adds, not what the outer page passes in.
Pattern 4 — Conditional modifiers. Compute conditional strings normally, then merge once. Avoid calling tw_merge inside loops for hot paths; if you must do per-item work for large lists, merge the static parts once and append per-item overrides.
$state = $error ? 'border-red-500 ring-red-500' : 'border-gray-300';
$classes = tw_merge('block w-full border rounded-md', $state, $attributes->get('class'));
Pattern 5 — Livewire. Use the same helper inside Livewire component views/blade partials. Because Livewire re-renders often, prefer merging in the Blade template (or a computed property) rather than recomputing in render() each cycle if the class set is large.
Pattern 6 — Slot/section classes. For named slots (<x-slot:label class="...">), read the slot's class attribute into a variable and pass it as the final argument, otherwise the label's overrides will not beat the defaults.
Pattern 7 — Layout primitives now benefit from position merging (v1.1.10+). Overlays, dropdowns and toast containers are usually "default to relative, caller may force fixed or absolute". Before v1.1.10 both classes survived and you'd get relative fixed — a coin flip on CSS order. Now the last one wins, so you can write the obvious thing:
@props(['position' => 'relative'])
<div {{ $attributes->merge(['class' => tw_merge('rounded-lg border', $position, $attributes->get('class'))]) }}>
{{ $slot }}
</div>
<x-panel position="fixed" class="absolute inset-0">…</x-panel>
{{-- renders: absolute (both the component default and the caller's class are resolved) --}}
Pattern 8 — Test the merge output. In a feature test, assert the rendered HTML does not contain both px-3 and px-6:
$this->blade('<x-button class="px-6" />')->assertDontSee('px-3');
$this->blade('<x-panel class="fixed" />')->assertDontSee('relative');
tw_merge('px-6', 'px-3') returns px-3. Every bug in this package will look like "my override didn't apply" — check argument order first.prose, aspect-* variants), or safelisted classes from tailwind.config.js are not automatically understood. If a merge is wrong, add the group to the published config rather than working around it in Blade. The list grows release by release — v1.1.10 added position utilities — so a merge that "didn't work" last quarter may work now, and vice versa if you pin an older tag.relative + fixed both survive silently. Add a one-line test per newly-supported group you start relying on, so a composer update/rollback that changes merge behaviour fails CI rather than shipping duplicate classes.size-*) fall through as non-conflicting and both classes survive. Pin Tailwind to the version the config targets, or vendor the config and update it yourself.absolute/relative/fixed now conflict correctly, but related groups — inset-*/top-*, z-*, isolate, stacking helpers — depend on the same config map. Don't assume "positions work" means your whole overlay style set is covered; test the specific utilities your components use.w-[calc(100%-1rem)] and w-[calc(100%-1rem )] are different tokens; whitespace inside arbitrary values will defeat the conflict matcher. Keep arbitrary values free of incidental spaces.hover:bg-blue-600 and bg-blue-600 are different groups and correctly do not conflict — but hover:bg-blue-600 vs hover:bg-red-600 will conflict. Don't assume the modifier is stripped.!important and prefixes. !px-4 / tw-px-4 are only merged correctly if important and prefix are set in the package config to match your Tailwind setup. Mismatched prefix is the classic "silently merges nothing" failure.sm:px-8, child's defaults don't touch it — fine — but if the child's defaults include px-8, the responsive override's base is gone). Keep exactly one merge layer per visual concern.tw_merge($base, $variant)) rather than one giant interpolated string.assertDontSee tests above, one per component) so a lag behind Tailwind or a stalled upstream fails your build instead of shipping duplicate classes. If it does stall again, the fallback is small: a single tw_merge() helper with a hand-maintained group map covering just the utilities your design system uses.How can I help you explore Laravel packages today?