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.
| Size | Pixels | Where it earns its place |
|---|---|---|
| sm | 16 px | Body, dense rows, button icons, inline status. |
| md | 24 px | Section headings, standalone icon buttons, the icon-as-affordance — the default. |
| lg | 32 px | Feature tiles, empty-state secondary. |
| xl | 48 px | Onboarding hero, marketing surfaces. |
| 2xl | 80 px | Brand-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.
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
Affordance
Previous reportNext reportIcons sit on the side of the action they imply — back-arrows lead, forward-arrows trail.
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.
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.
Glyph
Material Outlined SVG, ~2 device-px stroke at 24 px, ~2 px optical padding inside the viewBox. Fills the wrapper exactly.
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.
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.
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.
Do
Let the parent paint colour inside Buttons. tone='inherit' picks up the variant's text.
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
| Prop | Type | Default | Description |
|---|---|---|---|
| as | React.ComponentType | — | Glyph component (typically a Material Outlined export). Rendered with aria-hidden. |
| children | ReactNode | — | Inline 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-label | string | — | When set, the wrapper becomes role="img". Without it, aria-hidden="true" hides the icon from assistive tech. |
| title | string | — | Browser tooltip on hover. Don't lean on it for accessibility — pair with aria-label. |
| className | string | — | Forwarded to the wrapper span. |