Glyphs
Phlex icon components for every rails_icons library — plus bundled RuboCop cops that validate icon names at lint time and autocorrect legacy icon helpers.
class ApplicationComponent < Phlex::HTML
include Glyphs
end
LucideIcon(:house, class: "size-4")
PhosphorIcon("lock", variant: :bold)
HeroIcon(:check, class: "size-5 text-success")
Installation
# Gemfile
gem "glyphs"
Glyphs renders the SVG files that rails_icons syncs into your app. If you haven't already:
rails generate rails_icons:install --libraries=lucide heroicons
rails generate rails_icons:sync --libraries=lucide heroicons
Outside Rails, configure the icons gem directly:
Icons.configure do |config|
config.base_path = File.(__dir__)
config.icons_path = "svg/icons"
end
Components
include Glyphs (a Phlex::Kit) into your component base class and call icons as capitalized methods:
| Component | rails_icons library |
|---|---|
LucideIcon |
lucide |
PhosphorIcon |
phosphor |
HeroIcon |
heroicons |
TablerIcon |
tabler |
FeatherIcon |
feather |
BoxIcon |
boxicons |
FlagIcon |
flags |
HugeIcon |
hugeicons |
LinearIcon |
linear |
RadixIcon |
radix |
SidekickIcon |
sidekickicons |
WeatherIcon |
weather |
AnimatedIcon |
animated (bundled spinners: faded-spinner, bouncing-dots, ...) |
Names can be symbols or strings; underscores are dasherized (:circle_check → circle-check.svg).
variant: selects the library variant (default comes from your rails_icons/icons configuration);
every other keyword (class:, data:, stroke_width:, ...) is forwarded onto the <svg> tag.
The generic component requires an explicit library:
Icon(:house, library: :lucide) # flagged by Glyphs/PreferLibraryComponent, prefer LucideIcon(:house)
Custom libraries get first-class components too:
Glyphs.register_library(:brand, component: :BrandIcon)
BrandIcon(:logo)
(The SVG location for a custom library is configured through rails_icons/icons custom-library config.)
Missing-icon policy
# config/initializers/glyphs.rb
Glyphs.configure do |config|
# Re-raise Icons::IconNotFound? Defaults to true in local Rails
# environments (and outside Rails), false otherwise.
config.raise_on_missing = Rails.env.local?
# Optional handler, called before the fallback renders (when not raising).
config.on_missing_icon = lambda do |error, name:, library:, variant:|
Rails.logger.error("Icon missing: #{library}/#{variant}/#{name} (#{error.})")
end
# Rendered instead of the missing icon (per library). nil/absent => re-raise.
config.fallback_icons = {
lucide: "circle-question-mark",
phosphor: "question",
heroicons: "question-mark-circle"
}
# Memoize rendered SVG strings per [library, variant, name, attributes]. Default: true.
# Note: fallback renders are cached under the original name, so on_missing_icon
# fires once per process per missing icon rather than on every render.
config.cache_svgs = true
end
Shrinking icons in Docker
rails g rails_icons:sync copies a library's entire icon set into
app/assets/svg/icons/<library>/<variant>/ — often thousands of SVGs, of which
an app uses a handful. glyphs:prune_icons deletes the unreferenced ones so a
Docker image ships only what it renders, while the committed repo keeps the full
set for development.
It scans your source (app/**/*.rb, lib/**/*.rb with a real parser, plus
.erb/.haml/.slim text) for LucideIcon(:house), legacy helpers, generic
Icon(:x, library: :lucide), and iconify lucide--house class strings, then
keeps that set plus two safety nets: the keep_icons allowlist and every
configured fallback_icons (a pruned fallback would 500 on the next missing
icon).
Dynamic calls are resolved automatically
Most apps render most icons dynamically — LucideIcon(tile[:icon]),
PhosphorIcon(notification.icon) — where the name lives in a { icon: :activity }
hash, a CHANNEL_ICONS map, or an ICON = :bell constant, sometimes in a
different file from the render. A naïve static scan can't read those names and
would prune the icons they use.
The scanner resolves them from source, so you rarely need keep_icons at all:
- File-scoped — a file that renders a library dynamically (
LucideIcon(x)) keeps every icon-name-shaped literal in that file for that library. Catches ternaries (@open ? "caret-up" : "caret-down"),case/when, and locals. - Declaration-based — literals in icon-declaration positions anywhere — a
hash pair keyed like an icon (
icon: :gear,menu_icon: "house") or a constant named like one (ICON = :bell,STATUS_ICONS = { .. => :warning }) — are kept for every dynamically-rendered library, so a name declared in one file and rendered from another survives.
Only literals are harvested, so the scanner never invents a reference; the worst case is keeping a coincidentally icon-named string, which the post-prune verification tolerates.
keep_icons — the last-resort escape hatch
For names a static scan genuinely can't see — read from a database, an ENV var, or a gem's own chrome — list them explicitly:
# config/initializers/glyphs.rb
Glyphs.configure do |config|
# Flat list or per-library hash; names or fnmatch globs.
config.keep_icons = %w[menu palette search circle-*]
# config.keep_icons = { lucide: %w[menu palette], phosphor: %w[lock] }
end
# Preview (dry run — deletes nothing):
bin/rails glyphs:prune_icons
# Delete (both env vars required — a deliberate opt-in so it never fires
# accidentally on a developer's working tree):
PRUNE=1 GLYPHS_PRUNE_ICONS=1 bin/rails glyphs:prune_icons
Run it in the image build, after assets:precompile and before the final
stage copies the app — so the deletions land in the image but never in a
developer's checkout:
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
RUN SECRET_KEY_BASE_DUMMY=1 PRUNE=1 GLYPHS_PRUNE_ICONS=1 ./bin/rails glyphs:prune_icons
After deleting, the task verifies every kept icon still resolves and exits
non-zero if not — a bad prune (e.g. a missing allowlist entry) fails the build
instead of shipping broken icons. The bundled animated library and any
custom_path library are left untouched, and it refuses to empty a library
whose keep-set is empty.
RuboCop cops
Add the plugin (RuboCop >= 1.72):
# .rubocop.yml
plugins:
- glyphs
(Classic require: [glyphs/rubocop] also works, but you must enable the cops yourself.)
Glyphs/LegacyIconHelper
Autocorrects legacy helper calls to Glyphs components:
_lucide(:house, class: "size-4") # => LucideIcon(:house, class: "size-4")
_heroicon(:check) # => HeroIcon(:check)
icon("check", library: "lucide") # => LucideIcon("check")
icon("check") # => HeroIcon("check") (DefaultLibraryComponent)
Glyphs/LegacyIconHelper:
Include:
- app/**/*.rb
DefaultLibraryComponent: HeroIcon
# Mappings replaces the built-in defaults (_lucide/_phosphor/_hero/_heroicon/_tabler):
# Mappings:
# _custom: CustomIcon
Glyphs/IconResolution
Validates statically-known icon names against your synced SVG directories, honors literal
variant: keywords, suggests close matches (with autocorrect for unambiguous typos and the
Lucide v1 renames), and flags raw iconify class strings:
LucideIcon(:alert_triangle) # => corrected to LucideIcon(:triangle_alert)
span(class: "iconify lucide--house size-4") # => corrected to LucideIcon(:house, class: "size-4")
Glyphs/IconResolution:
Include:
- app/**/*.rb
IconsPath: app/assets/svg/icons
# Libraries merges over the built-in defaults (component => Dir/DefaultVariant):
Libraries:
PhosphorIcon: { Dir: phosphor, DefaultVariant: regular }
Glyphs/PreferLibraryComponent
Icon(:house, library: :lucide) # => LucideIcon(:house)
Migrating an app off icon helpers
- Add
gem "glyphs",include Glyphswhere the helpers used to be included. - Add the plugin and cop config above to
.rubocop.yml. - Delete your local icon helper, then run:
bundle exec rubocop -A --only Glyphs/LegacyIconHelper,Glyphs/PreferLibraryComponent
- Wire your old missing-icon logging into
Glyphs.configure(see above).
Development
bundle install
bundle exec rspec
bundle exec rubocop
License
MIT — see LICENSE.txt.