Icons
Urengi ships 576 icons behind one typed component, with no icon package as a dependency. Every glyph is inline SVG markup in a TypeScript module, resolved at build time — icons inline into the HTML and nothing lands in client JavaScript.
Do not copy that number into your own prose. Read it off iconNames.length, which is what the dev catalog prints; a count written down is a count that goes stale.
Using one
---
import { Icon } from "@components/svg/icons";
---
<Icon name="activity" />
<Icon name="trash-01" size="lg" class="text-error" />
<Icon name="search-01" title="Search" />
name is a typed IconName, so an illegal name fails astro check and your editor autocompletes the whole registry. This is the single most useful property of the system: there is no such thing as a typo’d icon that renders as a blank box.
size is sm md lg xl — size-4 through size-8, defaulting to md. Or pass class="size-6" and skip the variant.
Colour is currentColor throughout, so any text-* token recolours a glyph and dark mode is free. The brand marks are filled silhouettes — the negative space is a path knockout — so they flip with the theme too, rendering in one tone rather than in brand colours.
Accessibility: decorative by default (aria-hidden="true"). Pass title to expose an accessible name, and the component switches to role="img" with a <title> child. An icon that carries meaning on its own — an icon-only button, a status glyph — needs one; an icon beside a text label does not, and giving it one makes a screen reader say the same thing twice.
The primitive follows the same contract as everything in ui/: data-slot="icon", an exported tv() config, native svg props plus variant props, merged class, tokens only.
The three-file registry
src/components/svg/icons/
├── icons.ts # AUTO-GENERATED — 571 icons ported from Figma. Do not hand-edit.
├── custom.ts # hand-maintained additions — 5 today
├── registry.ts # merges the two into ONE map and ONE IconName union
├── Icon.astro # the primitive
└── index.ts # the barrel
export const ICONS = { ...GENERATED_ICONS, ...CUSTOM_ICONS } as const;
export type IconName = keyof typeof ICONS;
export const iconNames = Object.keys(ICONS) as IconName[];
The split exists because icons.ts is regenerated wholesale from its Figma source, so anything written there by hand is lost on the next run. Glyphs the source frames do not carry live in custom.ts instead.
registry.ts is the only module Icon.astro and the barrel import from, and neither half re-exports its own IconName — that would be a second, smaller name universe one import away from the right one.
Collision rule: additions win. The spread order above is the rule, so a hand-written glyph with a generated name is a deliberate override rather than a silent no-op. Do not reorder it.
The five custom icons today are x-twitter, history, currency-dollar, users and clipboard-list, and their comments are worth reading before you add a sixth. Two of them exist because the generated set has a near name that draws a different idea: arrow-rotate-left-01 is the nearest thing to a clock and it draws an upload tray; the three bar-group-* names are the only “group” names in the set and all three draw bar charts. A wrong-but-valid icon name is invisible to astro check, so it costs a browser pass to find — which is the argument for opening the catalog after you pick names.
Exports at other sizes carry a <g transform="scale(…)"> wrapper so they sit correctly in the registry’s 24×24 box.
Where a wrong name is caught
Three places, in order of how early they fire.
In the content schema. The integrations collection validates capabilities.cards[].icon against the live registry with z.enum(ICON_NAMES). That placement is the whole point: a bare z.string() widens the name back to any string, ICONS[name] for an unknown one is undefined, and set:html then draws nothing — the card ships with a silent empty 24×24 hole that the build, the types and astro check are all perfectly happy with. As an enum it is a build failure naming the offending entry file, and the section that draws it needs neither a guard nor a cast.
In astro check. Any <Icon name="…" /> in markup is typed.
In the browser. For the near-name case above, which nothing else can see.
One import detail matters if you ever reach for the registry outside a component: src/content.config.ts imports svg/icons/registry, not the svg/icons barrel. The barrel re-exports Icon.astro, and pulling a component into the content config would drag the Astro renderer into a module the content layer loads on its own.
What is in the set
The bulk are Stratis UI Icons line icons, ported by category frame — General, Arrows (partial), Media & Devices, Alerts, Security, Images, Files, Charts, Development, Communication, Editor. On top is a filled social and brand set from a second Figma Community file: facebook, instagram, tiktok, threads, messenger, whatsapp, telegram, behance, github, discord, linkedin, slack, line, apple, google, pinterest, google-play, bluesky.
Read THIRD-PARTY.md before you ship. Figma Community publishers choose their own licence per file, and neither of these two has been verified. The platform default is CC BY 4.0, which would mean keeping an attribution line; a publisher may instead have set CC0, a custom licence, or a non-commercial one — in which case those icons cannot ship in a commercial product and the set has to be replaced. That section is marked confirm before production for a reason.
Adding an icon by hand
Add it to custom.ts:
export const CUSTOM_ICONS = {
"my-glyph": '<path d="…" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>',
};
Same contract as the generated file: the value is the inner markup of a 24×24 icon, drawn with currentColor so it recolours with text-* and resizes with size-*. If your export is at another size, wrap it in <g transform="scale(24/N)">.
The name is available immediately, everywhere, with autocomplete — because IconName is derived from the merged map rather than declared.
Regenerating from Figma
icons.ts is rebuilt wholesale rather than appended to. The runbook lives in src/components/svg/icons/README.md; in outline, you pull one category frame at a time through the Figma MCP, collect { id, name, url } into a manifest deduped by node id (the display names are not unique in the source), and run the generator, which downloads each SVG, normalises colour to currentColor, flattens bare <g> wrappers, strips ids, scale-fits the off-grid viewBoxes, dedupes by cleaned content and rewrites the file.
Anything you added by hand survives, because it was never in that file.
The one ceiling
The registry is a single module of roughly 450 KB. It stays build-time — icons inline into HTML and nothing reaches the client — but every icon’s markup is loaded at build even if a page uses one.
That is fine as long as it stays server-side. Importing the registry inside a client <script> would ship all of it. If you ever need icons on the client, the upgrade path is per-file .svg imports through Astro’s native SVG components, or an SVG sprite.
Finding one
Open /examples/ui in dev. The icons panel renders the entire registry with names, and it prints the live count off iconNames.length. It is faster than grepping, and it is the only way to catch the near-name problem before a browser pass does.