Lucide Icons in Rails
Render Lucide icons as inline SVGs via the lucide-rails gem. Icons use currentColor so they inherit text color automatically.
When To Use This Skill
- Adding icons to a Rails view (buttons, nav, badges, empty states)
- Choosing the right Lucide icon name for a UI element
- Making icons accessible (decorative vs interactive)
- Styling or sizing SVG icons with CSS
Philosophy
- Icons are decoration until they aren't — Decorative icons need no label. Interactive icons (buttons, links) MUST have accessible text.
- Size intentionally — Always pass
size:. The default 24px is too large for most inline contexts. - Let CSS handle color — Icons inherit
currentColor. Style the parent, not the SVG. - Use the helper, never raw SVGs —
lucide_iconkeeps icons consistent, cacheable, and updatable. - kebab-case names only — It's
"chevron-right", never"chevronRight"or"ChevronRight".
Installation
# Gemfile
gem "lucide-rails"
bundle install
No JavaScript, no import maps, no asset pipeline config. The gem provides a Ruby helper that renders inline SVGs.
Basic Usage
<%= lucide_icon "home", size: 18 %>
Renders:
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="..."></path>
</svg>
Helper Parameters
| Parameter | Default | Description |
|-----------|---------|-------------|
| size | 24 | Width and height in pixels |
| class | nil | CSS classes |
| stroke_width | 2 | Stroke width (1–3 typical range) |
<%= lucide_icon "home", size: 16, class: "text-muted" %>
<%= lucide_icon "home", size: 24, stroke_width: 1.5 %>
Any additional HTML attributes are passed through to the <svg> element:
<%= lucide_icon "copy", size: 14, data: { action: "click->clipboard#copy" } %>
<%= lucide_icon "x", size: 18, aria: { label: "Close" } %>
Size Guidelines
Pick size based on context. Be consistent within each context.
| Context | Size | When | |---------|------|------| | Badges, inline labels | 12px | Tight spaces, small text | | Dismiss/close, inline actions | 14px | Body text companion | | Buttons with text | 16px | Most common for actions | | Navigation sidebar | 18px | Nav links, menu items | | Card/list item icons | 20px | Content type indicators | | Empty states, hero | 32–48px | Large decorative icons |
Styling with CSS
Color Inheritance
Icons stroke with currentColor — they inherit the parent's color:
.nav-link { color: var(--color-muted); }
.nav-link:hover { color: var(--color-default); }
/* Icon color changes automatically on hover */
Direct Icon Styling
When you need icon-specific colors, use a utility class on the parent or icon wrapper:
.text-success { color: var(--color-positive); }
.text-danger { color: var(--color-negative); }
.text-warning { color: var(--color-warning); }
.text-muted { color: var(--color-muted); }
<span class="text-success"><%= lucide_icon "check", size: 14 %></span>
<span class="text-danger"><%= lucide_icon "alert-circle", size: 14 %></span>
Stroke Width
Thinner strokes look more refined at larger sizes. Thicker strokes read better small.
<%= lucide_icon "settings", size: 48, stroke_width: 1.5 %> <%# Large, elegant %>
<%= lucide_icon "check", size: 12, stroke_width: 2.5 %> <%# Small, readable %>
Dark Mode
Since icons use currentColor, they adapt to dark mode when your text colors do. No extra work needed if your color system uses CSS custom properties.
Common Patterns
Icon + Text Button
<button class="btn btn-primary">
<%= lucide_icon "plus", size: 16 %>
Create Project
</button>
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
Icon-Only Button
<button class="btn-icon" aria-label="Close">
<%= lucide_icon "x", size: 18 %>
</button>
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
}
Icon-only buttons MUST have aria-label.
Navigation Link
<%= link_to dashboard_path, class: "nav-link" do %>
<%= lucide_icon "layout-dashboard", size: 18 %>
Dashboard
<% end %>
Badge with Icon
<span class="badge">
<%= lucide_icon "lock", size: 12 %>
Restricted
</span>
.badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
Empty State
<div class="empty-state">
<%= lucide_icon "folder-plus", size: 48, stroke_width: 1.5 %>
<h3>No projects yet</h3>
<p>Create your first project to get started.</p>
</div>
.empty-state {
text-align: center;
padding: 3rem 1rem;
}
.empty-state svg {
color: var(--color-muted);
margin-bottom: 1rem;
}
Icon in Link (Turbo)
<%= link_to edit_post_path(@post), class: "action-link", data: { turbo_frame: "post_form" } do %>
<%= lucide_icon "edit", size: 14 %>
Edit
<% end %>
Stimulus Integration
<button data-controller="clipboard" data-action="click->clipboard#copy"
data-clipboard-text-value="<%= @invite_url %>">
<%= lucide_icon "copy", size: 14 %>
Copy Link
</button>
Accessibility
Rule: Decorative vs. Meaningful
Decorative — icon accompanies visible text. No extra markup needed:
<button>
<%= lucide_icon "plus", size: 16 %> Add Item
</button>
Meaningful — icon is the only content. MUST add accessible text:
<%# Option 1: aria-label on the interactive element %>
<button aria-label="Close" class="btn-icon">
<%= lucide_icon "x", size: 18 %>
</button>
<%# Option 2: sr-only span %>
<button class="btn-icon">
<%= lucide_icon "x", size: 18 %>
<span class="sr-only">Close</span>
</button>
Screen Reader Only Class
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
SVGs Are aria-hidden by Default
The lucide_icon helper renders SVGs that screen readers skip. This is correct — the accessible name belongs on the interactive parent element, not the icon.
Common Icons by Use Case
Navigation
| Icon Name | Use |
|-----------|-----|
| home | Home/Dashboard |
| layout-dashboard | Dashboard/Overview |
| settings | Settings |
| users | Team/People |
| file-text | Documents/Pages |
| log-out | Sign out |
| log-in | Sign in |
| menu | Hamburger menu |
| search | Search |
Actions
| Icon Name | Use |
|-----------|-----|
| plus | Create/Add |
| edit | Edit/Modify |
| trash | Delete |
| copy | Copy to clipboard |
| x | Close/Dismiss |
| check | Confirm/Save |
| eye | View/Preview |
| eye-off | Hide |
| download | Download |
| upload | Upload |
| external-link | Open in new tab |
Arrows & Chevrons
| Icon Name | Use |
|-----------|-----|
| arrow-left | Back |
| arrow-right | Forward |
| chevron-down | Expand/Dropdown |
| chevron-right | Next/Drill-in |
| chevron-up | Collapse |
| arrow-up-right | External/Outbound |
Status & Feedback
| Icon Name | Use |
|-----------|-----|
| check-circle | Success |
| alert-circle | Error |
| alert-triangle | Warning |
| info | Information |
| help-circle | Help |
| loader | Loading (animate with CSS) |
| lock | Restricted/Private |
| unlock | Public/Open |
Communication
| Icon Name | Use |
|-----------|-----|
| mail | Email |
| bell | Notifications |
| message-circle | Chat/Comments |
| megaphone | Announcements |
| send | Send message |
Files & Content
| Icon Name | Use |
|-----------|-----|
| file | Generic file |
| file-text | Document |
| folder | Folder/Category |
| folder-plus | New folder |
| image | Image/Photo |
| video | Video |
| link | Link/URL |
Social & Misc
| Icon Name | Use |
|-----------|-----|
| github | GitHub |
| twitter | Twitter/X |
| globe | Website/World |
| calendar | Date/Schedule |
| clock | Time/History |
| star | Favorite |
| heart | Like |
| filter | Filter |
| sort-asc | Sort ascending |
| sort-desc | Sort descending |
Anti-Patterns
- Wrong icon name casing —
"chevronRight"→ use"chevron-right"(kebab-case always) - Missing size — Don't rely on default 24px. Be explicit:
size: 16 - Raw
<svg>or<img>tags — Always uselucide_iconhelper - Styling SVG directly — Style the parent element's
color, not the SVG'sstroke - Icon-only button without
aria-label— Screen readers can't describe it - Inconsistent sizes — Pick a size per context and stick to it
- Too many different icons — Reuse the same icon for the same concept across your app
- Using icons without visible text when meaning is ambiguous — A pencil icon might mean "edit" or "write." Add text or a tooltip.
Finding Icons
Browse the full library at lucide.dev/icons.
Search tips:
- Search by concept: "arrow", "file", "user", "check"
- Icons are named descriptively:
circle-checknotcheckmark-in-circle - Related icons share prefixes:
file,file-text,file-plus,file-x - If an icon doesn't exist in lucide-rails, the helper will raise an error at render time
Reference
For detailed patterns, edge cases, and advanced usage, see reference.md.