Learn How to Build an Angular UI Kit From a Static Style Guide
AI links open with a title + excerpt (these tools can't fetch the page themselves) — use "Copy full article" to paste the complete text for a fuller summary.
Introduction
This article demonstrates how we turned a static HTML style guide into a working Angular UI kit.
The style guide came first — one file, design-system-light.html, with its own inline CSS, showing every button size, badge, input, card, alert, toast and avatar on a single page. No build step, no framework. Open it in a browser and you see the whole design system.
Then we had to get it into an Angular app without losing that.
Keep the Style Guide as a Reference
First decision, and the one that keeps this maintainable: the HTML file is not loaded by the app.
It is not imported, not linked, not built. It is a reference document that happens to be executable. Its inline CSS is deliberately separate from the app's CSS.
That sounds wasteful — the same button styles in two places. It is not, for one reason: a reference you can open instantly and compare against is worth far more than one less duplicate. The moment you wire the style guide into your build, it becomes production code, and it stops being a place you can freely experiment.
Features of this approach
- One page shows every component in every state.
- Designers and developers argue against something real.
- The app has no dependency on it, so it cannot break the build.
With the following steps, you can port your own.
- Split the components into two groups.
- Port the presentational ones into CSS.
- Wrap the stateful ones as Angular components.
- Accept that not everything gets used at once.
Split the Components Into Two Groups
This is the whole design, and it is one question per component:
Does it need to remember anything?
A button does not. It looks different on hover and when disabled, and CSS handles both. A toggle switch does — something must know whether it is on.
That question sorts every component in the style guide into one of two piles.
| Needs state | Presentational only |
|---|---|
| Toggle, checkbox, radio | Buttons, badges, tags |
| Tabs | Inputs, selects, textareas |
| Toasts | Cards, avatars, alerts, progress bars, tables |
The presentational pile is much bigger, which is the point — most of a design system is not components at all. It is CSS.
Port the Presentational Ones Into CSS
These go into frontend/src/styles.css, in a Tailwind @layer components block, as plain CSS reading from the --tk-* design tokens.
.btn { /* base */ }
.btn-xs, .btn-sm, .btn-lg, .btn-xl { /* sizes */ }
.btn-outline, .btn-ghost, .btn-grad, .btn-pill, .btn-icon, .btn-loading { /* variants */ }
The same for .badge-blue / -green / -gold / -neutral with their ::before dot, .tk-input / .tk-select / .tk-textarea with .label / .hint / .error-text, .card-blue / -green / -gold, .alert with .a-info / -success / -warning / -danger, .prog-track / .prog-fill, .tk-table, .pricing-card and the .tk-logo system.
No Angular component for any of these. A button is <button class="btn btn-lg">. Wrapping that in <tk-button size="lg"> would add an Angular component, an input, a template and change detection to produce a <button> — and then you would spend a year adding pass-through inputs for every native attribute somebody needs.
My suggestion: if your component would only forward props to one native element, use a CSS class.
One honest carve-out. The style guide hardcodes decorative tint opacities — the faint background behind a badge or an alert — as rgba() values. We kept them as literal rgba() rather than forcing them into tokens, because the tokens file does not enumerate every tint permutation and inventing tokens nobody asked for is its own kind of mess.
Wrap the Stateful Ones as Angular Components
The short list lives in frontend/src/app/ui/, as small standalone components.
toggle.component.ts → tk-toggle
checkbox.component.ts → tk-checkbox
radio.component.ts → tk-radio
tabs.component.ts → tk-tabs
toast.service.ts + toast-container.component.ts
turnstile.component.ts → tk-turnstile
Each one is a thin wrapper around the CSS classes above. The CSS still does the looking; the component only does the remembering.
RadioComponent shows where the line is. It is presentational — the parent owns which radio in a group is selected. A radio group's state belongs to whoever owns the group, not to each button. Putting it in the child would mean components coordinating with each other, which is exactly the complexity you are trying to avoid.
TabsComponent holds the selected tab and projects the body with <ng-content>, so callers write their own content and the component only handles the strip.
Toasts are a service plus a container. The container mounts once in app.component.ts, and anywhere in the app:
inject(ToastService).success('Saved', 'Your changes are live.');
That replaced a bare alert() in the items page. Worth noting as a small, concrete win — a design system is not only about looking consistent, it is about having a good answer ready when a developer needs one, so they do not reach for alert() at 6pm.
turnstile.component.ts is in the same folder for a different reason. It is a third-party widget with a script to load and a token to emit, and it follows the same small-wrapper pattern so it does not become a special case somewhere else.
Accept That Not Everything Gets Used at Once
Here is the part worth being honest about.
Most of this kit is not used anywhere yet. The pages actually shipping — home, items, blog — use badge, glass-card and btn-primary. The tabs, toggles, pricing cards and data tables are waiting.
That is intentional, and we wrote it into the architecture notes so nobody deletes them:
None of these are used everywhere yet — they exist so new features have a ready-made, on-brand primitive instead of one-off CSS.
Without that note, someone finding twelve unused components reasonably concludes they are dead code.
The value shows up at the next feature, not this one. When a settings page needs a toggle, the toggle exists, on-brand, keyboard-accessible, already dark-mode correct. The alternative is a developer writing one at 6pm that is nearly right, and a design system that drifts one component at a time.
Be clear-eyed about it though. "We will need it later" is also how dead code is born. The test we apply: is it in the design system spec, and would the next obvious feature use it? Tabs and toggles pass. A component invented from scratch because it seemed useful would not.
Conclusion
In this article we learned to keep a static style guide as an unbuilt reference, sort every component by whether it needs state, port the presentational majority to CSS classes reading from design tokens, wrap the small stateful minority as standalone Angular components, and document deliberately unused pieces so nobody deletes them.
The ratio is the lesson. Most of a design system is CSS. The number of things that genuinely need to be Angular components is smaller than you expect.
Reference
Comments
Be the first to comment.