clsx-rails

The fastest conditional CSS class builder for Rails — 2–4x faster drop-in replacement for
class_names.
Powered by clsx-ruby.
Auto-loads clsx and cn helpers into every Rails view. Zero configuration.
Contents
- Quick Start
- Why clsx-rails?
- Usage
- Framework Integration
- Differences from JavaScript clsx
- Supported Versions
- Development
- Contributing
- License
Quick Start
bundle add clsx-rails
Or add it manually to the Gemfile:
gem 'clsx-rails', '~> 3.1'
That's it — clsx and cn are now available in all your views:
<%= tag.div class: clsx('btn', 'btn-primary', active: @active) do %>
Click me
<% end %>
Why clsx-rails?
Blazing fast
2–4x faster than Rails class_names — never slower, on realistic markup:
| Scenario | clsx | Rails class_names |
Speedup |
|---|---|---|---|
| String array | 1.2M i/s | 317K i/s | 3.9x |
| Multiple strings | 1.3M i/s | 346K i/s | 3.8x |
| Single string | 2.3M i/s | 812K i/s | 2.9x |
| Mixed types | 901K i/s | 331K i/s | 2.7x |
| Hash | 1.7M i/s | 684K i/s | 2.4x |
| String + hash | 1.2M i/s | 550K i/s | 2.1x |
Ruby 4.0.1, Apple M1 Pro. Reproduce: bundle exec ruby benchmark/run.rb</sup>
More feature-rich than class_names
| Feature | clsx-rails | Rails class_names |
|---|---|---|
| Conditional classes | ✅ | ✅ |
| Auto-deduplication | ✅ | ✅ |
| 2–4× faster | ✅ | ❌ |
Returns nil when empty |
✅ | ❌ (returns "") |
| Complex hash keys | ✅ | ❌ |
Tailwind conflict merge (twm) |
✅ | ❌ |
Short cn alias |
✅ | ❌ |
Usage
clsx and its cn alias are available in every view — no include, no setup.
Input types
# Strings (variadic)
clsx('foo', true && 'bar', 'baz')
# => 'foo bar baz'
# Hashes
cn(foo: true, bar: false, baz: a_method_that_returns_true)
# => 'foo baz'
# Hashes (variadic)
clsx({ foo: true }, { bar: false }, nil, { '--foobar': 'hello' })
# => 'foo --foobar'
# Arrays
cn(['foo', nil, false, 'bar'])
# => 'foo bar'
# Arrays (variadic)
clsx(['foo'], ['', nil, false, 'bar'], [['baz', [['hello'], 'there']]])
# => 'foo bar baz hello there'
# Symbols
clsx(:foo, :'bar-baz')
# => 'foo bar-baz'
# Numbers
clsx(1, 2, 3)
# => '1 2 3'
# Multi-token strings (deduplicated)
clsx('a b', 'b c')
# => 'a b c'
# Whitespace is normalized; blank/whitespace-only => nil
clsx(" a\tb\n\nc ")
# => 'a b c'
# Kitchen sink (with nesting)
cn('foo', ['bar', { baz: false, bat: nil }, ['hello', ['world']]], 'cya')
# => 'foo bar hello world cya'
Template engines
<%# ERB %>
<%= tag.div class: clsx('foo', 'baz', 'is-active': @active) do %>
Hello, world!
<% end %>
<div class="<%= clsx('foo', 'baz', 'is-active': @active) %>">
Hello, world!
</div>
-# HAML
%div{class: clsx('foo', 'baz', 'is-active': @active)}
Hello, world!
/ Slim
div class=clsx('foo', 'baz', 'is-active': @active)
| Hello, world!
Framework Integration
Plain Rails views (ERB, HAML, Slim) get clsx/cn automatically. ViewComponent and Phlex
render in their own object hierarchies, so include Clsx::Helper once in your base class.
Tailwind CSS
Compose conditional utilities without string juggling:
class NavLink < ViewComponent::Base
include Clsx::Helper
def initialize(active: false)
@active = active
end
def classes
clsx(
'px-3 py-2 rounded-md text-sm font-medium transition-colors',
'text-white bg-indigo-600': @active,
'text-gray-300 hover:text-white hover:bg-gray-700': !@active
)
end
end
Merging conflicting utilities
clsx/cn keep every class, so conflicting Tailwind utilities both survive:
clsx('px-2 px-4') # => 'px-2 px-4'
For conflict resolution, opt into the tailwind_merge
gem. Add it to your Gemfile (clsx-rails itself pulls in only clsx-ruby), then require the
integration once at boot:
# Gemfile
gem 'tailwind_merge'
# config/initializers/clsx.rb
require 'clsx/tailwind_merge'
# Optional: configure the merger (prefix, cache size, custom theme, …)
Clsx.merger = TailwindMerge::Merger.new
This adds a merged variant — twm — available in every view alongside clsx/cn.
clsx/cn stay pure; only twm merges, and the last conflicting utility wins:
twm('px-2 px-4') # => 'px-4'
twm('p-4', 'p-2', 'bg-red', 'bg-blue') # => 'p-2 bg-blue'
clsx('px-2 px-4') # => 'px-2 px-4' (unchanged)
It accepts the same arguments as clsx (strings, hashes, arrays, nesting), then merges:
twm(['px-2', 'rounded'], 'px-4', 'px-6': active) # => 'rounded px-6' (when active)
# Also available as a bracket API and a module method:
Twm['px-2 px-4'] # => 'px-4'
Clsx.twm('px-2 px-4') # => 'px-4'
ViewComponent
Include the mixin once in your base component instead of per component:
class ApplicationComponent < ViewComponent::Base
include Clsx::Helper
end
Accept a caller-supplied class: and merge it — clsx dedupes across every argument, so
callers can extend or repeat classes safely:
class AlertComponent < ApplicationComponent
def initialize(variant: :info, dismissible: false, class: nil)
@variant = variant
@dismissible = dismissible
@html_class = binding.local_variable_get(:class) # `class` is a Ruby keyword
end
def classes
clsx("alert", "alert-#{@variant}", @html_class, dismissible: @dismissible)
end
end
<div class="<%= classes %>">...</div>
Phlex
Include the mixin once in your base component, then merge caller-supplied attributes — clsx dedupes across every argument:
class ApplicationComponent < Phlex::HTML
include Clsx::Helper
end
class Badge < ApplicationComponent
def initialize(color: :blue, pill: false, **attributes)
@color = color
@pill = pill
@attributes = attributes
end
def view_template
span(class: clsx("badge", "badge-#{@color}", @attributes[:class], pill: @pill)) { yield }
end
end
Differences from JavaScript clsx
-
Returns
nilwhen no classes apply (not an empty string). Rails tag helpers skipnil, preventing emptyclass=""attributes:clsx(nil, false) # => nil -
Deduplication — Duplicate classes are automatically removed, even across multi-token strings:
clsx('foo', 'foo') # => 'foo' clsx('foo bar', 'foo') # => 'foo bar' -
Falsy values — In Ruby only
falseandnilare falsy, so0,'',[],{}are all truthy:clsx('foo' => 0, bar: []) # => 'foo bar' -
Complex hash keys — Any valid
clsxinput works as a hash key:clsx([{ foo: true }, 'bar'] => true) # => 'foo bar' -
Ignored values — Boolean
trueandProc/lambda objects are silently ignored:clsx('', proc {}, -> {}, nil, false, true) # => nil
Supported Versions
Ruby 3.2+ and Rails 7.2+.
Development
bin/setup # install dependencies
bundle exec rake test # run tests
bundle exec ruby benchmark/run.rb # run benchmarks
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/svyatov/clsx-rails.
License
The gem is available as open source under the terms of the MIT License.