EmIcon
Draws a named glyph from Emerald’s icon set. Icons are addressed by the role they play, not by the picture they show, and are decorative unless you say otherwise.
Usage
name is a role: what the icon is for, not what it depicts. You ask for settings and get the sliders drawing; you ask for mail and get the envelope. There is no file to import, no icon font to load, and no sprite sheet — the glyph is inline SVG drawn from a registry the plugin installs.
The set is deliberately small. 48 drawings answer to 72 names, because 24 of those names are aliases onto a shared drawing. That is a design decision rather than an economy: finance and payments point at the same card because they are the same concept in two dashboards, and giving them separate art would make the product look less coherent, not more.
Anatomy
<script setup lang="ts">
import { EmIcon } from '@paper/emerald'
</script>
<template>
<EmIcon name="calendar" />
</template>Composed on v0
There is no v0 icon component; EmIcon is built on a v0 composable instead. The registry is a createTokens instance, and resolution is createTokens’ own alias dereferencing rather than anything Emerald wrote.
That is what makes the alias table free. createTokens resolves {role} references at read time, so an alias is stored as the literal string {envelope} and dereferenced when it is asked for. mail and envelope are two entries pointing at one array of path data, and adding your own is the same one-line shape.
The plugin is a createPluginContext trinity — createEmeraldIconsContext, createEmeraldIconsPlugin, useEmIcons — which is why namespace exists on the component: a subtree can be given its own registry, and EmIcon will resolve against that one instead. It also carries a fallback, so EmIcon draws correctly in an app with no Emerald plugin installed at all; the built-in set is built once on first use and shared from then on.
Examples
Roles
The canonical set, grouped the way the source groups it.
| Group | Roles |
|---|---|
| Objects | layout sparkle tag help sliders window info login card truck megaphone cart envelope speech-bubble kanban calendar table document layers book receipt |
| People | user users |
| Charts | chart-line chart-bar activity trend-up trend-down |
| Chrome | moon sun palette menu sidebar bell search |
| Direction | chevron-up chevron-down chevron-left chevron-right sort |
| Marks | check minus close plus kebab eye star currency |
Aliases
Product vocabulary pointing at the canonical drawings. Both names are equally valid at the call site; prefer whichever reads correctly in the surface you are building.
| Alias | Resolves to | Alias | Resolves to |
|---|---|---|---|
about | info | logistics | truck |
analytics | chart-bar | mail | envelope |
campaign | megaphone | modals | window |
chat | speech-bubble | orders | layers |
components | layers | payments | card |
contact | speech-bubble | pricing | tag |
contacts | user | productivity | activity |
dashboard | layout | sales | chart-line |
datatable | table | settings | sliders |
ecommerce | cart | signin | login |
faqs | help | features | sparkle |
finance | card | forms | document |
Extending the set
createEmeraldIconsPlugin takes both halves. icons merges glyphs over the built-in set — an existing key replaces its artwork, a new key extends the vocabulary. aliases adds role → target references, and a bare target is wrapped for you, so { expand: 'chevron-down' } and { expand: '{chevron-down}' } mean the same thing.
import { createEmeraldIconsPlugin } from '@paper/emerald'
app.use(createEmeraldIconsPlugin({
icons: { flame: ['M12 2c3 4 6 6 6 10a6 6 0 0 1-12 0c0-4 3-6 6-10Z'] },
aliases: { expand: 'chevron-down', trending: 'trend-up' },
}))A glyph is an array of SVG path d strings, drawn into a 24×24 viewBox with fill="none" and stroke="currentColor". Artwork that assumes a fill will not look right in this set.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
name | EmIconName | — | Required. Role to draw, aliases included |
label | string | — | Accessible name. Set only when the icon is the whole message; promotes the icon from decorative to role="img" |
size | 's' | 'm' | 'l' | 'xl' | 'm' | Step on the --emerald-icon-* scale |
namespace | string | — | Registry to resolve against. Only needed when a subtree was given its own set |
EmIconName accepts any canonical role, any alias, and any string — the last so a set extended at runtime still type-checks. There are no slots.
Accessibility
The component has exactly one accessibility contract, and it is the label prop.
label | Rendered attributes | Reads as |
|---|---|---|
| absent | aria-hidden="true" | Nothing — skipped entirely |
| set | role="img", aria-label="…" | An image with that name |
Decorative is the default because it is the correct answer far more often. An icon that sits beside text, inside a labelled button, or as ornament in a heading adds nothing an assistive-technology user needs, and announcing it makes every one of those surfaces noisier.
Two rules follow from the table:
Never label an icon inside an interactive element. The element itself takes the name — ariaLabel on EmButton, the text content of a link. An icon labelled inside a button produces a control whose name is assembled from both, announced twice.
Never rely on an icon alone to convey state without a label. A red glyph meaning “failed” is invisible to a screen reader and to a colorblind reader both. Either label it, or put the state in text nearby and leave the icon decorative.
An empty string is falsy and therefore decorative, not “labelled with nothing” — label="" renders aria-hidden="true".