Icons
Grafio ships its own icon system: a single typed <Icon> component over a registry of 571 icons, inlined into HTML at build time. There is no icon package, no runtime fetch, and nothing lands in client JavaScript.
The set is line icons from the Stratis UI collection plus eighteen social and brand marks.
Using an icon
---
import { Icon } from "@components/svg/icons";
---
<Icon name="search-01" />
<Icon name="arrow-right" size="lg" class="text-primary" />
<Icon name="github" title="GitHub" />
Four props:
name— required, typed as a union of all 571 keys. An invalid name failsastro check, and your editor autocompletes the valid ones.size—"sm" | "md" | "lg" | "xl", mapping tosize-4,size-5,size-6,size-8. Defaultmd.title— optional. Its presence changes the accessibility mode; see below.class— merged throughtailwind-merge, plus any native<svg>attribute.
Sizing and colour
Either use the size variant or just pass a size class — class="size-7" wins over the variant, because the merge resolves in the caller’s favour.
All geometry is currentColor, both the strokes on line icons and the fills on brand marks. So recolouring is a text utility:
<Icon name="alert-triangle" class="text-error" />
<Icon name="check" class="text-success size-4" />
Dark mode is free — there are no per-theme icon variants to maintain.
One thing that does not scale independently: line icons carry stroke-width="2" baked into their paths. A large icon has a proportionally thinner-looking stroke, which is usually what you want, but it is not adjustable per instance.
Accessibility
Icons are decorative by default — aria-hidden="true" and no role. That is the right default, because most icons sit beside a text label that already names the control.
Passing title flips it: the component emits role="img" and a <title> child, and drops aria-hidden.
<!-- decorative: the button text already says what it does -->
<Button><Icon name="download" /> Download CV</Button>
<!-- meaningful: the icon IS the label -->
<Button icon><Icon name="search-01" title="Search" /></Button>
Finding an icon name
Four ways, in order of speed:
- Type it.
IconNameis a literal union, so typing<Icon name="lists all 571 in your editor. - Browse the catalog. Run
pnpm devand open/examples/ui— the Icons section renders every glyph with its name underneath and the live count in the heading. - Enumerate them.
import { iconNames } from "@components/svg/icons"gives you the array. - Grep
src/components/svg/icons/icons.tsfor a keyword.
The naming scheme is kebab-case throughout. Numbered variants of one concept use a two-digit suffix (add-square-01 through -04, x-01, volume-05), and semantic modifiers are spelled out (wifi-on, wifi-off, x-circle-contained). Brand marks are plain slugs: github, whatsapp, google-play.
How it works
The registry is a single generated module mapping each name to the inner markup of a 24×24 icon:
export const ICONS = {
activity: '<path d="M4 11.6661H8L…" stroke="currentColor" stroke-width="2" />',
"add-square-01": '<path … />',
} as const;
export type IconName = keyof typeof ICONS;
export const iconNames = Object.keys(ICONS) as IconName[];
Icon.astro supplies the <svg> wrapper and interpolates the markup with set:html, so the paths land in the HTML at build time.
There is a documented ceiling. The registry is one module of roughly 450 KB. That is fine while it stays build-time — icons inline into HTML and nothing reaches client JavaScript — but importing it inside a client <script> would ship the whole thing. If you ever need icons in browser code, the upgrade path is per-file SVG imports or a sprite sheet.
The brand marks
Eighteen filled silhouettes from a separate source: facebook, instagram, tiktok, threads, messenger, whatsapp, telegram, behance, github, discord, linkedin, slack, line, apple, google, pinterest, google-play, bluesky.
They render in one tone, not brand colours — the negative space is a path knockout, so they theme with text-* like everything else.
Note that youtube in the registry is the line-icon version, not the brand mark. The brand file’s version was dropped rather than overwriting the existing name.
Adding your own icons
For one or two icons, the simplest route is to add them to the registry by hand. Take a 24×24 SVG, strip the outer <svg> wrapper, replace every hard-coded colour with currentColor, and add the entry:
export const ICONS = {
// …
"my-icon": '<path d="…" stroke="currentColor" stroke-width="2" stroke-linecap="round" />',
} as const;
The type union updates itself, so <Icon name="my-icon" /> type-checks immediately.
For a bulk import from Figma, src/components/svg/icons/README.md documents the pipeline used to build the shipped set — pull a category frame through the Figma MCP, collect node ids into a manifest, and run a generator that downloads, normalises colour, flattens wrapper groups, strips ids and dedupes by content.
Two caveats if you go that route. The generator script itself is not in the repo — the README is a runbook, not runnable code. And the source Figma file mislabels some glyphs, so the documented practice is to fix names with node-id-keyed overrides in the generator rather than hand-editing the registry, which would be overwritten on the next run.
The logo
src/components/svg/GrafioLogo.astro is the brand mark, deliberately outside the icon registry: the registry is a generated set of 24×24 glyphs on a shared grid, and a logo is a one-off with its own aspect ratio.
It follows the same contract but sizes by height rather than as a square — sm, md, lg map to h-6, h-7.5, h-10, with width following the viewBox. Same currentColor fill, same title-flips-accessibility behaviour.
Replacing it is a single <path> swap plus adjusting the viewBox. It appears twice: in the header at md, and as the oversized ornamental mark in the footer.