Skip to content

Iconography

In review

Icons are stroke-first, monochrome, and token-colored. There is one home for the SVG shell (the NockerlIcon primitive), so every glyph shares the same viewBox, stroke, caps, and currentColor inheritance, and a system-wide change is one edit, not a sweep through hand-rolled <svg> tags. An icon is a shape, never a halo: it carries meaning through silhouette and (where paired) a status color, never through a glow or a colored shadow (see Design laws §1).

The utility set is Lucide geometry, an even-stroke, rounded, open-source line set drawn on a 24-unit grid (viewBox="0 0 24 24"). It is a deliberate choice: Lucide’s weight and corner treatment match the thin-forward brand (geometric, unfussy, no filled blobs), and its glyphs line up 1:1 with Material Symbols (Android) and SF Symbols (Swift), so the same concept resolves to a near-identical silhouette on every platform.

NockerlIcon ships a small registry of the glyphs that were being re-typed across the app (chevrons, check, x, plus, minus) behind a name prop; any other glyph is passed as SVG path children on the same 24-grid. The registry is intentionally tiny: it is a convenience for the handful of universal control glyphs, not a bundled icon font. Product surfaces bring their own glyphs; the framework owns the shell and the rules, not an exhaustive library.

Concern Rule
Grid viewBox="0 0 24 24" (every glyph is drawn to the same 24-unit box)
Geometry Lucide line set (rounded, even-weight); custom glyphs match its construction
Fill fill: none, outline by default (see Outline vs filled)
Color stroke: currentColor. The icon inherits its host’s text color; never a hardcoded hue
Registry NockerlIcon name="…" for the universal control glyphs; path children for the rest

Icon sizes are tokens, not literals: a six-step ramp from 12 to 24, mirrored in code as ICON_SIZE and in CSS as --icon-*. Omit the size and the surrounding CSS sizes the glyph (the dominant pattern, so an icon tracks its text); pass a scale key when a glyph needs a fixed box.

Token Value Typical use
--icon-xs 12px Dense inline marks, tiny meta rows, delta glyphs
--icon-sm 14px Chip / badge glyphs, compact captions, inline note checks
--icon-md 16px The default control glyph: buttons, fields, menu rows
--icon-lg 18px List-row leading marks, tool-call tiles, section affordances
--icon-xl 20px Icon-buttons, toolbar actions, dialog header coins
--icon-2xl 24px Empty-state and hero glyphs, the largest inline icon

The ramp is even and small on purpose: icons support text, they do not shout. Anything larger than 24 is an illustration, not an icon (see below).

Every glyph obeys the same drawing contract, encoded once in the primitive:

  • Stroke weight 2 on the 24-grid (stroke-width: 2), scaling with the box. A heavier 2.4 is used only for a few emphasis marks (a bold check); nothing goes thinner than the type it sits beside would imply.
  • Round caps and joins (stroke-linecap: round, stroke-linejoin: round), the soft terminal that matches Outfit’s geometry and the pill/soft-corner language.
  • currentColor stroke, none fill: the icon is a silhouette that takes its host’s ink. Status color, when an icon carries it, comes from a semantic token on the host, never a literal.
  • No glow, ever. Depth and attention come from shape, weight, and (for status) a filled disc, never a blurred or colored shadow behind the glyph (Design laws §1 / §6).

The default is outline (stroke on transparent). A glyph goes filled in exactly one situation: as a knockout, a solid shape cut out of a colored surface. That is the alert family’s grammar (Design laws §6): severity and categorical family ride in a filled status / family disc or tile, with the glyph knocked out to the canvas ink. So the same check is an outline stroke inline, and a knockout inside a success disc. One geometry, two treatments, chosen by the surface, not by a separate “filled” icon set. We do not ship a parallel filled icon library; filled is a compositing decision, not a glyph variant.

Ratified canon (Design laws → Ratified component canon): an icon’s treatment tells you whether it is interactive, before any hover or focus.

  • Interactive: a flat / plain glyph (a control’s own icon) or a filled-circle-with-height (an icon button / FAB coin). Both sit up and read as pressable.
  • Informational: the inset (recessed-well) icon treatment, e.g. the EmptyState glyph. It sits down and is never interactive: an inset element carries no click handler. If a spot needs to be pressable, it uses a pressable treatment, never an inset that also clicks.

This pairs with the fields-sink / lift laws (§2): pressable things lift, informational insets sink, so pressability is legible from depth alone (never color or hover alone; see §13).

Icons follow the standing posture of unify brand expression, honor platform behavior (Design laws §12):

  • Web: NockerlIcon renders the SVG shell (Lucide geometry, currentColor, the --icon-* ramp). Utility glyphs and illustrations are both SVG here.
  • Android: utility glyphs are Material Symbols (outlined, weight matched to the ramp); empty-state illustrations are the shared Nockerl stroke-SVG set (imported as vectors).
  • Swift: utility glyphs are SF Symbols (matched to the ramp via .imageScale / point size); empty-state illustrations are the shared Nockerl stroke-SVG set.

The stroke, sizing intent, and currentColor behavior are the unified part; the concrete glyph source for utility icons is the honored platform delta.

The sizing ramp is published in @dizyx/nockerl-tokens as --icon-xs … --icon-2xl (authored in tokens/core/dimension.json), and mirrored in @dizyx/nockerl-react as the ICON_SIZE scale on the NockerlIcon primitive. Consume the tokens. An icon size, like every other value, is never hardcoded in a client or a demo.