RSSAmplifier

Andy Croll · May 11, 2026

Use class_names to Conditionally Apply CSS Classes

0
Sign in to vote or save

Andy Croll

When you’re building views in Rails, you often need to apply CSS classes conditionally. Maybe a nav link should look different when it’s the current page, or a form field needs error styling. Since Rails 6.1, the class_names helper does this cleanly.

Instead of…

…interpolating conditional classes with ternaries or post-statement conditionals:

<div class="p-4 rounded <%= @error ? 'bg-red-50 border-red-500' : '' %> <%= 'opacity-50 cursor-not-allowed' if @disabled %>">
  <%= @message %>
</div>

Use…

…the class_names helper:

<%= tag.div class: class_names(
  "p-4 rounded",
  "bg-red-50 border-red-500": @error,
  "opacity-50 cursor-not-allowed": @disabled
) do %>
  <%= @message %>
<% end %>

String arguments are always applied; trailing keyword-style entries are included when their value is truthy and silently dropped otherwise.

Better still — Rails tag helpers (tag.*, link_to, form builders) already run the class: argument through the process implicitly, so you can drop the wrapper and hand it an array directly. The trailing key: value pairs don’t need their own {} either; Ruby wraps them into a Hash for you:

<%= tag.div class: [
  "p-4 rounded",
  "bg-red-50 border-red-500": @error,
  "opacity-50 cursor-not-allowed": @disabled
] do %>
  <%= @message %>
<% end %>

An active nav link:

<%= link_to "Home", root_path,
  class: ["nav-link px-3 py-2",
    "text-blue-700 font-semibold": current_page?(root_path)] %>

A form field with errors:

<%= f.text_field :email,
  class: ["field", "field--error": @user.errors[:email].any?] %>

A flash message:

<% flash.each do |type, message| %>
  <%= tag.p message, class: ["flash",
    notice: type == "notice",
    alert: type == "alert"] %>
<% end %>

An active tab:

<%= link_to "Overview", project_path(@project),
  class: ["tab", "tab--active": current_page?(project_path(@project))] %>

Or wrap a repeated pattern in a helper. Helpers often return Strings, which would be a case where you call class_names directly:

def class_names_for_project(project)
  class_names("status-badge",
    "status-badge--primary": project.active?,
    "status-badge--muted": project.archived?)
end
<%= tag.span @project.status, class: class_names_for_project(@project) %>

Why?

The unsophisticated approach ends up with extra whitespace in the rendered HTML with ERB tags inside an HTML attribute (which I’m not a fan of visually), plus it’s hard to scan which classes are always present and which are conditional.

The tag helpers call token_list on whatever you pass to class:, which is aliased as class_names. It splits whitespace-separated tokens, deduplicates them, and returns an HTML-safe string. So ["p-4", "p-4 rounded"] collapses to "p-4 rounded" rather than repeating p-4.

You have to call class_names directly when you’re not inside a tag helper — building a string in a helper method, or interpolating into raw HTML. It’s available in all views and helpers in Rails since it’s defined in ActionView::Helpers::TagHelper. You might also see it referred to as token_list, which is the original method name.

Why not?

If you’ve only got a single conditional class, plain ERB is readable enough:

<div class="p-4 <%= 'font-bold' if @important %>">

Though the array form does avoid the awkward whitespace issue when the condition is false:

<%= tag.div class: ["p-4", "font-bold": @important] do %>

Read the original on andycroll.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.