Skip to content
AstroCraft Docs
On this theme

Icons

Olsa ships its own icon system: a single typed <Icon> component over a generated 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 registry has two sources, both Figma Community files. The bulk are Stratis UI Icons line icons across eleven category frames — General, Arrows (partial), Media & Devices, Alerts, Security, Images, Files, Charts, Development, Communication and Editor. On top sits a filled Social/brand set: facebook, instagram, tiktok, threads, messenger, whatsapp, telegram, behance, github, discord, linkedin, slack, line, apple, google, pinterest, google-play, bluesky and youtube.

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" />

Four things to know:

name is typed. It is the generated IconName union, so an illegal name fails astro check and every icon autocompletes in your editor. This is the main practical benefit over a string-keyed sprite — you cannot ship a blank square.

size is sm | md | lg | xl, mapping to size-4 through size-8, defaulting to md. Or skip the variant and pass class="size-6" — the merge rule means your class wins.

Colour is currentColor. Every path’s geometry inherits the text colour, so you recolour with any token utility: text-primary, text-muted-foreground, text-error. Dark mode is free. The brand marks are filled silhouettes with path-knockout negative space — Facebook’s “f” is a hole, not a white shape — so they theme the same way and render in one tone rather than in brand colours.

Decorative by default. The component sets aria-hidden, which is correct for the overwhelming majority of uses where an icon sits beside a text label. Passing title flips it to role="img" with a <title> child, giving it an accessible name for the cases where the icon is the label.

It follows the primitive contract

Icon.astro is a primitive in every sense — it just lives under svg/ rather than ui/ because it is generated rather than hand-written. It exports its tv() config as icon, types props as native svg attributes plus variant props, carries data-slot="icon", and merges the consumer class. ESLint’s frontmatter-export allowance globs src/components/svg/**/*.astro alongside ui/** for exactly this reason.

The barrel at src/components/svg/icons/index.ts exports Icon, IconVariants, iconNames and the IconName type. iconNames is the array the catalog iterates.

Build-time only — and the ceiling

icons.ts maps each name to the inner SVG markup, and Icon.astro inlines it with <Fragment set:html>. Everything happens at build. No icon bytes reach client JavaScript, no sprite is fetched, and an unused icon costs nothing in the output HTML.

The registry does carry a stated ceiling, marked ponytail: in its README: the whole thing is one module of roughly 450 KB. That is fine at build time, where only the icons a page actually uses end up in its HTML. Importing it from a client <script> would ship all 450 KB. If you ever need icons at runtime, that is the point to split to per-file .svg imports (Astro’s native SVG components) or a sprite — not to keep the single module and hope.

Finding an icon

Open /examples/ui in dev and go to the Icons section. It iterates iconNames, renders every glyph with its name, and shows the live count. That is faster than grepping, and it is the only way to judge whether a glyph reads at the size you need it.

If you are searching by concept rather than by name, remember the registry is auto-generated from a design file and its naming reflects that source. Numbered variants (add-square-01 through -04) are visual alternates of the same idea. Some obvious names simply are not there — there is no gear or people glyph, and no Twitter/X mark — which the shipped code works around visibly: navData.social uses github where the design showed an X mark, and /features/ maps its “settings” card to audio-settings-01 and its “users” card to annotation-check, both with a comment saying why and what would fix it.

That is the honest state of a generated registry, and the fix is to import more frames rather than to hand-edit.

Regenerating and extending

icons.ts is auto-generated and says so on its first line. Do not hand-edit it. The pipeline is documented in src/components/svg/icons/README.md, and its shape is:

  1. Call the Figma MCP get_design_context on a category frame node — one call per frame. The response is React reference code with an asset-URL table and one function per icon carrying data-node-id and data-name.
  2. Collect { id, name, url } into a manifest.json, deduping by node id — the source’s data-name values are not unique.
  3. Run the generator, which downloads each SVG, normalises colour to currentColor, flattens bare <g> wrappers, strips ids, scale-fits the few off-grid viewBoxes, dedupes by cleaned content, and rewrites icons.ts.

The generator and its cleaner live in the scratchpad that built the set rather than in the repository, so the README is the runbook rather than a pnpm script. The cleaner asserts on anything unexpected — a <g transform>, a colour it could not normalise, an empty icon — so a bad export fails loudly instead of shipping a broken glyph.

Brand marks export differently: filled rather than stroked, wrapped in a <defs><clipPath> frame. They run through the same generator with BRAND=1, which asserts the clip really is just the frame before stripping it. Their data-name values are unreliable in the source file — “Social Icons” is LinkedIn, “Telegram (Only sign)” is Telegram — so the manifest carries the corrected slug.

Two things to review before committing a regeneration. The Stratis file mislabels some glyphs (duplicate names on -01/-02 variants, a checkmark labelled message-square-plus, icons named Component or -); fix those with a node-id-keyed override in the generator, never by editing icons.ts. And a cross-file name clash is reported as a collision and skipped, not overwritten — rename in the manifest if you want to keep both.

Still to import from the source file: the remaining Arrows columns and the Finance frame.

Replacing the system entirely

If you would rather use an icon package, the surface to replace is small: swap the <Icon> import in each consumer and delete src/components/svg/icons/. Around twenty-five files import it. What you lose is the typed name union and the guarantee that nothing reaches client JavaScript; what you gain is a maintained upstream. The theme’s own position is that a 450 KB build-time registry with autocomplete beats a runtime dependency, but it is a file in your project either way.

NEXT STEPMotion & Animation