Components · Media and visuals

Icon

A glyph-agnostic wrapper that sets size, tone, and accessibility. Glyphs come from the Material Outlined set; the wrapper is the only place size and colour for an icon should be set.

Documentedby Derek Fidler
sm16 px
md24 px
lg32 px
xl48 px
2xl80 px

Overview

The Icon wrapper lives in @flatpay-dk/ui and accepts any Material Outlined glyph through the asprop. It carries the system's five sizes — 16, 24, 32, 48, 80 px — plus a small set of tones mapped onto the Badge palette so paired-with-text status reads with the same hue without nesting wrappers.

The icon set is curated, not infinite

Adding an icon is a foundations question, not a component one. Pick from the existing library at /library; if it isn't there, append to apps/portal/lib/docs/icons.ts with a category and a couple of synonym keywords. Confirm the glyph reads at 16 — if it's ambiguous there, it doesn't belong.

Sizes

Five steps. md (24) is the default; sm (16) belongs in dense rows and inline status; the larger steps are reserved for empty states, onboarding, and editorial surfaces.

SizePixelsWhere it earns its place
sm16 pxBody, dense rows, button icons, inline status.
md24 pxSection headings, standalone icon buttons, the icon-as-affordance — the default.
lg32 pxFeature tiles, empty-state secondary.
xl48 pxOnboarding hero, marketing surfaces.
2xl80 pxBrand-leading editorial. Pair with a heading; never inline.

Tones

Tones map onto the Badge palette so an icon paired with status copy reads in the same hue. Default sets the icon to text-foreground; subtle drops it to muted-foreground; inherit opts out of any colour — useful inside Buttons and chrome that already paint their own text.

tone="default"Foreground. The right answer 90 % of the time.
tone="subtle"Muted. For meta lines, captions, helper rows.
tone="success"Confirms a positive state — paid, demo-ready, settled.
tone="warning"Pre-empts a problem — building, pending review.
tone="danger"Calls out an error or a destructive action.
tone="info"Contextual or instructive — note, tip, advisory.

In context

Icons earn their place by doing one of three jobs — flagging state, stretching a dense row, or carrying direction. Outside those, the label alone reads better.

Inline status

Payout settled
Awaiting verification
Payment refused

Bring your first prototype in.

Empty-state surfaces earn the bigger sizes — `xl` and `2xl` here, paired with a heading.

Anatomy

The wrapper is a square box; the glyph fills its viewBox.

  1. Wrapper

    Square box at the chosen size (16/24/32/48/80 px). Sets currentColor so the glyph picks up the parent's text colour by default.

  2. Glyph

    Material Outlined SVG, ~2 device-px stroke at 24 px, ~2 px optical padding inside the viewBox. Fills the wrapper exactly.

  3. Accessible name

    Provided via aria-label when the icon is informative. Without it, the wrapper is decorative — aria-hidden="true".

Accessibility

Decorative icons hide from assistive tech automatically; informative icons promote themselves to role="img" when given an aria-label.

Demo-ready

Do

Pair an icon with text. The label does the work; the icon stretches the row.

Don't

Don't ship a stand-alone icon button without aria-label. The screen-reader hears nothing.

Verification pending

Do

Use tone for status icons. Color reinforces; the label still has to explain.

Don't

Don't substitute color for meaning. Status icons need a label; colour-blind users read the words.

Code

Pass an MUI Material Outlined component through as, or pass an SVG as children. The wrapper handles size and ARIA.

Decorative — paired with text

tsx

import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
import { Icon } from "@flatpay-dk/ui";

<span className="inline-flex items-center gap-2 text-sm">
  <Icon as={CheckCircleOutlined} size="sm" tone="success" aria-hidden />
  Demo-ready
</span>;

Informative — stand-alone

tsx

import SearchOutlined from "@mui/icons-material/SearchOutlined";
import { Icon } from "@flatpay-dk/ui";

<button type="button" aria-label="Search">
  <Icon as={SearchOutlined} size="md" />
</button>;

// Or — when the icon itself carries the meaning:
<Icon as={SearchOutlined} size="md" aria-label="Search prototypes" />;

Inline children — custom SVG

tsx

import { Icon } from "@flatpay-dk/ui";

<Icon size="lg" aria-label="Flatpay">
  <svg viewBox="0 0 24 24" fill="currentColor">
    <path d="M5 5h14v14H5z" />
  </svg>
</Icon>;

Best practices

The icon library is curated. Use the wrapper. Default to tone="inherit" inside chrome that already paints colour.

Back

Do

Let the parent paint colour inside Buttons. tone='inherit' picks up the variant's text.

Back

Don't

Don't override tone inside chrome that already sets text — the icon goes off-key.

Do

Reach for sm in dense rows; md is the default; lg+ is reserved for hero surfaces.

Don't

Don't grow icons to compensate for missing text. The label is doing the work, not the glyph.

Props

PropTypeDefaultDescription
asReact.ComponentTypeGlyph component (typically a Material Outlined export). Rendered with aria-hidden.
childrenReactNodeInline glyph — alternative to `as`. Useful for one-off SVGs that don't live in the icon library.
size'sm' | 'md' | 'lg' | 'xl' | '2xl''md'16 / 24 / 32 / 48 / 80 px square box.
tone'default' | 'subtle' | 'success' | 'warning' | 'danger' | 'info' | 'discovery' | 'inherit''inherit'Sets the icon's currentColor. Use `inherit` (default) inside chrome that already sets text — Buttons, Badges, status pills.
aria-labelstringWhen set, the wrapper becomes role="img". Without it, aria-hidden="true" hides the icon from assistive tech.
titlestringBrowser tooltip on hover. Don't lean on it for accessibility — pair with aria-label.
classNamestringForwarded to the wrapper span.