Logo

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',
    }),
  ],
};

Library-managed via a shared .dark class

Use this when you want ThemeService to stay in charge, but Tailwind utilities should react to the same switch. This works well when a team intentionally wants the component library to drive theming, even though the preferred setup is usually for Tailwind itself to remain the source of truth.

App config

import { provideFrameUI } from '@frame-ui-ng/foundation';

export const appConfig = {
  providers: [
    provideFrameUI({
      strategy: 'class',
      className: 'dark',
    }),
  ],
};

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;
}