TailwindMerge

repository·main·Indexed 40 days ago

https://github.com/gjtorikian/tailwind_merge

A Ruby port of the tailwind-merge utility for dynamically merging Tailwind CSS classes and resolving conflicts. It ensures the last conflicting class wins while preserving non-conflicting styles, supporting modifiers, arbitrary values, and custom theme scales. Version 1.0+ supports Tailwind v4.0+, while versions below 1.0 support Tailwind v3.0 to v3.4.

Tokens
12K
Snippets
17
Records
25
Agent score
99%

What's inside TailwindMerge

  1. Handling ambiguous arbitrary values in tailwind_merge

    main

    When using arbitrary values like text-[calc(var(--rebecca)-1rem)], tailwind_merge attempts to infer the type (e.g., length vs color) to resolve conflicts.

    If the value is ambiguous (e.g., text-[theme(myCustomScale.rebecca)]), it defaults to a text-color class. To force a specific type, use Tailwind CSS data type labels:

    # Use length label to disambiguate
    @merger.merge("text-[length:theme(myCustomScale.rebecca)]")
    # Example of disambiguation using CSS data type labels
    @merger.merge("text-[length:theme(myCustomScale.rebecca)]")
  2. How class groups work in TailwindMerge

    main

    A class group is an array of Tailwind classes that modify the same CSS property. When multiple classes from the same group are passed to merge, only the last one is kept.

    Defining Class Groups

    Class groups can be defined in two ways:

    1. Simple Array: A flat list of class names. Example: ["static", "fixed", "absolute", "relative", "sticky"]
    2. Recursive Object: An object where keys are joined with elements in the array using a dash (-). This is useful for classes like overflow-auto. Example: { overflow: ["auto", "hidden", "visible", "scroll"] }

    Using Validators for Arbitrary Values

    If you cannot enumerate every class (e.g., classes with arbitrary values like fill-[color]), you can use a validator function. The function receives the class part (the string after the prefix) and must return a boolean.

    To indicate a class that represents the DEFAULT state in Tailwind (where the preceding part is the end of the name), use an empty string "" as a class part.

    Example: [{ filter: ["", "none"] }] resolves to filter and filter-none.

    # Simple array group
    position_class_group = ["static", "fixed", "absolute", "relative", "sticky"]
    @merger.merge("static sticky relative") # → "relative"
    
    # Recursive object group
    overflow_class_group = [{ overflow: ["auto", "hidden", "visible", "scroll"] }]
    
    # Group with a validator for arbitrary values
    is_arbitrary_value = (class_part: string) => /^\[.+\]$/.test(class_part)
    fill_class_group = [{ fill: ["current", IS_ARBITRARY_VALUE] }]
  3. How tailwind_merge resolves class conflicts

    main

    The library follows specific rules to handle Tailwind CSS class conflicts:

    • Last conflicting class wins: If multiple classes target the same property (e.g., padding), the last one in the list is kept.
    • Refinements: Classes that refine a property (like px-5 refining p-3) are both kept if they don't conflict.
    • Non-trivial conflicts: It resolves complex conflicts like inset-x-4 vs right-4 or inline vs block.
    • Modifiers: It supports standard, stacked (e.g., hover:focus:), and important (!) modifiers.
    • Arbitrary values/properties: It supports Tailwind's [...] syntax for arbitrary values and properties.
    • Non-Tailwind classes: Any classes that do not match Tailwind patterns are preserved untouched.
    @merger = TailwindMerge::Merger.new
    
    # Last wins
    @merger.merge("p-5 p-2 p-4") # → "p-4"
    
    # Refinements
    @merger.merge("p-3 px-5") # → "p-3 px-5"
    
    # Modifiers
    @merger.merge("hover:p-2 hover:p-4") # → "hover:p-4"
    
    # Important modifiers
    @merger.merge("p-3! p-4! p-5") # → "p-4! p-5"
    
    # Preserving non-tailwind
    @merger.merge("p-5 p-2 my-non-tailwind-class p-4") # → "my-non-tailwind-class p-4"
  4. Handle order-sensitive modifiers

    main

    Most Tailwind modifiers (like hover:) are order-independent; tailwind_merge sorts them internally so they can override each other regardless of input order.

    However, some modifiers are order-sensitive (e.g., the direct children modifier *). In these cases, hover:*:text-red-500 and *:hover:text-red-500 target different elements and should both be preserved.

    Use the order_sensitive_modifiers key in your config to specify which modifiers should have their order preserved. tailwind_merge comes pre-configured with Tailwind's default order-sensitive modifiers.

  5. Performance and Caching in TailwindMerge

    main

    TailwindMerge is optimized for performance through several mechanisms:

    1. LRU Caching: Results are cached by default to prevent wasteful re-renders. The cache is applied after all arguments are joined into a single string. If you call merge with different arguments that produce the same joined string, the cache will be hit.
    2. Default Cache Size: The library uses a Least Recently Used (LRU) cache that stores up to 500 different results by default.
    3. Data Structure Reuse: Expensive computations are performed upfront to ensure that even cache misses remain fast.
    4. Lazy Initialization: Initial computations are deferred until the first call to merge to avoid impacting application startup performance.

    You can modify the cache behavior using the cache_size configuration variable.

  6. Define asymmetric conflicts with conflicting_class_groups

    main

    Standard class groups resolve conflicts symmetrically. However, some Tailwind conflicts are asymmetric.

    Use the conflicting_class_groups configuration to define these. The key is the ID of a class group that creates a conflict, and the value is an array of IDs of class groups that receive the conflict.

    Behavior: If a class group that creates a conflict appears in a string, all preceding class groups in that string that receive the conflict will be removed.

    Example: If px (padding x-axis) creates a conflict received by pr (padding right) and pl (padding left):

    • merge("pr-4 px-3") $\rightarrow$ px-3 removes pr-4.
    • merge("px-3 pr-4") $\rightarrow$ pr-4 does not remove px-3 (because pr does not create a conflict for px).
    # px creates a conflict that pr and pl receive
    conflicting_class_groups = { px: ["pr", "pl"] }
  7. Map custom Tailwind theme scales to tailwind_merge

    main

    If you have added custom theme variables to your Tailwind config (e.g., --text-huge-af: 100px), you must inform tailwind_merge so it can merge classes like text-huge-af correctly.

    Use the theme key in the configuration. The key in the theme object should match the Tailwind namespace (e.g., text for --text-*), and the value should be an array of the variable names.

    Namespace Mapping Reference:

    Tailwind CSS namespacetailwind_merge theme key
    --color-*color
    --font-*font
    --text-*text
    --font-weight-*font-weight
    --tracking-*tracking
    --leading-*leading
    --breakpoint-*breakpoint
    --container-*container
    --spacing-*spacing
    --radius-*radius
    --shadow-*shadow
    --inset-shadow-*inset-shadow
    --drop-shadow-*drop-shadow
    --blur-*blur
    --perspective-*perspective
    --aspect-*aspect
    --ease-*ease
    --animate-*animate
    # If you added --text-huge-af to Tailwind
    merger = TailwindMerge::Merger.new(config: {
      theme: {
        text: ["huge-af"],
      }
    })
  8. Understand conflicting class groups in TailwindMerge

    main

    TailwindMerge uses a definition of conflicting_class_groups to determine which classes override others. For example, if you provide both p (padding) and px (padding-left/right), the engine knows they conflict. This logic ensures that the last class in the list wins when a conflict is detected.

    Key conflict patterns include:

    • Spacing: p conflicts with px, py, ps, pe, etc. m conflicts with mx, my, etc.
    • Sizing: size conflicts with w and h.
    • Layout: flex conflicts with basis, grow, and shrink.
    • Border: border-w conflicts with specific side widths like border-w-t or border-w-x.
    • Rounded: rounded conflicts with specific corner utilities like rounded-t or rounded-ss.
    • Positioning: inset conflicts with top, right, bottom, left, and directional variants like inset-x or inset-y.
  9. Configure tailwind_merge for custom Tailwind configs

    main

    The default TailwindMerge::Merger works out-of-the-box if your custom Tailwind config follows these constraints:

    1. Color names do not clash with other Tailwind class names.
    2. Deviations in number values do not clash with existing number-based classes.
    3. Font-family classes do not clash with default font-weight classes.
    4. All other configurations remain standard.

    If your custom configuration violates these, you must create a custom merge function by extending the default configuration or providing a completely custom one.

    Warning: The tailwind_merge configuration is intended for runtime use (e.g., in the browser or during request handling) and is distinct from the build-time Tailwind configuration. Avoid importing your full Tailwind config into client-side code to prevent massive bundle sizes.

  10. Understand TailwindMerge theme resolution

    main

    TailwindMerge uses a theme configuration to resolve non-arbitrary Tailwind classes. The engine looks up specific keys within your provided theme hash to determine if a class is a valid theme-based value.

    Supported theme categories include:

    • color, font, text, font-weight, tracking, leading, breakpoint, container, spacing, radius, shadow, inset-shadow, text-shadow, drop-shadow, blur, perspective, aspect, ease, animate.

    When a class is encountered, the engine checks if it matches one of these theme keys or follows an arbitrary value pattern (e.g., bg-[#B91C1C]).