The Icon chrome ships standalone. This page registers a tiny inline demo pack so the placeholders render.
Saved successfully. Warning please review.
/*
* Icon — foundation
*
* The visual chrome. Authors write a placeholder element:
*
* <i class="icon" data-icon="check"></i>
*
* The icon adapter runtime swaps the placeholder's innerHTML with the
* SVG fetched from a registered icon pack (e.g. @malevich/icons-default).
* Without a registered pack, the element stays empty — CSS still
* applies (sized box reserved, currentColor binding ready).
*
* Per the answered design question recorded with this component:
* function-based adapter — getIcon(name, size) -> SVG string.
*/
@layer malevich.components {
.icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
inline-size: var(--icon-inline-size);
block-size: var(--icon-inline-size);
color: currentColor;
fill: currentColor;
stroke: currentColor;
line-height: 1;
vertical-align: var(--icon-baseline);
}
.icon > svg {
inline-size: 100%;
block-size: 100%;
}
/* Size variants — overrideable */
.icon.-s {
inline-size: var(--size-control-s);
block-size: var(--size-control-s);
}
.icon.-m {
inline-size: var(--size-control-m);
block-size: var(--size-control-m);
}
.icon.-l {
inline-size: var(--size-control-l);
block-size: var(--size-control-l);
}
.icon.-xl {
inline-size: var(--size-control-xl);
block-size: var(--size-control-xl);
}
/* Tone variants */
.icon.-accent { color: var(--color-accent); }
.icon.-danger { color: var(--color-danger); }
.icon.-success { color: var(--color-success); }
.icon.-warning { color: var(--color-warning); }
.icon.-info { color: var(--color-info); }
.icon.-muted { color: var(--color-ink-subtle); }
}
// Icon — adapter API + runtime injection.
//
// Per the answered design question: function-based adapter
// getIcon(name: string, size?: number | string) => string (SVG markup)
//
// Consumers register a pack:
//
// import { registerIconPack } from "@malevich/components";
// import { getIcon } from "@malevich/icons-default";
// registerIconPack(getIcon);
//
// The runtime then resolves any <i class="icon" data-icon="name"> by
// calling the registered getIcon. Multiple packs are supported via
// data-icon-pack on the host element ("default" if not specified).
export type GetIcon = (name: string, size?: number | string) => string | null;
const READY_ATTR = "data-malevich-ready";
const PACK_ATTR = "data-icon-pack";
const NAME_ATTR = "data-icon";
const DEFAULT_PACK = "default";
const packs = new Map<string, GetIcon>();
/**
* Register an icon pack. Subsequent init() / initIcons() calls render
* icons whose data-icon-pack matches the registered name.
*
* Calling without a `name` registers the function as the "default"
* pack — the one used when data-icon-pack is omitted.
*/
export function registerIconPack(getIcon: GetIcon, name: string = DEFAULT_PACK): void {
packs.set(name, getIcon);
}
export function unregisterIconPack(name: string = DEFAULT_PACK): void {
packs.delete(name);
}
export function getIconPack(name: string = DEFAULT_PACK): GetIcon | undefined {
return packs.get(name);
}
function renderIcon(host: HTMLElement): void {
const name = host.getAttribute(NAME_ATTR);
if (!name) return;
const packName = host.getAttribute(PACK_ATTR) ?? DEFAULT_PACK;
const getIcon = packs.get(packName);
if (!getIcon) {
// No pack registered yet. Leave the host empty; CSS reserves the
// sized box so layout doesn't shift when the pack registers later.
return;
}
const svg = getIcon(name);
if (svg) {
host.innerHTML = svg;
// Ensure the inner <svg> inherits color
const innerSvg = host.querySelector("svg");
if (innerSvg && !innerSvg.getAttribute("aria-hidden")) {
innerSvg.setAttribute("aria-hidden", "true");
innerSvg.setAttribute("focusable", "false");
}
}
}
/**
* Initialize a single icon placeholder.
*/
export function initIcon(host: HTMLElement): void {
if (host.getAttribute(READY_ATTR) === "true") {
// Re-render in case data-icon changed after init.
renderIcon(host);
return;
}
renderIcon(host);
host.setAttribute(READY_ATTR, "true");
}
/**
* Initialize every `<i class="icon" data-icon="…">` in a root.
*/
export function initIcons(root: ParentNode = document): void {
for (const host of root.querySelectorAll<HTMLElement>(`.icon[${NAME_ATTR}]`)) {
initIcon(host);
}
}
/**
* Re-render every existing icon. Useful after registering a pack at
* runtime (some icons were waiting because no pack was registered yet).
*/
export function refreshIcons(root: ParentNode = document): void {
for (const host of root.querySelectorAll<HTMLElement>(`.icon[${NAME_ATTR}]`)) {
renderIcon(host);
}
}
# Icon — AGENTS.md
> Auto-generated from `icon.docs.md` + the modifier applicability matrix.
> Edit those source files; this file regenerates on build.
## Identity
- **Component:** `icon`
- **Layer:** foundations
- **Status:** stable
- **Last updated:** 2026-05-19
## Summary
- Any place you need a meaningful visual glyph: button labels,
## Modifier applicability
Per ADR-0007. Modifiers attach via `data-{category}="value"` attributes.
| Category | Accepts |
|---|---|
| `background` | — |
| `surface` | — |
| `effect` | — |
| `shader` | — |
| `rhythm` | — |
| `motion` | any value |
| `behavior` | — |
## Anatomy
```html
<!-- 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]`):
1. Reads `data-icon-pack` (defaults to `"default"`).
2. Looks up the registered `getIcon` for that pack.
3. Calls `getIcon(name)` and injects the returned SVG string.
4. Adds `aria-hidden="true"` and `focusable="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).
## Accessibility contract
- 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 an `aria-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.
## Guidance
### Do
- Use `<i>` as the host element (semantically neutral, short tag).
- Pair `aria-label` on 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
set `color` on the host so currentColor inherits.
- Don't omit `data-icon` — the runtime needs it to know what to render.