Angular
Standalone-API theming with DI providers, reactive injectables, and a scoping directive.
@themvora/angularAngularv2.0.0StableSSR:PartialZero-flash:Partial1Install
pnpm add @themvora/core @themvora/angular2Set up
import { AppComponent } from "./app/app.component";
import { provideTheme } from "@themvora/angular";
import { bootstrapApplication } from "@angular/platform-browser";
bootstrapApplication(AppComponent, {
providers: [provideTheme({ defaultTheme: "light", initialMode: "system" })],
});provideTheme({ defaultTheme, initialMode }) chooses the starting theme. initialMode: "system" follows the visitor's OS.3Add a small UI
Design system
ActiveTokens resolve at runtime, so this card follows whichever theme is active.
<div class="themvora-example-card">
<div class="themvora-example-header">
<h3 class="themvora-example-title">Design system</h3>
<span class="themvora-example-status">Active</span>
</div>
<p class="themvora-example-description">
Tokens resolve at runtime, so this card follows whichever theme is active.
</p>
<p class="themvora-example-meta">Updated 2 minutes ago</p>
<button class="themvora-example-button" type="button">View themes</button>
</div>This is the component's template. class is right for a static style like this one; [class.some-class] is for a class that depends on component state.
4Style it with Themvora tokens
/* Themvora emits its design tokens as CSS custom properties on the root
element, so a stylesheet only has to read them. Nothing here is
theme-specific: these rules render every theme, including ones you add
later. */
html,
body {
background: var(--theme-color-background);
color: var(--theme-color-foreground);
}
.themvora-example-card {
display: grid;
gap: 10px;
max-width: 22rem;
padding: 16px;
border: 1px solid var(--theme-color-border);
border-radius: var(--theme-radius-lg, 12px);
background: var(--theme-color-card);
color: var(--theme-color-cardForeground);
font-family: inherit;
line-height: 1.5;
box-shadow: var(--theme-shadow-sm, 0 1px 2px rgb(0 0 0 / 0.06));
}
.themvora-example-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.themvora-example-title {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.themvora-example-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
color: var(--theme-color-success);
}
.themvora-example-status::before {
content: "";
width: 6px;
height: 6px;
border-radius: 9999px;
background: currentColor;
}
.themvora-example-description {
margin: 0;
font-size: 13px;
color: var(--theme-color-mutedForeground);
}
.themvora-example-meta {
margin: 0;
font-size: 12px;
color: var(--theme-color-mutedForeground);
}
.themvora-example-button {
justify-self: start;
padding: 8px 14px;
border: 0;
/* Presets and generated themes emit --theme-radius-lg and nothing else on
the radius scale, so the smaller step is derived from it rather than
assumed — the same proportion the utility-class mapping uses. */
border-radius: calc(var(--theme-radius-lg, 12px) * 0.8);
background: var(--theme-color-primary);
color: var(--theme-color-primaryForeground);
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.themvora-example-button:hover {
filter: brightness(1.08);
}
.themvora-example-button:focus-visible {
outline: 2px solid var(--theme-color-ring);
outline-offset: 2px;
}.themvora-example-card and its children read Themvora tokens, so switching the active theme repaints them — no re-render, and no per-theme CSS.The `body` rule uses exactly the two foundational colours — `background` and `foreground`. Every component rule reads one of the 21 colours `validateTheme()` requires, so any valid theme emits it. Radius and shadow are optional and carry fallbacks; no typography token is read, so a theme without a typography group still renders correctly.
5Run and choose a theme
import { Component } from "@angular/core";
import { injectTheme } from "@themvora/angular";
@Component({
selector: "theme-switcher",
standalone: true,
template: `
<main class="page">
<h1>Themvora</h1>
<p>Current theme: {{ state().theme.name }} ({{ state().mode }})</p>
<button class="primary" (click)="toggle()">Toggle theme</button>
<section class="card">
<h2>Themed card</h2>
<p class="muted">Surface, border and text follow the theme.</p>
</section>
</main>
`,
styles: `
.page {
background: var(--theme-color-background);
color: var(--theme-color-foreground);
min-height: 100vh;
padding: 2rem;
}
.primary {
background: var(--theme-color-primary);
color: var(--theme-color-primaryForeground);
border: 0;
border-radius: 0.5rem;
padding: 0.5rem 1rem;
}
.card {
background: var(--theme-color-card);
border: 1px solid var(--theme-color-border);
border-radius: 0.75rem;
margin-top: 1.5rem;
padding: 1rem;
}
.muted {
color: var(--theme-color-mutedForeground);
}
`,
})
export class ThemeSwitcherComponent {
// Must be public — Angular templates cannot access private members.
state = injectTheme();
toggle() {
this.state().toggleTheme();
}
}Theme configuration
import { bootstrapApplication } from "@angular/platform-browser";
import { provideTheme } from "@themvora/angular";
import { AppComponent } from "./app/app.component";
bootstrapApplication(AppComponent, {
providers: [provideTheme({ defaultTheme: "mint-light" })],
});provideTheme() when the app bootstraps. Angular does not use Vite, so there is no bundler plugin to add: the provider emits the pre-paint script. The stylesheet lives in src/styles.css, which the Angular CLI already registers in angular.json.What's Available
@themvora/angular ships 17 exports in 5 categories. Click an export to see what it does and jump to the code that uses it.
Three pages cover this package, and they do different jobs: this index says what exists, the examples page shows each export in code with the problem it solves, and the API reference gives exact signatures and return types.
Use Cases
Short snippets you can drop into an existing app. For the same problems worked through end to end — problem, APIs, expectations, limitations — see the Angular examples.
Provide the runtime
Wire Themvora once in your app config, then inject reactive state anywhere.
import { provideTheme } from "@themvora/angular";
export const appConfig: ApplicationConfig = {
providers: [
provideTheme({ defaultTheme: "light" }),
],
};Toggle light / dark
injectTheme returns reactive ThemeState with setters.
import { Component, inject } from "@angular/core";
import { injectTheme } from "@themvora/angular";
@Component({ selector: "app-toggle", template: `
<button (click)="theme().toggleTheme()">
{{ theme().theme.name }} · {{ theme().mode }}
</button>
` })
export class ThemeToggleComponent {
readonly theme = injectTheme();
}Undo / redo theme changes
Inject history controls and the runtime for live edits.
import { Component, inject } from "@angular/core";
import { injectThemeHistory, injectThemeRuntime } from "@themvora/angular";
@Component({ selector: "app-history", template: `
<button (click)="history.undo()" [disabled]="!history.history().canUndo">Undo</button>
<button (click)="history.redo()" [disabled]="!history.history().canRedo">Redo</button>
<button (click)="runtime.update({ colors: { primary: '#6366f1' } })">
Make primary indigo
</button>
` })
export class ThemeHistoryComponent {
readonly history = injectThemeHistory();
readonly runtime = injectThemeRuntime();
}Scope a subtree
ThemeScopeDirective themes a single element subtree.
import { Component } from "@angular/core";
import { ThemeScopeDirective } from "@themvora/angular";
@Component({
selector: "app-dashboard",
template: `
<div class="global-theme">Inherits global</div>
<div [themvoraScope]="'plum-dark'">Always plum-dark here</div>
`,
standalone: true,
imports: [ThemeScopeDirective],
})
export class DashboardComponent {}Smooth theme transitions
Enable CSS transitions on theme changes for a polished user experience.
import { provideTheme } from "@themvora/angular";
export const appConfig: ApplicationConfig = {
providers: [
provideTheme({
defaultTheme: "light",
transition: { enabled: true, duration: 300, easing: "ease-in-out" },
}),
],
};A scoped panel and the component that hosts it
The directive does the scoping, and a second component assembles the two children so the guide shows a real tree rather than one file in isolation.
import { Component } from "@angular/core";
import type { ThemeTransitionOptions } from "@themvora/core";
import {
injectThemeHistory,
ThemeScopeDirective,
} from "@themvora/angular";
@Component({
selector: "history-controls",
standalone: true,
imports: [ThemeScopeDirective],
template: `
<div [themvoraScope]="'plum-dark'" [themvoraScopeTransition]="transition">
Scoped to plum-dark
</div>
<button (click)="undo()" [disabled]="!canUndo()">Undo</button>
<button (click)="redo()" [disabled]="!canRedo()">Redo</button>
`,
})
export class HistoryControlsComponent {
// `history` is a signal of `{ canUndo, canRedo }`; `undo`/`redo` are actions.
private history = injectThemeHistory();
transition: ThemeTransitionOptions = { duration: 300, easing: "ease" };
canUndo() { return this.history.history().canUndo; }
canRedo() { return this.history.history().canRedo; }
undo() { this.history.undo(); }
redo() { this.history.redo(); }
}ThemeScopeDirective bound to plum-dark through [themvoraScope], with the history injectables driving the buttons.
import { Component } from "@angular/core";
import { ThemeSwitcherComponent } from "./theme-switcher.component";
import { HistoryControlsComponent } from "./history-controls.component";
@Component({
selector: "app-root",
standalone: true,
imports: [ThemeSwitcherComponent, HistoryControlsComponent],
template: `
<header><theme-switcher /></header>
<main><history-controls /></main>
`,
})
export class AppComponent {}The standalone root component that imports both children — the tree the snippets above assume.
The complete runnable app behind this guide is examples/apps/angular — listed with the other example apps.
Related documentation
Every export in @themvora/angular, with code and use cases
Build your own color palettes and semantic tokens
Complete @themvora/angular documentation
Upgrade from previous versions
Check framework and library version support