# 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](https://github.com/dcastil/tailwind-merge) and [clsx](https://github.com/lukeed/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`:
```elixir
def deps do
[
{:tm, path: "../tm_tailwind_merge"}
]
end
```
## Usage
### Basic Merge
```elixir
# 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)
```elixir
# 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
```elixir
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>
"""
end
```
### Just clsx (no merge)
If you only need conditional class building without conflict resolution:
```elixir
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:
1. **clsx** - Recursively processes inputs (strings, lists, maps, keyword tuples), filters falsy values, and joins with spaces
2. **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_color`
- `text-sm`, `text-lg` → `:font_size`
- `text-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