Icon

The Icon chrome ships standalone. This page registers a tiny inline demo pack so the placeholders render.

Sizes (currentColor inheritance)

Tones

Inline with text

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.
\n ";