Icons
Icons are inline SVGs rendered by app/helpers/icon_helper.rb, sourced from
Lucide (ISC license — free to use, no attribution
required). There is no icon font, no icon webfont/CDN, and no JS icon
library — this replaced a FontAwesome Kit CDN script that used to load on
every page (see PR #339).
Using an existing icon
<%= icon(:trash-2) %> <%# decorative, paired with visible text - aria-hidden %>
<%= icon(:"trash-2", label: t("Delete")) %> <%# icon-only control - needs an accessible name %>
<%= icon(:bookmark, filled: true) %> <%# solid variant, e.g. a toggled/active state %>
<%= icon(:bookmark, size: 32, class: "foo") %> <%# override size (default 20) / add classes %>
Every icon renders with the class icon icon-<name> (plus icon--filled
when filled: true), so CSS can target .icon generically or a specific
.icon-<name> variant. Icons need a symbol/string key matching one of the
names in IconHelper::ICONS — see that file for the current list.
Rules for the label:/decorative choice: if the icon sits next to visible
text (a button labelled “Delete” with a trash icon beside it), leave label
off — the icon is decorative and gets aria-hidden="true" automatically. If
the icon is the only content of a link/button (a bare close “x”, a bare
bookmark toggle), always pass label: with a real accessible name — icon-only
controls with no label are invisible to screen readers.
Adding a new icon
- Find the icon at lucide.dev/icons and note its
kebab-case name (e.g.
circle-check). - Fetch the raw SVG source directly (don’t guess the path data from memory —
copy it exactly):
curl -s https://raw.githubusercontent.com/lucide-icons/lucide/main/icons/circle-check.svg - Copy everything inside the
<svg>tag (the<path>/<circle>/etc. elements only — not the outer<svg ...>wrapper, sinceicon_helper.rbsupplies that itself with the project’s standard attributes). - Add an entry to the
ICONShash inapp/helpers/icon_helper.rb, keyed by the same kebab-case name as a symbol:"circle-check": '<circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" />', - Use it:
<%= icon(:"circle-check") %>.
All Lucide icons share the same 24x24 viewBox and stroke-based style
(stroke="currentColor", stroke-width="2", round caps/joins), so a new
icon’s paths drop in without needing per-icon attribute tweaking. If an icon
needs to support a filled/solid state (like bookmark’s toggle between saved
and not-saved), check that its shape is a single closed path first — Lucide
icons made of multiple thin stroke paths (e.g. bell’s clapper, circle-x’s
X) can look wrong with fill: currentColor applied, since the fill and
stroke share the same color and thin strokes can visually disappear into a
solid fill. icon(name, filled: true) sets fill="currentColor" on the
whole <svg>, so test the rendered output before relying on it for
multi-path icons.