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 %>
Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.