TM - Tailwind Merge for Elixir
Pure Elixir utility for merging Tailwind CSS classes with conditional support. Zero dependencies, fast native BEAM performance.
Based on the JavaScript libraries tailwind-merge and clsx.
Features
- Pure Elixir: No Node.js, no external dependencies
- Fast: Native BEAM performance (~0.02ms per call)
- Conflict Resolution: Later classes override earlier ones for the same CSS property
- Conditional Classes: clsx-style syntax with maps and keyword lists
- Modifier Support: Handles
hover:,dark:,sm:, etc. as separate scopes - Arbitrary Values: Full support for
bg-[#fff],p-[13px], etc.
Installation
Add tm to your dependencies in mix.exs:
def deps do
[
{:tm, path: "../tm_tailwind_merge"}
]
endUsage
Basic Merge
# Later classes win when they conflict
TM.merge("text-red-500 text-blue-500")
#=> "text-blue-500"
TM.merge("p-4 p-2")
#=> "p-2"
# Non-conflicting classes are preserved
TM.merge("p-4 px-2")
#=> "p-4 px-2"
# Modifiers create separate scopes
TM.merge("hover:bg-red-500 hover:bg-blue-500")
#=> "hover:bg-blue-500"
TM.merge("bg-red-500 hover:bg-red-500")
#=> "bg-red-500 hover:bg-red-500"With Conditionals (tc = tailwind conditionals)
# Map syntax
TM.tc(["p-4", %{"bg-red-500" => is_error, "bg-green-500" => is_success}])
#=> "p-4 bg-red-500" (when is_error is true)
# Keyword syntax
TM.tc(["flex", hidden: should_hide])
#=> "flex" or "hidden" depending on should_hide
# Handles nil/false from if() expressions
TM.tc(["base", if(false, do: "hidden")])
#=> "base"
# Multiple conditions
TM.tc([
"px-4 py-2",
%{
"bg-blue-500" => variant == :primary,
"bg-red-500" => variant == :danger
}
])In Phoenix Components
def button(assigns) do
~H"""
<button class={TM.tc([
"inline-flex items-center justify-center font-medium transition-all",
"px-3 py-1.5 text-sm": @size == :sm,
"px-4 py-2 text-base": @size == :md,
"px-6 py-3 text-lg": @size == :lg,
"bg-blue-500 text-white hover:bg-blue-600": @variant == :primary,
"bg-red-500 text-white hover:bg-red-600": @variant == :danger,
"opacity-50 cursor-not-allowed": @disabled
])}>
<%= render_slot(@inner_block) %>
</button>
"""
endJust clsx (no merge)
If you only need conditional class building without conflict resolution:
TM.clsx(["foo", %{"bar" => true, "baz" => false}])
#=> "foo bar"
# Note: clsx doesn't merge conflicts
TM.clsx(["p-4", "p-2"])
#=> "p-4 p-2"API
| Function | Description |
|---|---|
TM.tc/1 | Conditionals + merge (main function) |
TM.merge/1 | Merge only (string or list input) |
TM.clsx/1 | Conditionals only (no merge) |
How It Works
TM is a pure Elixir port of the tailwind-merge and clsx algorithms:
- clsx - Recursively processes inputs (strings, lists, maps, keyword tuples), filters falsy values, and joins with spaces
- tailwind-merge - Parses classes into components (modifiers + base class), identifies class groups, processes right-to-left keeping only the last class per group
Class Groups
Classes are organized into groups based on the CSS property they modify:
p-4,p-2,p-8→:p(padding all)px-4,px-2→:px(padding x)bg-red-500,bg-blue-500→:bg_colortext-sm,text-lg→:font_sizetext-red-500,text-blue-500→:text_color
Groups also define conflicts (e.g., p-* overrides px-*, py-*, etc.)
Performance
Pure Elixir implementation with no external process calls:
- ~0.02ms per merge (vs ~50-100ms with Node.js bridge)
- Zero memory overhead (runs in existing BEAM)
- No startup cost (no process to spawn)
Requirements
- Elixir ~> 1.14
License
MIT