Back to home
Prelude DS Icons preview

Drawing the icon in Figma is just the beginning

Prelude DS Icons. From a 10.9 KB zero-dependency icon gallery to a pnpm monorepo that generates four framework-native packages from a single set of SVGs.

Design EngineerPersonal project2026

A library nobody can find isn't a library, it's a folder

I've been building the iconography for a design system that runs in production, and that's where I learned where the real bottleneck is. It's not in the drawing. It's in everything that happens after.

An icon truly exists when someone who didn't draw it can find it by name or category, see it at the exact size they'll use it, inspect its code, copy it, and drop it into their screen without opening Figma, without installing anything, and without asking the design person. Until that point, the library is a directory of files with a nice name.

The options on the market always failed in the same place. They were either tied to a framework, or bundled inside a full UI library dragging its own visual identity, or they were pretty galleries with no SVG inspection or real export. None of them solved the distribution problem, which is the only one that matters when the team consuming the icons isn't the team producing them. And a gallery alone doesn't solve it either, because the team that builds in React needs React components, the one in Angular needs Angular components, and the one that just wants a custom element needs that too.

That's where Prelude comes from. It started as the distribution infrastructure for an icon set. Then the infrastructure learned to speak every framework.

Zero dependencies isn't a technical constraint, it's the thesis

The decision that orders the entire project is that an icon catalog is infrastructure, and infrastructure has to survive the framework decisions of the team that uses it.

If the catalog needs React to open, it dies the day the team migrates. If it needs an npm install of 40 KB of runtime to render a grid of SVGs and open a modal, it's paying a toll for something that document.createElement and <dialog> solve natively. The entire application logic fits in 255 lines of JavaScript, and that number isn't an optimization anecdote: it's proof that the problem didn't require a framework.

The result is 10.9 KB of minified JS and CSS for the entire catalog, and zero production dependencies. Vite stays in the development layer, where it provides HMR and a build with hashed assets, without leaking into what the end user downloads.

Dropping a file is the entire API

The icon registry isn't maintained by hand. A Node script traverses /public/icons/, extracts name, category, tags, and sizes from each SVG, and generates icons.json. In development, chokidar watches the folder and regenerates the manifest on the fly. The reasoning is the same one I apply to any design system: any process that forces someone to register the same data in two places ends up out of sync. Always. The manifest is derived data, not maintained data. Adding an icon to the catalog is a single-step action, and that's the condition for the library to grow without me being the intermediary.

Normalize on consumption, not at the source

Each SVG is processed right before rendering: width and height attributes are stripped so it scales with context, and aria-hidden="true" and focusable="false" are injected so it doesn't announce to screen readers or enter the tab order. What matters is where this happens. Normalization runs at load time and the source files stay untouched. The SVG in the repository remains the pure design artifact, exportable to an icon font, a package, or back to Figma without carrying decisions made for a specific website. That's the difference between a source of truth and a source already contaminated by its first consumer.

Tokens here too

Spacing, typography, color, radii, shadows, and blurs live as custom properties on :root. Components consume tokens, never literal values. Changing the theme or adding dark mode means overriding properties, not rewriting stylesheets. It's exactly the same principle I apply in a product design system, applied to a tool with 794 lines of CSS, because scale doesn't change the argument.

The details you don't see in a screenshot

Each icon's detail view opens in a native <dialog>, which already brings modal behavior, backdrop, and Escape-to-close without any library. On top of that, focus enters the modal on open and returns to the trigger element on close, which is the part almost no custom implementation gets right.

The SVG code syntax highlighting doesn't use a library either. It's a regex-based tokenizer that wraps tags, attributes, and values in <span> elements with CSS classes. It saves an entire highlighting package for the single language this application needs to display.

The copy button uses the Clipboard API with a fallback to execCommand for older browsers, because a design system tool ends up being opened by someone in whatever corporate browser they've been given, not the one you have.

And all transitions respect prefers-reduced-motion. Accessibility here isn't an audit I passed at the end: it's semantic HTML (<dialog>, <nav>, <aside>, <section>), ARIA labels on every interactive element, full keyboard navigation, and a skip link, decided at the same moment each component was written.

From gallery to monorepo: one source, four frameworks

The gallery proved the thesis: a zero-dependency catalog can distribute icons without forcing a framework. The next question was inevitable. What if the teams that do use a framework could consume the same icons as native components, without adapting anything?

The answer is a pnpm monorepo with a code generation engine at its center. One TypeScript script reads the source SVGs, optimizes them through SVGO with custom plugins, and writes four complete packages: raw SVG with TypeScript metadata, Web Components with Shadow DOM, React components with forwardRef and context provider, and Angular standalone components with dependency injection.

The gallery app still exists inside the monorepo and still weighs 10.9 KB. But now it's one consumer among five. The same source SVGs that feed the gallery also feed every framework package, and adding a new icon to all five outputs is still one step: drop the file.

One script writes four packages

generate-icons.mts is the heart of the system. It reads each SVG from the source folder, runs it through an SVGO pipeline that strips dimensions, converts fills to currentColor, and normalizes attributes. Then it generates one TypeScript file per icon in each package: a metadata export for @prelude-icons/svg, a Custom Element class with Shadow DOM for /web, a forwardRef component for /react, and a standalone component with DomSanitizer for /angular. Every naming convention, every accessibility attribute, and every export shape is decided in one place. If the API changes, one file changes.

Each framework gets a native API

The React package gives you CalendarIcon as a forwardRef SVG component, or a generic PiIcon with an IconProvider context for dynamic rendering. The Angular package gives you standalone components like CalendarIconComponent with @Input bindings and DomSanitizer, plus a PiIconComponent with an injection token for the registry. The Web Components package gives you pi-icon-calendar as a Custom Element with observed attributes for size, color, and label. Consumers don't adapt to the library. The library adapts to them.

The gallery is now one consumer among five

The vanilla gallery that started the project hasn't changed. It still runs on zero production dependencies, still weighs 10.9 KB, and still updates by dropping a file. The difference is that the same SVG source that feeds the gallery now also feeds four npm packages with dual ESM and CJS exports, TypeScript declarations, and tree-shakeable imports. The gallery didn't get replaced. It got company.

Measurable Impact

10.9 KB

total catalog weight in production, minified JS and CSS

0

production dependencies, with only 4 in development

4

framework packages generated from a single set of source SVGs

1 script

generates all four packages: SVG, Web Components, React, and Angular

What remains

The gallery is shipped. The monorepo and the four packages are shipped. Icon coverage keeps growing by category and every new addition inherits optimization, multi-framework codegen, and accessibility for free. What's left is tightening the two ends of the pipeline that are still manual.

First, automated vector quality control in CI: checking that each icon respects the grid, stroke weight, and optical size before entering the catalog, so consistency stops depending on my eye. Then, connection with Figma variables, so the name decided in design is the same one that reaches code, without manual translation.

The goal isn't to have more icons. It's to make adding icon number two hundred cost exactly the same as adding number six.