Styling

ng-oat doesn't invent its own styling system. Everything is powered by Oat CSS design tokens โ€” plain CSS custom properties you can override anywhere.

How styling works

Oat CSS uses a layered architecture with @layer โ€” theme tokens come first, then base element styles, then component styles, then utilities. This means you can override any token in your own stylesheet without specificity wars.

ng-oat components render semantic HTML elements that Oat already knows how to style. A <ng-oat-button> renders a real <button>, a <ng-oat-card> renders a <div class="card">, and so on. This means Oat's CSS applies naturally โ€” no shadow DOM, no encapsulation issues.

Design tokens

Oat defines 80+ CSS custom properties (design tokens) that control colors, spacing, typography, borders, and animations globally. All tokens use light-dark() for automatic dark-mode support and clamp() for fluid scaling.

Colors

TokenPurposeLight / Dark
--primaryPrimary accent#574747 / #fafafa
--primary-foregroundText on primary#fafafa / #18181b
--backgroundPage background#fff / #09090b
--foregroundDefault text#09090b / #fafafa
--cardCard background#fff / #18181b
--card-foregroundCard text#09090b / #fafafa
--secondarySecondary background#f4f4f5 / #27272a
--muted-foregroundMuted text#71717a / #a1a1aa
--accentHover / tint#f4f4f5 / #27272a
--borderBorders#d4d4d8 / #52525b
--dangerError / destructive#d32f2f / #f4807b
--successSuccess state#008032 / #6cc070
--warningWarning state#a65b00 / #f0a030
--ringFocus ring#574747 / #d4d4d8

Spacing (fluid)

TokenValue
--space-10.25rem (4px)
--space-20.5rem (8px)
--space-3clamp(0.5rem, 1.5vw, 0.75rem)
--space-4clamp(0.5rem, 2vw, 1rem)
--space-5clamp(0.75rem, 2.5vw, 1.25rem)
--space-6clamp(0.75rem, 3vw, 1.5rem)
--space-8clamp(1rem, 4vw, 2rem)
--space-10clamp(1.5rem, 5vw, 2.5rem)
--space-123rem
--space-143.5rem
--space-164rem
--space-184.5rem

Border radius

TokenValue
--radius-small0.125rem
--radius-mediumclamp(0.25rem, 0.8vw, 0.375rem)
--radius-largeclamp(0.5rem, 1.5vw, 0.75rem)
--radius-full9999px (pill)

Typography

TokenValue / Range
--text-1clamp(1.75rem, โ€ฆ, 2.25rem) โ€” largest heading
--text-2clamp(1.5rem, โ€ฆ, 1.875rem)
--text-3clamp(1.25rem, โ€ฆ, 1.5rem)
--text-4clamp(1.125rem, โ€ฆ, 1.25rem)
--text-5clamp(1rem, โ€ฆ, 1.125rem)
--text-61rem โ€” base
--text-7clamp(0.8125rem, โ€ฆ, 0.875rem) โ€” small
--text-8clamp(0.6875rem, โ€ฆ, 0.75rem) โ€” smallest
--text-regularclamp(1rem, โ€ฆ, 1.125rem) โ€” body text

Line-height & letter-spacing

TokenValue
--leading-none1
--leading-tight1.25
--leading-snug1.375
--leading-normalclamp(1.5, โ€ฆ, 1.6)
--leading-relaxed1.625
--leading-loose2
--tracking-tighter-0.05em
--tracking-tight-0.025em
--tracking-normal0em
--tracking-wide0.025em
--tracking-wider0.05em
--tracking-widest0.1em

Font weight & family

TokenValue
--font-normal400
--font-medium500
--font-semibold600
--font-bold700
--font-sanssystem-ui, sans-serif
--font-monoui-monospace, Consolas, monospace

Effects

TokenValue
--shadow-small0 1px 2px rgb(0 0 0 / 0.05)
--shadow-medium0 1px 3px rgb(0 0 0 / 0.1), โ€ฆ
--shadow-large0 4px 6px rgb(0 0 0 / 0.1), โ€ฆ
--transition-fast120ms cubic-bezier(0.4, 0, 0.2, 1)
--transition200ms cubic-bezier(0.4, 0, 0.2, 1)

