Find unused CSS classes in your Rails app — zero runtime overhead, smart dynamic class detection.
| Feature | deadweight | rails-css_unused |
|---|---|---|
| Maintained | ❌ abandoned | ✅ |
| Rails 7+ | ❌ | ✅ |
| Static analysis (no server needed) | ❌ needs running server | ✅ |
| BEM selector support | ❌ | ✅ |
| HAML / Slim support | ❌ | ✅ |
| ViewComponent / Phlex support | ❌ | ✅ |
| Stimulus JS class detection | ❌ | ✅ |
| ERB dynamic class extraction | ❌ | ✅ |
| Smart dynamic class variable detection | ❌ | ✅ v0.2.1 |
| Source file attribution | ❌ | ✅ |
| CI exit code support | ❌ | ✅ |
| Regex ignore patterns | ❌ | ✅ |
| Extension false-positive protection | ❌ | ✅ |
# Gemfile
gem "rails-css_unused", group: :developmentbundle install# Standard report
bundle exec rake css_unused:report
# With source file for each ghost class
bundle exec rake css_unused:report_verbose
# CI — exits with code 1 if any ghost classes found
bundle exec rake css_unused:ci# List ghost class names
Rails::CssUnused.ghost_classes # => ["old-btn", "legacy-card"]
# Full report to custom IO
Rails::CssUnused.report(output: File.open("report.txt", "w"))A ghost class is a CSS class that is:
- ✅ Defined in a
.css,.scss, or.sassfile - ❌ Never referenced in any
.erb,.haml,.slim,.rb, or.jsfile
Ghost classes add dead weight to your CSS bundle and confuse future developers.
A common Rails pattern is to assign CSS class strings to a variable and render them via ERB interpolation:
<% status_label, status_class =
if exam.cancelled?
["Cancelled", "status-cancelled"]
elsif exam.approved?
["Approved", "status-approved"]
elsif exam.request_approval?
["Requested approval", "status-requested"]
else
["Draft", "status-draft"]
end %>
<span class="status-pill <%= status_class %>"><%= status_label %></span>Before v0.2.1, status-cancelled, status-approved, status-requested, and status-draft were all reported as ghost classes — false positives. From v0.2.1 onwards, the scanner automatically detects them as used via two smart rules:
Any string assigned to a variable whose name ends in _class, _classes, _style, or _css is treated as a CSS class value:
status_class = "status-active" # ✅ status-active detected
button_classes = "btn btn-primary" # ✅ btn, btn-primary detected
card_style = "card-elevated" # ✅ card-elevated detected
nav_css = "navbar-fixed" # ✅ navbar-fixed detectedRuby variable names cannot contain hyphens — so any quoted string containing a hyphen is unambiguously a string value, never a variable name. The scanner exploits this to safely extract class names:
["Cancelled", "status-cancelled"] # ✅ status-cancelled extracted (hyphen = string value)
["Approved", "status-approved"] # ✅ status-approved extracted
"btn--primary" # ✅ BEM modifier extracted
"card__header" # ✅ BEM element extractedThese two rules together eliminate the most common source of false positives in Rails apps that use server-side conditional class assignment — no ignore_patterns workarounds needed.
Create config/initializers/css_unused.rb:
Rails::CssUnused.configure do |config|
# Paths to scan (relative to Rails.root)
config.stylesheet_paths = %w[app/assets/stylesheets app/assets/builds]
config.view_paths = %w[app/views]
config.component_paths = %w[app/components]
config.javascript_paths = %w[app/javascript]
# Exact class names to never flag as ghost
config.ignore_classes = %w[
clearfix sr-only visually-hidden
active disabled selected
]
# Regex patterns — any matching class is ignored
config.ignore_patterns = [
/\Ajs-/, # JS hook classes (js-submit-btn)
/\Ais-/, # state classes (is-active, is-open)
/\Ahas-/, # state classes (has-error)
]
# Detect classes added via classList.add() in JS files
config.scan_javascript_for_classes = true
# Scan ViewComponent .rb files for class: attributes
config.scan_ruby_components = true
# Show which stylesheet each ghost class came from
config.show_source_files = false
# Exit with code 1 in CI when ghosts are found
config.fail_on_unused = false
endThe scanner detects CSS classes from all of these patterns:
<%# Standard HTML %>
<div class="foo bar baz">
<div class='foo bar'>
<%# Ruby helpers %>
<%= tag.div class: "btn btn-primary" %>
<%= content_tag :div, class: "card" %>
<%= tag.div class: ["card", "card-body"] %>
<%= tag.div class: %w[flex items-center] %>
<%# Dynamic interpolation (static parts extracted) %>
<div class="prefix-<%= var %> suffix">
<div class="<%= condition ? 'on' : 'off' %>">
<%# v0.2.1: Dynamic class variable assignment %>
<% status_class = "status-active" %>
<% button_classes = "btn btn-sm" %>
<span class="<%= status_class %>">
<%# v0.2.1: Conditional multi-branch assignment %>
<% badge_class = exam.passed? ? "badge-success" : "badge-danger" %>-# HAML
.foo.bar
%div.foo.bar
%span{ class: "foo bar" }/ Slim
div.foo.bar
.foo.bar# ViewComponent / Phlex
render MyComponent.new(class: "card card-body")
tag.div(class: "flex items-center")// Stimulus JS
this.element.classList.add("is-loading")
this.element.classList.toggle("hidden")| Situation | Solution |
|---|---|
class="status-#{record.state}" (pure string interpolation) |
Add ignore_patterns << /\Astatus-/ |
<% cls = condition ? "foo-a" : "foo-b" %> |
v0.2.1: auto-detected ✅ |
["Label", "css-class-name"] in conditionals |
v0.2.1: auto-detected ✅ |
JS-only classes (e.g. js-modal-open) |
Auto-ignored via ignore_patterns default |
| Third-party component classes | Add prefix pattern to ignore_patterns |
| Turbo / Stimulus data-action targets | Add to ignore_classes |
See CHANGELOG.md for full version history.
Bug reports and pull requests welcome at https://github.com/sghani001/rails-css_unused.
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Write tests for your changes
- Run
bundle exec rspec - Open a Pull Request
MIT — © Syed Ghani