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 Package

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...

View on GitHub
Deep Wiki
Context7

Getting Started

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."
  • The service provider — confirm whether the helper is registered globally or only inside Blade.
  • 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.

Implementation Patterns

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');

Gotchas and Tips

  • Order is semantics. 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.
  • It only knows what's in the config class groups. Custom utilities, plugin classes (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.
  • Pin and verify the version. Position support landed in v1.1.10; on v1.1.9 and earlier, 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.
  • Tailwind version drift matters. The class-group map is a snapshot of Tailwind's utility names. Running Tailwind v4 against a map generated for v3 means new utilities (e.g. renamed spacing/shadow tokens, 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.
  • Position support is not the whole layout story. 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.
  • Arbitrary values need exact string equality. 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.
  • Modifiers must match. 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.
  • Don't double-merge. If a parent already merged and the child merges again, the second pass can strip a class the parent intentionally added (e.g. parent added 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.
  • Caching/perf. If the package memoizes results, class strings built by interpolation are unique per render and defeat the cache. Pass stable components (tw_merge($base, $variant)) rather than one giant interpolated string.
  • Debugging procedure: echo the pre-merge inputs and the post-merge output, then grep the config for the conflicting utility. If neither token appears in the group map, the package won't merge them, no matter how obviously they conflict in CSS. Confirm the installed version first — the group map changes between releases.
  • Maintenance status. The package is single-maintainer and was previously quiet; v1.1.10 (position support) shows it is still being patched, but the release cadence is unpredictable. Keep your own component test suite in CI (the 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.
  • Extension point to lean on: the class-group config. It's the lever that survives any upstream slowdown, and it lets you teach the merger about your own design tokens without touching vendor code.
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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core