Icons

Summary

41 Lucide-sourced glyphs shipped as CSS mask images, colored by currentColor: an element's text color becomes the icon color, no font and no inline SVG. Each glyph is also a root-level custom property (--soma-icon-<name>) so component CSS can use glyphs without markup (dropdown chevrons, sort indicators). The set is curated in src/icons/icons.config.mjs; npm run icons:build regenerates.

When to use

PatternUse it forAccessibility consequence
Inline with text Buttons, menu items, messages: the icon reinforces a visible label and picks up its color. Nothing extra: the visible text is the accessible name; the empty icon span contributes nothing.
Icon-only control Toolbar actions, dismiss buttons — the icon is the label. The control MUST get an aria-label (or hidden-text fallback, below); otherwise it announces as an unnamed button.
Decorative A visual hint that adds no information next to visible text. Already silent: an empty <span> has no accessible name and no role; don't add a label that would duplicate the text.

Examples

The set

HTML

An inline icon picks up currentColor from its parent:

<span class="soma-icon soma-icon-settings"></span>

Inside a button with a text label, nothing extra is needed:

<button class="soma-button">
  <span class="soma-icon soma-icon-plus"></span>
  Add item
</button>

CSS-only usage in your own component styles (no markup span):

.my-thing::before {
  mask-image: var(--soma-icon-chevron-down);
}

Labelling icon-only controls

An icon-only control has no text, so it has no accessible name. Give it one with aria-label:

<button class="soma-button soma-button-subtle" aria-label="Edit">
  <span class="soma-icon soma-icon-pencil"></span>
</button>

Alternatively, text inside the icon span is visually hidden (clipped by the mask box) but still read by screen readers; use it as a fallback label where an attribute is awkward, e.g. markup generated from data:

<span class="soma-icon soma-icon-mail">Email</span>

The glyph grid above uses exactly this idiom — each cell's icon span contains the glyph name as hidden text.

CSS classes

ClassEffect
.soma-iconBase 16px glyph box; combines with a per-glyph class.
.soma-icon-<name>Selects the glyph (names below each cell).
.soma-icon-smallExplicit 16px, identical to the base; exists so markup can state the size.
.soma-icon-large32px.

JavaScript

None — icons are CSS-only. Color comes from currentColor, so styling is just a matter of setting color on the parent (or the icon span itself).

Adding a new icon

The set is a curated list in src/icons/icons.config.mjs, each entry mapping a Soma glyph name to a Lucide source SVG. Append an entry:

// src/icons/icons.config.mjs
export default [
  { name: 'apps',      source: 'lucide', from: 'grid-3x3' },
  // …
  // add your new icon:
  { name: 'heart',     source: 'lucide', from: 'heart' },
];

Then npm run icons:build regenerates src/css/_icons.generated.scss (the dev and build scripts also run it automatically, and the output is committed so the repo builds without re-running it). Your glyph is now .soma-icon-heart and --soma-icon-heart.

Two entry options: strokeWidth overrides Lucide's default stroke of 2 — the close glyph ships at strokeWidth: 3.10 because at dismiss-button sizes the stock ✕ reads lighter than every other glyph. And source: 'inline' embeds a hand-drawn or third-party glyph as a raw SVG string:

{ name: 'close', source: 'lucide', from: 'x', strokeWidth: 3.10 },
{ name: 'brand', source: 'inline', svg: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">…</svg>' },

If the inline geometry isn't yours, add the attribution to NOTICE, the same rule the Lucide set itself follows.

One-off icons without the build script

Consuming apps never need the build script: each glyph class only sets the --soma-icon-mask custom property, and the base class does all the rendering. A one-off icon is plain CSS:

.icon-heart {
  --soma-icon-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' …%3E…%3C/svg%3E");
}
<span class="soma-icon icon-heart"></span>