Icon library support for glimmer-dsl-web projects. Ships Lucide, Heroicons, and Phosphor today.
glimmer-dsl-web has no built-in icon story: every project doing real UI work ends up hand-copying SVG path data into its own components. This gem fills that gap by shipping icon data from popular icon libraries in glimmer-dsl-web's own native format -- so an icon becomes a normal DSL call instead of a pile of hand-maintained SVG markup.
Early. Three libraries ship today, deliberately chosen (after Lucide) to stress-test the design against real differences rather than guessed ones:
:lucide-- one style, stroke-based.:heroicons_outline,:heroicons_solid,:heroicons_mini,:heroicons_micro-- Heroicons' own four style/size variants (24px stroke, 24px fill, 20px fill, 16px fill). Each is a real, separately designed icon set, not a resize.:phosphor_thin,:phosphor_light,:phosphor_regular,:phosphor_bold,:phosphor_fill,:phosphor_duotone-- Phosphor's six "weights". Unlike Heroicons, all six share one identical root<svg>(fill-based, same viewBox) -- still verified per weight rather than assumed, since nothing guarantees a future release keeps it that way, but it turned out not to be where Phosphor's real difference is.
Each of these was added for a specific reason, not just to pad the list:
- Heroicons showed the root
<svg>attributes (fill/stroke/viewBox) aren't universal -- an earlier version hardcoded Lucide's stroke-based convention directly into theIconcomponent, and Heroicons' solid styles (fill: currentColor, no stroke at all) broke that assumption. Fixed by moving those attrs into the registry, per library --register(library, icons, attrs: {...})-- seeregistry.rb. - Phosphor was chosen specifically to test multi-color icon support,
flagged from the start as an unvalidated gap (single-color libraries
don't exercise it). Its
duotoneweight turned out to need no design change at all: the two-tone effect is just two<path>s sharing onefill: currentColor, where the background layer carries its ownopacity="0.2"-- a per-shape attribute, which this gem's[[tag, attrs], ...]format already carried through generically from day one (the same mechanism that already handled Heroicons' per-pathstroke-linecap). Confirmed against all 1512 duotone icons, not assumed from one example -- seegenerate/phosphor.rb.
The public API (library: option, one data file per library that
self-registers with the registry, attrs: for whatever a library's icons
share at the root) has now absorbed two real design pressures without
breaking callers of the first library added under it.
Next additions are meant to match the icon-library coverage of
rails_icons /
Rails-Designer/icons -- the
prior art this gem's two-layer shape (data registry + thin rendering
component) is deliberately modeled on. rails_icons supports 12 libraries
total; the full list, with the 3 already shipped checked off:
- Lucide (1,799 icons)
- Heroicons (4 variants, 324/324/324/316 icons)
- Phosphor (6 weights, 1,512 icons each)
- Tabler (~5,700 icons)
- Hugeicons (~4,600 icons)
- Boxicons (~1,600 icons)
- Radix Icons (~300 icons)
- Feather (~280 icons)
- Linear Icons (~170 icons)
- Flags (~540 icons)
- Weather icons (~215 icons)
- SidekickIcons (~49 icons, complements Heroicons)
Not committed to this exact order, and not committed to shipping all nine remaining -- each one only earns a slot once it's actually confirmed against its real source (the way Lucide/Heroicons/Phosphor were), and only kept if it either works with the current design as-is or teaches it something new, the same bar Heroicons and Phosphor were held to above. Tabler and Feather are stroke-based like Lucide and are expected to need no design changes; Flags and Weather are the more likely candidates to surface something new (Flags in particular may not be simple monochrome shapes).
This assumes your app already has glimmer-dsl-web's Opal build pipeline set
up (i.e. you followed glimmer-dsl-web's own
Rails setup guide
and have a working config/initializers/opal.rb).
-
Add to your
Gemfileandbundle install:gem 'glimmer-dsl-web-icons'
-
Add it to
config.opal.use_gemsinconfig/initializers/opal.rb. This step is easy to miss and the gem will silently fail to compile into your browser bundle without it:config.opal.use_gems = %w[glimmer-dsl-web glimmer-dsl-web-icons]
Why: Opal compiles your
app/assets/opalcode to JavaScript ahead of time, so there's no Ruby process in the browser to resolve arequirelater the way a normal RailsGemfilegem would be.use_gemsis the explicit whitelist that tells the Opal build step which gems' Ruby source to scan and compile into that bundle. Being in yourGemfileonly makes a gem available to server-side Ruby -- any gem your Opal-side coderequires has to also be listed here, and glimmer-dsl-web's own setup guide only shows this line with itself in the array, so it's worth calling out explicitly rather than leaving people to hit the failure and guess. -
In whichever Opal file needs icons, require this gem plus the library data file(s) you want (each library is opt-in, so nothing loads that you don't ask for):
require 'glimmer-dsl-web-icons' require 'glimmer/dsl/web/icons/lucide' require 'glimmer/dsl/web/icons/heroicons' # if you want Heroicons too require 'glimmer/dsl/web/icons/phosphor' # if you want Phosphor too
Note: each data file above compiles its entire icon set into your Opal bundle once required -- Lucide alone is ~1800 icons, Phosphor's six weights ~9000 between them. There's no way yet to require only the handful of icons you actually use (that would mean generating one file per icon instead of one per library) -- a real tradeoff, tracked as a possible future change rather than solved here.
Every icon, from any library, is rendered by the same icon call --
there's no per-library Ruby API, only different library: values (and
different icon names within each):
icon(name, library: :lucide, size: 24)| Option | Required | Default | Meaning |
|---|---|---|---|
name |
yes | -- | Icon name string, e.g. 'circle-check'. Can be passed positionally (icon('circle-check', ...), mirroring shadcn_glimmer's ShButton/ShIcon convention) or as name: -- both are equivalent. |
library |
no | :lucide |
Which registered library/variant to look the name up in (see per-library tables below). Its data file must be required first (Installing, step 3), or Icons.lookup raises UnknownLibraryError. |
size |
no | 24 |
Sets both width and height on the root <svg>, in pixels. The only option that works identically across every library. |
attrs |
no | {} |
Extra attributes merged onto the root <svg>, applied last (so they can override anything above). For attributes the call site knows about, not the library -- data-icon="inline-start", a class, an aria-label on a meaningful icon -- not for changing a library's own fill/stroke defaults. |
An unrecognized icon name raises Icons::UnknownIconError rather than
rendering nothing, so a typo fails loudly instead of silently.
Root <svg> attributes (fill/stroke/stroke-width/viewBox) come from
whichever library: value you pick -- attrs: is for anything on top of
that, not a way to override a library's whole convention. For example, a
button component that needs data-icon for its own spacing CSS:
icon('git-branch', library: :lucide, attrs: {'data-icon' => 'inline-start'})1,799 icons, one style, stroke-based.
icon('circle-check', library: :lucide, size: 20)
icon('circle-check', size: 20) # library: :lucide is the default, can be omittedRoot <svg> attrs: viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round".
4 independently-designed style/size variants. The variant is the
library: value -- there's no separate style:/variant: option. 324
icons per variant (316 for micro -- Heroicons ships a few fewer icons at
that size).
icon('heart', library: :heroicons_outline) # 24px, stroke-based
icon('heart', library: :heroicons_solid) # 24px, fill-based
icon('heart', library: :heroicons_mini) # 20px, fill-based
icon('heart', library: :heroicons_micro) # 16px, fill-basedlibrary: |
viewBox | fill | stroke |
|---|---|---|---|
:heroicons_outline |
0 0 24 24 |
none |
currentColor, stroke-width: 1.5 |
:heroicons_solid |
0 0 24 24 |
currentColor |
none |
:heroicons_mini |
0 0 20 20 |
currentColor |
none |
:heroicons_micro |
0 0 16 16 |
currentColor |
none |
Phosphor -- :phosphor_thin, :phosphor_light, :phosphor_regular, :phosphor_bold, :phosphor_fill, :phosphor_duotone
6 weights, selected via library: the same way as Heroicons' variants.
1,512 icons per weight.
icon('heart', library: :phosphor_thin)
icon('heart', library: :phosphor_light)
icon('heart', library: :phosphor_regular)
icon('heart', library: :phosphor_bold)
icon('heart', library: :phosphor_fill) # flat solid
icon('heart', library: :phosphor_duotone) # two-tone: light bg shape + solid fg shapeAll six weights share identical root <svg> attrs: viewBox="0 0 256 256" fill="currentColor".
The thin/light/regular/bold difference is baked into each icon's own path
data (there's no adjustable stroke-width the way Lucide has one), and
duotone's two-tone look comes from two overlapping shapes at different
opacities within the icon data itself -- nothing you pass at the call site
changes it.
No Bundler required to hack on this gem -- it's pure Ruby with only
rake/minitest as dev dependencies (both ship with a standard Ruby
install). Run the test suite with:
rake test
generate/lucide.rb, generate/heroicons.rb, and generate/phosphor.rb
are dev-only scripts (not shipped in the gem) that regenerate their
respective lib/glimmer/dsl/web/icons/*.rb files from a local
npm install lucide / npm install heroicons /
npm install @phosphor-icons/core checkout under references/ -- see
each script's header comment for how to re-sync when a library releases
new icons.
Each library is one file under lib/glimmer/dsl/web/icons/ that builds an
{"icon-name" => [[tag, attrs], ...]} Hash and calls
Glimmer::DSL::Web::Icons.register(:your_library, that_hash, attrs: {...}),
where attrs: is whatever root <svg> attributes every icon in that
library shares (see registry.rb). Whether a new library needs one
registration or several (the way Heroicons and Phosphor both needed
several) depends on whether it actually ships multiple
independently-designed styles -- confirm that against the library's real
source rather than assuming either way, the same way each of these three
was. See "Roadmap" above for the specific libraries planned next.
MIT.