Icons
Develi ships 572 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.
Using it
---
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. An illegal name fails astro check rather than rendering an empty box, and your editor autocompletes the full set.
size is sm md lg xl — size-4 through size-8, defaulting to md. Or skip it and pass class="size-6"; the class merges and wins.
Colour comes from currentColor. Every glyph’s geometry uses it, so recolour with any text token — text-primary, text-muted-foreground, text-error. Dark mode is free.
Brand marks are filled silhouettes where the negative space is a path knockout — Facebook’s “f” is a hole, not a white shape — so they flip with the theme too. They render in one tone, not in brand colours. If you need a full-colour brand mark, that is the third-party exception described in Colors: keep the hex on the data entry beside the company that owns it and apply it inline.
Accessibility: decorative by default. The component sets aria-hidden="true" unless you pass a title, in which case it emits role="img" and a <title> element:
<Icon name="menu-01" /> <!-- decorative; the button labels itself -->
<Icon name="search-01" title="Search" /> <!-- standalone; the icon is the label -->
The rule of thumb: if the icon sits inside a button or link that already has an accessible name, leave it decorative. If it stands alone, give it a title.
The registry is two halves
src/components/svg/icons/
├── Icon.astro # the primitive
├── icons.ts # 571 auto-generated — never edit by hand
├── custom.ts # 7 hand-maintained
├── registry.ts # merges the two into one namespace
└── index.ts # re-exports Icon, iconNames, IconName
export const ICON_REGISTRY = { ...ICONS, ...CUSTOM_ICONS } as const;
export type IconName = keyof typeof ICON_REGISTRY;
export const iconNames = Object.keys(ICON_REGISTRY) as IconName[];
The indirection exists because icons.ts is rewritten wholesale by the regeneration pipeline, so anything added to it by hand is lost on the next run. custom.ts is the hand-maintained half, and registry.ts is the single place the two are joined so every consumer sees one namespace and one type.
Additions win on a key collision, which is what you want: it makes a hand-written glyph a deliberate override of a generated one rather than a silent no-op.
That mechanism is load-bearing here. Of the seven entries in custom.ts, six are overrides — facebook, linkedin, github, youtube, pinterest and phone all also exist in icons.ts, and these are the design file’s versions winning on purpose. phone is the clearest case: it is a bare handset, where every generated phone-* decorates it with call arcs or a direction arrow.
Only x is a name the generated set does not have. So the merged total is 571 + 1 = 572.
Count the registry; never quote a comment. Every figure in this part of the tree was wrong at some point: the README said 504,
icons.ts’s own header said 497, and a note insisted the brand batch was never committed when in fact it is inicons.ts. The real number comes fromObject.keys(ICON_REGISTRY).length, and the dev catalog has been rendering the true 572 officonNames.lengththe whole time.Check
iconNamesbefore planning to reuse a glyph, and re-derive rather than trusting any paragraph — including this one.
Finding an icon
Open /examples/ui in dev and scroll to the Icons panel. It renders every name in the registry from iconNames, so it is always current by construction.
The generated set is the Stratis UI Icons line-icon library, imported by category frame: General, Arrows (partial), Media & Devices, Alerts, Security, Images, Files, Charts, Development, Communication, Editor, plus the Social/brand frame. Still to import: Arrows columns 4–5, Finance, and any remaining frames.
Adding one icon by hand
Add it to custom.ts in the same shape as a generated entry:
export const CUSTOM_ICONS = {
"my-glyph": '<path fill="currentColor" d="…"/>',
} as const;
Two rules, and both are easy to get wrong:
The value is the inner markup of a 24×24 viewBox. Icon.astro supplies the root <svg>. Pasting a full <svg> element produces nested SVGs.
Colour goes on the geometry, never on a root element. Icon.astro’s root carries fill="none", which would blank a glyph relying on an inherited fill.
Adding it to custom.ts immediately widens IconName, so it autocompletes and type-checks with nothing else to register.
Regenerating the set
Each category is one frame of the source Figma file. To re-pull one or add another:
- Call the Figma MCP
get_design_contexton the category frame node. One call per frame is usually enough; per-column pulls are the fallback if a frame response is too large. The response is React reference code — an asset-URL table plus one function per icon carryingdata-node-idanddata-name. - Collect
{ id, name, url }intomanifest.json. Cache and dedupe by node id, becausedata-names are not unique in the source. - Run the generator. It downloads each SVG, normalizes colour to
currentColor, flattens bare<g>wrappers, strips ids, scale-to-fits the few off-grid viewBoxes, dedupes by cleaned content against the existing registry, and rewritesicons.ts.
The cleaner asserts on anything unexpected — a <g transform>, a colour it could not normalize, an empty icon — so a bad export fails loudly instead of shipping a broken glyph. It merges idempotently, so re-imports never double-add.
Brand marks export differently and need BRAND=1: each is filled rather than stroked and wrapped in a <defs><clipPath> 24×24 frame, which the cleaner asserts is really just the frame before stripping it. Their data-names are Social/<Brand>/Black, and the file’s own labels are unreliable — one frame labels LinkedIn as “Social Icons”.
Review the generator’s report before committing. The source file mislabels some glyphs: duplicate data-names on -01/-02 variants, a checkmark labelled message-square-plus, icons named Component or -. Fix those with a node-id-keyed name override in the generator, never by hand-editing icons.ts.
A cross-file name clash is reported as a collision and skipped rather than overwritten — rename in the manifest if you want to keep both.
The generator must not emit its own
IconNameoriconNames. It used to, over the 571 inicons.tsalone. Nothing imported them, but two same-named exports one module apart is a trap: an import that autocompleted to./iconsrather than./registrytype-checked cleanly while rejecting"x"and vouching for five brand namescustom.tshas since overridden. If a regeneration restores them, delete them again and fix the generator’s template.
The one performance ceiling
The whole registry is a single module of roughly 450 KB. It stays build-time only — icons inline into HTML and nothing reaches the client — but every icon’s markup is loaded during the build even if a page uses one.
That is fine for a static site and is the deliberate trade. The ceiling to watch: importing the registry inside a client <script> would ship all of it. If you ever need icons on the client, split to per-file .svg imports using Astro’s native SVG components, or build a sprite.
Swapping the icon set entirely
Nothing outside src/components/svg/icons/ knows what the glyphs are — call sites only know names. To replace the set:
- Rewrite
icons.tswith your own name-to-markup map, following the inner-markup andcurrentColorrules. - Fix any call site whose name no longer exists —
astro checklists every one of them for you.
That last point is the payoff of the typed registry. Swapping icon libraries is normally a hunt for silently-missing glyphs; here it is a type error at each call site.