Icon
Status: stableAccessibility reviewedtenet-ui@0.4.0
import { Icon } from 'tenet-ui';Also accepts every prop of Omit<SVGAttributes<SVGSVGElement>, 'children' | 'width' | 'height' | 'viewBox'>.
Source: src/components/Icon/Icon.tsx
Usage guidelines
Section titled “Usage guidelines”When to use: to reinforce an action, status or object next to text, or as the only content of an
IconButton. Icons are drawn from the curated tenet set (Lucide, ISC) exported from
tenet-ui/icons; generated/icons.json is the searchable manifest.
- 16 (default) — inside controls:
Buttonleading/trailing icons,IconButton, input adornments, table-row actions, tag/label prefixes. Sits on the 14px body text line. - 20 — large controls (
size="large"buttons), list rows and navigation items with 16px text. - 24 — empty states, page/section headers, stat tiles, dialog titles. Never larger: for illustrations use an image, not a stroked icon.
- Do not set
width/height/viewBoxyourself;sizeowns geometry so every icon in a row lines up. Stroke width is a constant 2 across sizes (Lucide’s native weight).
Decorative vs meaningful — the label rule
Section titled “Decorative vs meaningful — the label rule”- Default is decorative. An icon renders
aria-hidden="true". This is right whenever the meaning is already in adjacent text or in the control’s name:\<Button leadingIcon=\{\<PlusIcon />\}>New article\</Button>, a status word with a check next to it, a chevron on a disclosure. - Pass
labelonly when the icon stands alone. A lone glyph in a table cell that carries information no text does (<PaperclipIcon label="Has attachments" />), a sole indicator in a header. A status is never a lone icon: it is a word in aTag(<Tag variant="success">Published</Tag>).labelsetsrole="img"+aria-label, so screen readers announce it. Name the meaning, not the glyph:label="Published", notlabel="Check circle". - Never give an icon a
labelthat duplicates visible text beside it — the name is read twice.
Pairing with IconButton and Button
Section titled “Pairing with IconButton and Button”- In
IconButtonthe accessible name goes on the button (aria-label="Delete row"); the icon stays decorative.IconButtonalready wraps the icon in anaria-hiddenspan, so do not add alabelto the glyph — the button would then have two competing names. Button’sleadingIcon/trailingIconslots arearia-hiddentoo; pass bare glyphs (<ChevronDownIcon />) and let the label text speak.- Icon color is
currentColor: it inherits the button/text token automatically. Do not setcoloron the icon; set it on the parent (or pick a token variant of the parent).
Naming and imports
Section titled “Naming and imports”- Every glyph is
<Thing>Iconfromtenet-ui/icons:import \{ SearchIcon, MoreHorizontalIcon \} from 'tenet-ui/icons'. The tenet name is ingenerated/icons.json(search→SearchIcon); the Lucide source name may differ (more-horizontalis Lucide’sellipsis). - Never inline arbitrary SVG when an icon exists in the set. Search
generated/icons.jsonby keyword first. If a glyph is genuinely missing, add it toicons/manifest.jsonand runnode scripts/build-icons.mjs; as a last resort wrap a one-off path in<Icon>so it still gets the system’s size, stroke and a11y defaults. - Do not add a runtime dependency on
lucide-reactor another icon package in product code; the tenet set is the vocabulary the docs, catalog and MCP tools know about.
Do / Don’t
Section titled “Do / Don’t”- ✅
<Button leadingIcon={<PlusIcon />}>New article</Button>— decorative icon, text carries meaning. - ✅
<IconButton icon={<TrashIcon />} variant="danger" aria-label="Delete row" />— name on the button. - ✅
<PaperclipIcon label="Has attachments" size={16} />alone in a table cell — meaningful, labelled. - ❌
<CheckCircleIcon label="Published" size={16} />alone in a status cell — a status is aTagword, not a glyph. - ❌
<svg width="16" height="16"><path d="M…" /></svg>— inline SVG for a glyph that exists (SearchIcon). - ❌
<IconButton icon={<TrashIcon label="Delete" />} />— name on the icon, button still nameless. - ❌
<SearchIcon style={{ width: 18, color: '#c2410c' }} />— off-scale size and a hardcoded color; usesizeand inherit the parent’s token.
See also: IconButton for an icon-only action, Button leadingIcon when a label fits, Tag for status words; system guidelines iconography (sizes, decorative vs meaningful, naming) and accessibility.
Examples
Section titled “Examples”6 stories for Icon — open in Storybook
- Default
components-icon--default - Sizes
components-icon--sizes - Labeled
components-icon--labeled - Gallery
components-icon--gallery - In Button
components-icon--in-button - In Icon Button
components-icon--in-icon-button
The full matrix (all args, controls, accessibility panel) is in Storybook.
| Prop | Type | Default | Description |
|---|---|---|---|
size | 16 | 20 | 24IconSize | 16 | Rendered width and height in pixels. 16 sits on the text line inside controls (Button, IconButton, inputs); 20 for large controls and list rows; 24 for empty states, page headers and stat tiles. |
label | string | — | Accessible name. Set it ONLY when the icon carries meaning on its own (no adjacent text names it). When set the SVG gets role="img" and aria-label; when absent the icon is decorative (aria-hidden="true") and the surrounding control must carry the name. |
children * | ReactNode | — | The glyph: SVG shape nodes (, , …) drawn on Lucide's 24×24 grid. Consumers normally render a named glyph from tenet-ui/icons () which supplies this. |
* required