Icon
One box, one size, one accessible name. The glyph is passed in as a value, so a page that uses two icons ships two.
import { Icon } from "@dede-style/ui/icon" and the glyph from @dede-style/ui/icons, then <Icon glyph={iconStar} />. There is no name string and no registry to populate — see /icons for the set.
Icon renders one <svg> and nothing around it. No wrapper, no background, no colour prop — everything except the box and the accessible name belongs to whatever contains it. That is what keeps an icon-only button from carrying two boxes, one inside the other, each with its own padding and radius.
It is a server component. No state, no hooks, no handlers — so a page full of icons ships no JavaScript for them.
Size
Seven steps of the element ramp, plus two that are not lengths at all.
- 3 · 12px
- 4 · 16px
- 5 · 20px
- 6 · 24px
- 8 · 32px
- 10 · 40px
- 12 · 48px
Numeric, not t-shirt sizes. The ramp is shared with control heights, and md cannot mean one thing across both — a medium icon is about 20px, a medium control about 40. So the scale holds lengths and the role lives in each component’s own API. Button is where size="md" gets decided, and it will be additive when it comes.
text is 1em — like that one, set in this sentence. It tracks the surrounding type rather than the ramp, so it stays proportional inside a heading, a caption, or body copy, and follows the fluid root down on a narrow screen. It is also the one size that is inline-block rather than block: a block box would break the line it sits in.
fill is 100% of its container — for a glyph used as decoration inside a card or a shape you have already sized. It does nothing on its own: the container has to have a size for there to be anything to fill.
Colour
There is no colour prop. The same glyph, in three contexts that set colour for their own reasons.
A heading, set to primary
A caption, set to secondary
An error message, set to the error colour
fill: currentColor is the whole colour model. A glyph paints with whatever color is in effect where it sits, so a link’s hover, a disabled grey and the mode flip all reach it with no prop and no code. Nothing above had to know an icon was there.
This is why a fill-based icon set was chosen over a stroke-based one: one property does it. Note it is not fill: inherit, which takes the parent’s fill — almost always the initial black — and never tracks text colour at all.
A colour prop would be a second mechanism that then fights the first from inside a Button, where the button already sets a colour for its own state. Background, radius and border are absent for the same reason, plus one more: those together are a combination, and a combination is a component — an icon in a tinted round box is an Avatar.
Beside a label
Sized to the words rather than to the ramp, and coloured by the row it sits in.
- Your changes have been saved
- This session expires in five minutes
- That address is already registered
- Two people are editing this page
Every icon there is decorative, and correctly so: the sentence beside it already says what it means. Announcing them would read “tick Deployed successfully”, which is noise, not help.
They are also carrying no meaning on their own. Colour plus a glyph is two signals, but the words are what makes the row understandable to a reader who cannot distinguish the first two — which is why an icon should never be the only thing saying whether something succeeded.
Your own glyphs
A glyph is a plain object — a list of path strings and, if it is not drawn on a 24 grid, a viewBox. Nothing registers it and nothing has to know about it:
const iconBrand = { paths: ["M12 2 2 22h20z"] } then <Icon glyph={iconBrand} />, and it gets the same box, the same sizes and the same currentColor as everything shipped here.
That is the payoff for passing the glyph as a value rather than a name. A registry would need your icon added to it before you could use it, which means either editing the package or calling a setup function at boot. Here a consuming site keeps the functional set — nobody redraws a chevron — and adds brand marks alongside, with no seam between the two.
Two things to match if you want yours to sit evenly beside the set. Keep the padding in the viewBox: every glyph here is drawn inside roughly 20 units of its 24 box, and that margin is the live area — it is what makes a wide glyph and a tall one read as the same size. Artwork cropped to its own bounding box renders visibly larger than its neighbours. And strip every fill, stroke, width and height attribute from the export: a presentation attribute beats inherited colour, so a stray fill="black" from a design tool will ignore the mode entirely.
Accessibility
Decorative by default. With no label, Icon renders aria-hidden="true" — because the overwhelmingly common case is an icon sitting beside a text label, where announcing it says the same thing twice.
The accessible name belongs to the interactive thing, not the glyph inside it. An icon-only button names itself — <button aria-label="Close"><Icon glyph={iconClose} /></button> — and never <Icon label="Close"> inside one. The two look the same in a screen reader today and diverge the moment anything changes: the name survives the glyph being swapped, removed, or joined by visible text, because it was never attached to the picture.
So label is for the narrower case where the icon is the content and nothing around it says so — a standalone status indicator, a flag in a table cell. It sets role="img" alongside aria-label, because an svg has no implicit role that reliably carries a name.
The rule underneath both: an icon must never be the only thing conveying meaning. That is a requirement for colour-blind and low-vision readers before it is one for screen readers, and it is why the status rows above are sentences with icons rather than icons with tooltips.