Overriding tokens globally

The simplest way to customize the look is to override tokens in your styles.css:

css
/* src/styles.css */
:root {
  --primary: #6366f1;        /* Indigo primary */
  --radius-medium: 12px;     /* Rounder cards */
  --font-bold: 700;          /* Heavier bold weight */
}

Because Oat uses @layer, your overrides in styles.css will always win regardless of source order.

Runtime theming with provideNgOatTheme

For dynamic themes (dark mode toggle, user-selected palettes), use the built-in theme provider:

typescript
// app.config.ts
import { provideNgOat, provideNgOatTheme } from '@letsprogram/ng-oat';

export const appConfig: ApplicationConfig = {
  providers: [
    provideNgOat(),
    provideNgOatTheme({
      tokens: {
        '--oat-primary': '#6366f1',
        '--oat-background': '#fafafa',
        '--oat-radius-medium': '12px',
      },
    }),
  ],
};

Then inject NgOatThemeRef to change tokens at runtime:

typescript
// any component
import { Component, inject } from '@angular/core';
import { NgOatThemeRef } from '@letsprogram/ng-oat';

@Component({ ... })
export class SettingsPage {
  private theme = inject(NgOatThemeRef);

  switchToOcean() {
    this.theme.setTokens({
      '--oat-primary': '#0ea5e9',
      '--oat-background': '#0f172a',
      '--oat-foreground': '#f8fafc',
    });
  }

  resetTheme() {
    this.theme.reset();
  }
}

Dark mode

Oat CSS has built-in dark mode support via color-scheme. You can toggle it by setting the color-scheme property on the document root:

typescript
// In your root component
import { Component, inject, signal } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Component({ ... })
export class App {
  private doc = inject(DOCUMENT);
  isDark = signal(false);

  toggleTheme() {
    this.isDark.update(v => !v);
    this.doc.documentElement.style.colorScheme =
      this.isDark() ? 'dark' : 'light';
  }
}

Oat automatically adjusts all token values when the color scheme changes. No need to define separate dark token values yourself โ€” it's handled by the framework.

Scoped overrides

Since tokens are CSS custom properties, you can scope them to any container:

css
/* Darker sidebar section */
.sidebar {
  --primary: #10b981;
  --background: #0f172a;
  --foreground: #e2e8f0;
  --border: #334155;
}

Any ng-oat component rendered inside .sidebar will pick up those overrides automatically. This is useful for sections with different visual treatments.

Oat's CSS layers

Oat organizes its CSS into layers for clean override behavior:

css
/* Oat's internal layer order */
@layer theme, base, components, animations, utilities;

/* theme     โ†’ CSS custom property definitions */
/* base      โ†’ element-level styles (h1, p, button, table, etc.) */
/* components โ†’ card, dialog, sidebar, accordion, etc. */
/* animations โ†’ keyframes and transitions */
/* utilities  โ†’ helper classes (.hstack, .mt-4, etc.) */

Your app styles (outside any @layer) automatically take precedence over all Oat layers. This is why simple token overrides in styles.css just work.

Utility classes

Oat provides a comprehensive set of utility classes for layout, spacing, typography, and responsive design. Here's a quick summary โ€” see the Utilities page for the full reference.

CategoryClasses
Flex.hstack / .vstack / .flex / .flex-col / .flex-wrap
Alignment.items-center / start / end ยท .justify-center / between / end / start
Gap.gap-1 through .gap-8 + responsive sm:gap-*, md:gap-*
Margin.mt-0 / 2 / 4 / 6 / 8 ยท .mb-0 / 2 / 4 / 6 / 8
Padding.p-0 / 2 / 4 / 6 / 8 ยท .px-2 / 4 ยท .py-2 / 4 + responsive
Width.w-100 / .w-auto / .max-w-sm / md / lg / xl
Display.d-none / block / flex / grid / inline / inline-block + responsive
Typography.text-1โ€“.text-8, colors, weights, alignment, tracking, leading, transform, decoration, wrapping, clamp
Grid.row ยท .col-1โ€“.col-12 + responsive sm: / md: / lg: / xl:

Next up

Now that you understand how styling works, head over to the Usage guide to learn how to work with components, directives, and Signal Forms effectively.