Icon composition

Composed variants

default / dark / context-colored variants and the WCAG-inspired innerColor rule.

Composed variants

Every element has two universal variants — default and dark. Categories that declare a context also expose one colored variant per context value.

The variant union

The compositor accepts three variant forms:

type ComposeVariant =
  | 'default'
  | 'dark'
  | { kind: 'colored'; color: string }  // color is #RRGGBB

Each resolves to three colors: the shape fill, the shape stroke, and the inner (symbol) color.

VariantShape fillShape strokeInner color
defaultnone (transparent)#000#000
dark#000#000#FFF
{kind:'colored',color}colorcolorinnerColorForShape(color)

Why the split exists

default and dark are universal — every element renders equally well outlined (light UI) or filled (dark UI). Context-colored variants exist because each category can declare a context.values[] set whose members carry a color. An element can therefore render in any context value its category declares.

This keeps the color palette scoped to where it semantically belongs: a category's context, not the element itself.

Discovering valid variants

The build step materializes an icon_variants[] array onto every element. Read it via get_element or GET /schemas/:version/elements/:element_id:

{
  "id": "purpose.example",
  "icon_variants": ["default", "dark", "commercial", "civic"]
}

get_icon_url mirrors this array in its response for convenience.

The innerColor rule

For colored variants, the inner color is derived from the shape color via WCAG 2 relative luminance:

innerColor = relativeLuminance(shapeColor) >= 0.179 ? '#000' : '#FFF'

The 0.179 threshold matches the common "choose black or white text on a background" heuristic.

Worked example

Take a category with context.values[{id:'commercial', color:'#0052CC'}]:

shapeColor    = #0052CC
rgb           = (0, 82, 204)
linearized    = (0.000, 0.0865, 0.6105)
luminance     = 0.2126·0.000 + 0.7152·0.0865 + 0.0722·0.6105
              = 0.106
0.106 < 0.179 → innerColor = #FFF

The commercial variant of an element in this category therefore renders with a #0052CC shape and white inner glyph.

Take a lighter context color, #FFDD00 (DTPR warning yellow):

luminance  ≈ 0.81
0.81 >= 0.179 → innerColor = #000

The rule deliberately biases toward high-contrast symbol legibility at small sizes.

Output SVG

A composed icon is an inline-compact 36×36 SVG:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="36" height="36">
  <path d="…shape path…" fill="…" stroke="…" stroke-width="2"/>
  <g color="…innerColor…">
    …stripped symbol inner…
  </g>
</svg>

Identical inputs produce byte-identical output — no randomness, no timestamps, no environment-dependent behavior.

See also

Copyright © 2026