The fastest, framework-agnostic conditional CSS class builder for Ruby.
Perfect for ViewComponent, Phlex, Tailwind CSS or just standalone.
Inspired by the JavaScript clsx package. Works with any Ruby codebase.
Contents
- Quick Start
- Why clsx-ruby?
- Blazing fast
- More feature-rich than
class_names - Tiny footprint
- Usage
- Bracket API
- Short alias
- Mixin
- Module methods
- Input types
- Framework Integration
- Rails
- Tailwind CSS
- Merging conflicting utilities
- ViewComponent
- Phlex
- Sinatra
- Differences from JavaScript clsx
- Development
- Contributing
- License
Quick Start
bundle add clsx-rubyOr add it manually to the Gemfile:
gem 'clsx-ruby', '~> 1.2'Then use it:
require 'clsx'
Clsx['btn', 'btn-primary', active: is_active, disabled: is_disabled]
# => "btn btn-primary active" (when is_active is truthy, is_disabled is falsy)Why clsx-ruby?
Blazing fast
2–4x faster than Rails class_names — never slower, on realistic markup:
| Scenario | clsx-ruby | Rails class_names
|
Speedup |
|---|---|---|---|
| Token | 4.7M i/s | 1.1M i/s | 4.2x |
| String array | 1.3M i/s | 452K i/s | 3.0x |
| String + hash | 1.8M i/s | 610K i/s | 2.9x |
| Utility string | 1.0M i/s | 371K i/s | 2.7x |
| Long utility | 561K i/s | 209K i/s | 2.7x |
| Utility + hash | 1.1M i/s | 457K i/s | 2.3x |
| Hash | 1.9M i/s | 936K i/s | 2.0x |
Ruby 4.0.5, Apple M1 Pro. 2.8× geomean; each row verified to produce output identical to Rails class_names. Reproduce: bundle exec ruby benchmark/vs_rails.rb
More feature-rich than class_names
| Feature | clsx-ruby | Rails class_names
|
|---|---|---|
| Conditional classes | ✅ | ✅ |
| Auto-deduplication | ✅ | ✅ |
| 2–4× faster | ✅ | ❌ |
Returns nil when empty |
✅ | ❌ (returns "") |
| Complex hash keys | ✅ | ❌ |
| Framework-agnostic | ✅ | ❌ |
| Zero dependencies | ✅ | ❌ |
Tiny footprint
~100 lines of code. Zero runtime dependencies. Ruby 3.2+.
Usage
Bracket API (recommended)
require 'clsx'
Clsx['foo', 'bar']
# => 'foo bar'
Clsx['foo', bar: true, baz: false]
# => 'foo bar'
Clsx['btn', 'btn-primary', active: is_active, disabled: is_disabled]
# => 'btn btn-primary active' (when is_active is truthy, is_disabled is falsy)Short alias
Cn['foo', bar: true]
# => 'foo bar'Cn is defined only if the constant is not already taken.
Mixin
include Clsx::Helper
clsx('foo', 'bar')
# => 'foo bar'
cn(hidden: @hidden, 'text-bold': @bold)
# => 'hidden text-bold' (when both are truthy)Module methods
Clsx.clsx('foo', 'bar')
# => 'foo bar'
Clsx.cn('foo', bar: true)
# => 'foo bar'Input types
# Strings (variadic)
Clsx['foo', true && 'bar', 'baz']
# => 'foo bar baz'
# Hashes
Clsx[foo: true, bar: false, baz: a_truthy_method]
# => 'foo baz'
# Hashes (variadic)
Clsx[{ foo: true }, { bar: false }, nil, { '--foobar': 'hello' }]
# => 'foo --foobar'
# Arrays
Clsx[['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'
# Kitchen sink (with nesting)
Clsx['foo', ['bar', { baz: false, bat: nil }, ['hello', ['world']]], 'cya']
# => 'foo bar hello world cya'Framework Integration
clsx is framework-agnostic. Any Ruby view object — ViewComponent, Phlex, or a plain object —
gets clsx/cn by including Clsx::Helper; no adapter needed.
Rails
The companion clsx-rails gem auto-loads clsx/cn
into every view. You can also use the bracket API directly in ERB:
<%= tag.div class: Clsx['foo', 'baz', 'is-active': @active] do %>
Hello, world!
<% end %>Tailwind CSS
Conditional utilities
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
endMerging 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-ruby itself stays dependency-free), then require the
integration once at boot:
# config/initializers/clsx.rb
require 'clsx/tailwind_merge'
# Optional: configure the merger (prefix, cache size, custom theme, …)
Clsx.merger = TailwindMerge::Merger.new(config: { prefix: 'tw' })This adds a merged variant — twm / Twm[] — the last conflicting utility wins.
clsx/cn stay pure; only twm/Twm merge:
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)
# Also available as a mixin method and a module method:
include Clsx::Helper
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
endAccept 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
endPhlex's own class: [...] arrays and mix cover simple cases. Reach for clsx when you want
hash-conditional syntax (pill: @pill) or cross-argument dedup when merging caller-supplied
classes.
Sinatra
erb :"<div class='#{Clsx['nav', active: @active]}'>...</div>"Differences from JavaScript clsx
-
Returns
nilwhen no classes apply (not an empty string). This prevents rendering emptyclass=""attributes in template engines that skipnil: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
Development
bin/setup # install dependencies
bundle exec rake test # run tests
bundle exec ruby benchmark/vs_rails.rb # run benchmarksContributing
Bug reports and pull requests are welcome on GitHub at https://github.com/svyatov/clsx-ruby.
License
The gem is available as open source under the terms of the MIT License.