Getting Started
Theming
FrameUI components read CSS variables. Change the variables and the components follow. The main values are primary color, background, text, surfaces, borders, focus color, status colors, radius, density, and shadows. Your app can own those values, or FrameUI can manage the light/dark selector for smaller setups.
How it works
The foundation package defines CSS variables. Light and dark provide different values for those variables. Components read the variables and do not need to know whether Tailwind, Bootstrap, your app, or FrameUI changed them.
1. Theme owner
Host app
Usually owns brand colors, dark mode, and app-level CSS variables.
2. Bridge layer
Foundation
Provides FrameUI variables, density, radii and shadow layers.
3. Consumption
Components
Read token values and stay visually consistent.
What you can change
Start with the shared values below. They cover the parts users usually expect to adjust without breaking the FrameUI style. Component-specific variables are still available for precise cases, but most apps should not need to start there.
Primary color
Action color and active states.
Background and text
Page background and readable foreground color.
Surfaces and borders
Cards, panels, inputs, overlays, and lines.
Focus color
Keyboard focus and technical frame accents.
Status colors
Success, warning, info, and destructive states.
Light and dark
Either your app or FrameUI controls the selector.
Radius
Square by default, with shared softer steps when needed.
Density
Compact, default, or comfortable spacing.
Shadows
Controlled elevation for overlays and raised surfaces.
Choose who controls the theme
The decision is two small questions: who controls the light/dark state, and which DOM switch does the app use? In existing apps, the host app should usually control the theme and FrameUI should follow it. In new or isolated apps, FrameUI can control the switch directly.
Preferred
App controls the theme
Tailwind, Bootstrap, or your app-level CSS keeps writing light/dark values. FrameUI reads the resulting --frame-* variables.
Supported
FrameUI controls the theme
FrameUI can write the root selector directly. This is useful for smaller apps or apps without an existing theme switch.
FrameUI controls data-theme
Use this when FrameUI should manage light/dark state directly. The service writes html[data-theme="light"] or html[data-theme="dark"].
App config
import { provideFrameUI } from '@frame-ui-ng/foundation';
export const appConfig = {
providers: [
provideFrameUI({
defaultTheme: 'light',
theme: {
controlledBy: 'frame',
using: 'data-theme',
},
}),
],
};FrameUI controls .dark
Use this when FrameUI should manage the theme, but the app expects the shared html.dark selector, for example in a Tailwind setup.
App config
import { provideFrameUI } from '@frame-ui-ng/foundation';
export const appConfig = {
providers: [
provideFrameUI({
theme: {
controlledBy: 'frame',
using: 'class',
},
}),
],
};App controls .dark
Use this when a shell, CMS, Tailwind-based app, or another design layer already owns dark mode. FrameUI reads html.dark and does not write light/dark state. In this setup, ThemeService.theme() can be read, but ThemeService.setTheme() throws because the app owns the switch.
App config
import { provideFrameUI } from '@frame-ui-ng/foundation';
export const appConfig = {
providers: [
provideFrameUI({
theme: {
controlledBy: 'app',
using: 'class',
},
}),
],
};App controls data-theme
Use this when the app already writes html[data-theme="light"] or html[data-theme="dark"]. FrameUI reads that attribute and keeps ThemeService.theme() in sync without changing it.
App config
import { provideFrameUI } from '@frame-ui-ng/foundation';
export const appConfig = {
providers: [
provideFrameUI({
theme: {
controlledBy: 'app',
using: 'data-theme',
},
}),
],
};Global appearance options
Use provideFrameUI for appearance choices that should apply to every component in the app. Density belongs here because it changes the feel of controls, menus, panels, and tables across the app. Corner handles are enabled by default as part of the FrameUI technical style. Disable them once at the foundation layer when your product should use quieter surfaces without editing each component.
App config
import { provideFrameUI } from '@frame-ui-ng/foundation';
export const appConfig = {
providers: [
provideFrameUI({
density: 'compact',
shadow: 'flat',
disableCornerHandles: true,
}),
],
};Migration
The old strategy, mode, attribute, and className options were replaced by theme.controlledBy and theme.using. The new names describe the actual decision instead of the internal implementation.
Before and after
// Before
provideFrameUI({
strategy: 'class',
mode: 'observe',
});
// After
provideFrameUI({
theme: {
controlledBy: 'app',
using: 'class',
},
});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-border-strong: oklch(0.72 0 0);
--color-primary: oklch(0.21 0 0);
--color-primary-foreground: oklch(0.98 0 0);
--color-destructive: oklch(0.58 0.2 25);
--color-destructive-foreground: oklch(0.98 0 0);
--color-success: oklch(0.62 0.18 149);
--color-success-foreground: oklch(0.98 0 0);
--color-warning: oklch(0.68 0.16 65);
--color-warning-foreground: oklch(0.15 0 0);
--color-info: oklch(0.58 0.19 255);
--color-info-foreground: oklch(0.98 0 0);
--color-accent: oklch(0.96 0 0);
--color-accent-foreground: oklch(0.15 0 0);
--color-ring: oklch(0.7 0 0);
--shadow-frame-sm: none;
--shadow-frame-md: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--shadow-frame-lg: 0 24px 80px rgb(0 0 0 / 0.18), 0 8px 24px rgb(0 0 0 / 0.12);
}
: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-border-strong: var(--color-border-strong);
--frame-primary: var(--color-primary);
--frame-primary-foreground: var(--color-primary-foreground);
--frame-destructive: var(--color-destructive);
--frame-destructive-foreground: var(--color-destructive-foreground);
--frame-success: var(--color-success);
--frame-success-foreground: var(--color-success-foreground);
--frame-warning: var(--color-warning);
--frame-warning-foreground: var(--color-warning-foreground);
--frame-info: var(--color-info);
--frame-info-foreground: var(--color-info-foreground);
--frame-accent: var(--color-accent);
--frame-accent-foreground: var(--color-accent-foreground);
--frame-ring: var(--color-ring);
--frame-shadow-sm: var(--shadow-frame-sm);
--frame-shadow-md: var(--shadow-frame-md);
--frame-shadow-lg: var(--shadow-frame-lg);
}
.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-border-strong: oklch(1 0 0 / 0.24);
--color-primary: oklch(0.92 0 0);
--color-primary-foreground: oklch(0.2 0 0);
--color-destructive: oklch(0.7 0.19 22);
--color-destructive-foreground: oklch(0.98 0 0);
--color-success: oklch(0.72 0.17 149);
--color-success-foreground: oklch(0.15 0 0);
--color-warning: oklch(0.78 0.16 75);
--color-warning-foreground: oklch(0.15 0 0);
--color-info: oklch(0.72 0.15 255);
--color-info-foreground: oklch(0.15 0 0);
--color-accent: oklch(0.27 0 0);
--color-accent-foreground: oklch(0.98 0 0);
--color-ring: oklch(0.56 0 0);
--shadow-frame-md: 0 10px 15px -3px rgb(0 0 0 / 0.35);
--shadow-frame-lg: 0 24px 80px rgb(0 0 0 / 0.38), 0 8px 24px rgb(0 0 0 / 0.24);
} In this setup, Tailwind owns the palette and the .dark selector, while the component library reads the same values through FrameUI variables. If you want FrameUI to drive the same selector instead, use theme: { controlledBy: 'frame', using: 'class' }, 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;
} Bootstrap usually writes data-bs-theme. FrameUI does not need to observe that attribute for visual theming. Let Bootstrap keep controlling the page and map the FrameUI variables to Bootstrap variables in CSS.
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;
}