Getting Started
Theming
FrameUI is token-driven. Components do not hardcode light or dark colors. They read semantic CSS variables such as --frame-background, --frame-foreground, and --frame-primary, so you can decide where theme state lives and how it should integrate with the rest of your app. In most cases, the host app should remain the single source of truth and the component library should align with that existing styling system.
How it works
The foundation package defines the token contract. Light and dark fill that contract with different values. The component package then consumes those tokens. That split is what makes the library work well with Tailwind CSS, Bootstrap, app-specific utility classes, and plain CSS without trying to replace them.
1. Source of truth
Host app
Owns the main token system, brand palette, and dark mode strategy that the components should align with.
2. Bridge layer
Foundation
Maps the host app tokens into the FrameUI contract and optionally provides Angular light/dark helpers.
3. Consumption
Components
Read token values and stay visually consistent without needing to know who owns dark mode.
Choose a source of truth
The recommended mental model is simple: your app's existing styling system should own the tokens and theme switch, and the component library should follow that source of truth. If a team prefers, the component library can also become the single source of truth, but that is typically the secondary option rather than the default recommendation.
Preferred
Host app owns the theme
Tailwind, Bootstrap, or your app-level token system remains the single source of truth. The component library consumes that palette through --frame-* tokens.
Supported
Library owns the theme
The component library can also manage theme state directly. This is useful when a team wants the library to coordinate theming, but it is usually not the first choice for existing applications.
Library-managed via data-theme
Use this when your Angular app wants FrameUI to manage light/dark state directly. The service writes an attribute such as html[data-theme="dark"]. This is fully supported, but in apps that already use Tailwind, Bootstrap, or another token system, it is usually preferable to keep the host app in charge instead.
App config
import { provideFrameUI } from '@frame-ui-ng/foundation';
export const appConfig = {
providers: [
provideFrameUI({
defaultTheme: 'light',
}),
],
};Externally managed and observed by the library
Use this when a shell, CMS, Tailwind-based app, or another design layer already owns dark mode. In observe mode FrameUI reads the DOM and does not write light/dark state. This is the preferred setup for most existing applications because it keeps ownership with the host app.
App config
import { provideFrameUI } from '@frame-ui-ng/foundation';
export const appConfig = {
providers: [
provideFrameUI({
strategy: 'class',
mode: 'observe',
className: 'dark',
}),
],
};Tailwind CSS
Tailwind works best as the primary token system. In an existing app, map the FrameUI tokens to your Tailwind tokens so custom layouts and library components stay visually aligned instead of drifting apart.
Tailwind token bridge
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-background: oklch(0.99 0 0);
--color-foreground: oklch(0.15 0 0);
--color-muted: oklch(0.96 0 0);
--color-muted-foreground: oklch(0.45 0 0);
--color-surface: oklch(1 0 0);
--color-surface-foreground: oklch(0.15 0 0);
--color-border: oklch(0.92 0 0);
--color-primary: oklch(0.21 0 0);
--color-primary-foreground: oklch(0.98 0 0);
--color-accent: oklch(0.96 0 0);
--color-accent-foreground: oklch(0.15 0 0);
--color-input: oklch(0.92 0 0);
--color-ring: oklch(0.7 0 0);
}
:root {
--frame-background: var(--color-background);
--frame-foreground: var(--color-foreground);
--frame-muted: var(--color-muted);
--frame-muted-foreground: var(--color-muted-foreground);
--frame-surface: var(--color-surface);
--frame-surface-foreground: var(--color-surface-foreground);
--frame-border: var(--color-border);
--frame-primary: var(--color-primary);
--frame-primary-foreground: var(--color-primary-foreground);
--frame-accent: var(--color-accent);
--frame-accent-foreground: var(--color-accent-foreground);
--frame-input: var(--color-input);
--frame-ring: var(--color-ring);
}
.dark {
--color-background: oklch(0.15 0 0);
--color-foreground: oklch(0.98 0 0);
--color-muted: oklch(0.27 0 0);
--color-muted-foreground: oklch(0.71 0 0);
--color-surface: oklch(0.2 0 0);
--color-surface-foreground: oklch(0.98 0 0);
--color-border: oklch(1 0 0 / 0.12);
--color-primary: oklch(0.92 0 0);
--color-primary-foreground: oklch(0.2 0 0);
--color-accent: oklch(0.27 0 0);
--color-accent-foreground: oklch(0.98 0 0);
--color-input: oklch(1 0 0 / 0.15);
--color-ring: oklch(0.56 0 0);
} In this setup, Tailwind owns the palette and the .dark selector, while the component library simply reads the same semantic values through its own token contract. If you want the library to drive the same selector instead, the shared class strategy is still available, but host-app-first remains the recommended baseline.
Bootstrap and other CSS frameworks
The same principle applies outside Tailwind: let the framework or app-level token system stay in charge, and map the FrameUI tokens to that existing palette instead of maintaining a second one.
Bootstrap variable bridge
:root {
--bs-body-bg: #ffffff;
--bs-body-color: #18181b;
--bs-border-color: #e4e4e7;
--bs-primary: #18181b;
--bs-primary-text-emphasis: #ffffff;
--bs-secondary-bg: #f4f4f5;
--bs-secondary-color: #18181b;
--frame-background: var(--bs-body-bg);
--frame-foreground: var(--bs-body-color);
--frame-border: var(--bs-border-color);
--frame-primary: var(--bs-primary);
--frame-primary-foreground: var(--bs-primary-text-emphasis);
--frame-surface: var(--bs-secondary-bg);
--frame-surface-foreground: var(--bs-secondary-color);
}
[data-bs-theme='dark'] {
--bs-body-bg: #18181b;
--bs-body-color: #fafafa;
--bs-border-color: rgba(255, 255, 255, 0.12);
--bs-primary: #fafafa;
--bs-primary-text-emphasis: #18181b;
--bs-secondary-bg: #27272a;
--bs-secondary-color: #fafafa;
}The important part is to bridge the component library into the host app's semantic token layer rather than duplicating light and dark values in multiple places.
Local overrides
Use scoped token overrides for brand, campaign, or product-specific moments. They stay inside the current light or dark mode instead of becoming additional registered themes.
Scoped token override
.marketing-hero {
--frame-primary: oklch(0.69 0.19 38);
--frame-primary-foreground: oklch(0.99 0.01 95);
--frame-radius-lg: 1rem;
}