Project

clsx-rails

0.0
The project is in a healthy, maintained state
Build CSS class strings from conditional expressions, hashes, arrays, or nested structures. 2-4x faster drop-in replacement for Rails class_names. Supports ViewComponent, Phlex, and Tailwind CSS.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

clsx-rails Gem Version Codecov CI GitHub License

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?
    • Blazing fast
    • More feature-rich than class_names
  • Usage
    • Input types
    • Template engines
  • Framework Integration
    • Tailwind CSS
      • Merging conflicting utilities
    • ViewComponent
    • Phlex
  • 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

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

  1. Returns nil when no classes apply (not an empty string). Rails tag helpers skip nil, preventing empty class="" attributes:

    clsx(nil, false) # => nil
  2. Deduplication — Duplicate classes are automatically removed, even across multi-token strings:

    clsx('foo', 'foo')      # => 'foo'
    clsx('foo bar', 'foo')  # => 'foo bar'
  3. Falsy values — In Ruby only false and nil are falsy, so 0, '', [], {} are all truthy:

    clsx('foo' => 0, bar: []) # => 'foo bar'
  4. Complex hash keys — Any valid clsx input works as a hash key:

    clsx([{ foo: true }, 'bar'] => true) # => 'foo bar'
  5. Ignored values — Boolean true and Proc/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.