glimmer-dsl-web-icons
Icon library support for glimmer-dsl-web projects. Ships Lucide, Heroicons, and Phosphor today.
Why
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.
Status
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.
Roadmap: planned libraries
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).
Installing in a Rails app
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.
Usage
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'})Lucide -- :lucide (default)
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".
Heroicons -- :heroicons_outline, :heroicons_solid, :heroicons_mini, :heroicons_micro
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.
Development
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.
Adding another library
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.
License
MIT.