Getting started: Angular
No React: valyro-ui/styles.css via @import in styles.css, the theme via attributes on <html> and a small signals-based service, and component markup straight in templates
1. Create the app
Tested with Angular 22.2 (zoneless by default). The same code also works with --ssr.
npx @angular/cli@latest new my-app --defaults --style=css --ssr=false --skip-git --ai-config=none
cd my-app
2. Install the package
You only need the package's CSS: valyro-ui/styles.css (plus the theme file if your theme isn't one of the two built-in ones). Fonts are separate files: the vercel theme (the ThemeProvider default) uses Geist, so import valyro-ui/fonts/geist.css and valyro-ui/fonts/geist-mono.css next to the styles, or text falls back to the system font; glass uses system fonts and needs none. npm installs react and react-dom into node_modules as peer dependencies, but they stay out of your bundle unless you import them. Until the package is published, install the tarball: npm install ../valyro-ui-0.1.0.tgz.
npm install valyro-ui
3. Add the styles
Angular resolves the file through the package's exports field, so you don't need to change angular.json.
@import 'valyro-ui/styles.css';
4. Disable critical CSS
By default, the production build inlines only part of the CSS into <head> (without the theme background) and loads the rest as media="print", switching it on with a script — until the CSS finishes loading, the page renders unstyled. With inlineCritical: false, the styles load through a regular <link> in <head>. Raise the initial budget: the package CSS is about 510 kB (83 kB gzip), so the default 500 kB warning fires on every build.
"production": {
"budgets": [
{ "type": "initial", "maximumWarning": "1MB", "maximumError": "2MB" },
{ "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }
],
"outputHashing": "all",
"optimization": {
"styles": { "inlineCritical": false }
}
}
5. Flash-free theme
data-theme (vercel, glass or a theme loaded from its own file) and data-mode (light or dark) on <html> turn the theme on: the --ui-* tokens and the page background depend on them. An inline script in <head> applies the saved choice before the first paint.
<!doctype html>
<html lang="en" data-theme="glass" data-mode="light">
<head>
<meta charset="utf-8">
<title>MyApp</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<script>
(function () {
var html = document.documentElement;
var theme = localStorage.getItem('ui-theme');
var mode = localStorage.getItem('ui-mode');
if (theme === 'vercel' || theme === 'glass') html.setAttribute('data-theme', theme);
if (mode === 'light' || mode === 'dark') html.setAttribute('data-mode', mode);
})();
</script>
</head>
<body>
<app-root></app-root>
</body>
</html>
6. Theme service
DOCUMENT instead of the global document, and writes only in the browser, so the service is safe under SSR too.
import { DOCUMENT, Injectable, PLATFORM_ID, inject, signal } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
export type Theme = 'vercel' | 'glass';
export type Mode = 'light' | 'dark';
@Injectable({ providedIn: 'root' })
export class ThemeService {
private readonly html = inject(DOCUMENT).documentElement;
private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
// index.html sets the defaults and its inline script has already applied the saved choice.
readonly theme = signal(this.html.getAttribute('data-theme') as Theme);
readonly mode = signal(this.html.getAttribute('data-mode') as Mode);
toggleTheme(): void {
this.theme.update((theme) => (theme === 'glass' ? 'vercel' : 'glass'));
this.write('theme', this.theme());
}
toggleMode(): void {
this.mode.update((mode) => (mode === 'light' ? 'dark' : 'light'));
this.write('mode', this.mode());
}
private write(name: 'theme' | 'mode', value: string): void {
if (!this.isBrowser) return;
this.html.setAttribute(`data-${name}`, value);
localStorage.setItem(`ui-${name}`, value);
}
}
7. Root component
import { Component, inject } from '@angular/core';
import { ThemeService } from './theme.service';
@Component({
selector: 'app-root',
styleUrl: './app.css',
templateUrl: './app.html',
})
export class App {
protected readonly themes = inject(ThemeService);
}
8. Component markup
The markup is exactly what the React components render (Button, Badge, TextInput, Alert, Card). Only visual components work without React (see the list in the HTML + CSS guide); for any of them, the “Usage” example on the component's page shows the markup when this framework is selected. Angular templates accept it unchanged, SVG included.
<main class="page">
<div class="toggles">
<button class="ui-button" data-variant="secondary" data-size="md" type="button" (click)="themes.toggleTheme()"><span class="ui-button__label">Theme: {{ themes.theme() }}</span></button>
<button class="ui-button" data-variant="secondary" data-size="md" type="button" (click)="themes.toggleMode()"><span class="ui-button__label">Mode: {{ themes.mode() }}</span></button>
</div>
<button class="ui-button" data-variant="primary" data-size="md" type="button"><span class="ui-button__label">Deploy</span></button>
<span class="ui-badge" data-color="green" data-variant="subtle" data-size="md"><span class="ui-badge__label">Ready</span></span>
<div class="ui-field ui-text-input" data-size="md"><input aria-label="Email" placeholder="[email protected]" type="text" class="ui-field__input ui-text-input__input" /></div>
<div role="status" class="ui-alert" data-variant="info">
<span class="ui-alert__icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false" class="ui-icon"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg></span>
<div class="ui-alert__content"><div class="ui-alert__title">Heads up</div><div class="ui-alert__description">Static markup, no React at runtime.</div></div>
</div>
<div class="ui-card ui-panel" data-variant="default" data-padding="md">
<div class="ui-card__header"><h3 class="ui-card__title">Project</h3><p class="ui-card__description">Production deployment</p></div>
<div class="ui-card__content">Body</div>
<div class="ui-card__footer"><button class="ui-button" data-variant="secondary" data-size="md" type="button"><span class="ui-button__label">Open</span></button></div>
</div>
</main>
9. Page layout
.page {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
max-width: 720px;
padding: 32px;
}
.page > div {
align-self: stretch;
}
.toggles {
display: flex;
gap: 12px;
}
10. Build
After replacing the template, update the scaffolded should render title test.
npx ng build
npx serve -s dist/my-app/browser