Icon
Pluggable icon system. Per the answered design question recorded with
this component: function-based adapter — getIcon(name, size)
returns an SVG string; the runtime injects it into placeholder
elements at init() time.
The chrome ships in @malevich/components. Icon packs ship as
separate packages (@malevich/icons-default for Phosphor,
@malevich/icons-lucide, etc. — packaged as a follow-up mission;
the registry API is ready and the placeholder renders correctly
without any pack registered).
When to use
- Any place you need a meaningful visual glyph: button labels, status indicators, navigation, empty states.
For animated icons that morph between states (copy ↔ check), use
@malevich/morph-icons instead.
Sizes
| Variant | Class | Default |
|---|---|---|
| inline | .icon |
1em — scales with surrounding text |
| Small | .icon.-s |
size-control-s |
| Medium | .icon.-m |
size-control-m |
| Large | .icon.-l |
size-control-l |
| Extra large | .icon.-xl |
size-control-xl |
Tones
| Variant | Class | Color |
|---|---|---|
| inherit | .icon |
currentColor (inherits) |
| Accent | .icon.-accent |
color.accent |
| Danger | .icon.-danger |
color.danger |
| Success | .icon.-success |
color.success |
| Warning | .icon.-warning |
color.warning |
| Info | .icon.-info |
color.info |
| Muted | .icon.-muted |
color.ink-subtle |
Anatomy
<!-- Inline with text (size = 1em) -->
<p>Saved <i class="icon" data-icon="check"></i></p>
<!-- With explicit size + tone -->
<i class="icon -m -success" data-icon="check"></i>
<!-- Using a non-default pack -->
<i class="icon -l" data-icon="rocket" data-icon-pack="lucide"></i>
The runtime (initIcon, auto-registered for .icon[data-icon]):
- Reads
data-icon-pack(defaults to"default"). - Looks up the registered
getIconfor that pack. - Calls
getIcon(name)and injects the returned SVG string. - Adds
aria-hidden="true"andfocusable="false"to the SVG so the glyph is decorative by default.
Without a registered pack, the host stays empty but its sized box is
reserved — no layout shift when the pack registers later (call
refreshIcons() after registration).
Registering a pack
import { registerIconPack, refreshIcons, init } from "@malevich/components";
import { getIcon } from "@malevich/icons-default";
registerIconPack(getIcon); // default pack
init();
refreshIcons(); // re-render placeholders that were waiting
A pack is just a function:
type GetIcon = (name: string, size?: number | string) => string | null;
Returns an SVG markup string for known names, null for unknown.
Multiple packs:
registerIconPack(getPhosphorIcon, "default");
registerIconPack(getLucideIcon, "lucide");
registerIconPack(getTablerIcon, "tabler");
The host element picks via data-icon-pack.
Accessibility
- Icons are decorative by default — the runtime adds
aria-hidden="true"to the injected SVG. - For meaningful icons (no surrounding text), wrap in a
<button>or<a>with anaria-label. Don't rely on the icon glyph alone to convey meaning to assistive tech. - For meaningful inline icons inside text, the surrounding text carries the meaning; keep the icon decorative.
Edge cases
- Unknown icon name:
getIconreturns null; the placeholder stays empty. CSS still reserves the sized box. - Late pack registration: call
refreshIcons()after registering a pack to re-render placeholders that were empty during the first init pass. - Pack swap: changing
data-icon-packafter init does not automatically re-render. CallrefreshIcons()orinitIcon(host)again on the affected host.
Do
- Use
<i>as the host element (semantically neutral, short tag). - Pair
aria-labelon parent interactive elements when the icon carries meaning. - Use the inline (1em) sizing inside button labels so the icon matches text size.
Don't
- Don't embed SVG manually inside
.icon. The runtime owns innerHTML. - Don't apply colors directly on the SVG — use
.icon.-{tone}or setcoloron the host so currentColor inherits. - Don't omit
data-icon— the runtime needs it to know what to render.