Dark mode theme

Toggle light and dark mode using the React useTheme hook or the Angular GoabThemeService. The user’s choice persists across sessions.

Dark mode is still being tested. The CSS API and tokens may evolve as we iterate.

Theme switching requires @abgov/design-tokens version 2.8.0 or higher with the dark theme stylesheet imported. See Setup for installation steps.

How it works

Both the React provider and the Angular service write a data-theme attribute on the document root (<html>). Design tokens scoped to :root[data-theme=“dark”] activate via CSS cascade, automatically applying dark colors to every component without re-rendering.

Initial mode priority

On first load, the active mode is resolved in this order:

  1. The user’s previously chosen mode, read from local storage
  2. The operating system preference (light or dark)
  3. The framework default (light)

React

The React package exposes a context provider and a hook.

1. Wrap your app with GoabThemeProvider

Place the provider near the top of your app, alongside other framework providers such as BrowserRouter:

// main.tsx
import { GoabThemeProvider } from "@abgov/react-components";
import { BrowserRouter } from "react-router-dom";
import "@abgov/design-tokens/dist/tokens.css";
import "@abgov/design-tokens/dist/dark-theme.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
<BrowserRouter>
  <GoabThemeProvider>
    <App />
  </GoabThemeProvider>
</BrowserRouter>,
);

2. Read and toggle theme with useTheme

Inside any component below the provider, call useTheme() to read the current mode and switch theme:

import { GoabButton, useTheme } from "@abgov/react-components";

function ThemeToggle() {
const { mode, toggle } = useTheme();
const isDark = mode === "dark";

return (
  <GoabButton
    type="tertiary"
    leadingIcon={isDark ? "sunny" : "moon"}
    onClick={toggle}
  >
    {isDark ? "Light mode" : "Dark mode"}
  </GoabButton>
);
}

API

interface GoabThemeContextValue {
/** Current theme mode. */
mode: "light" | "dark";
/** Set theme to a specific mode. */
setMode: (next: "light" | "dark") => void;
/** Flip between light and dark. */
toggle: () => void;
}

interface GoabThemeProviderProps {
children: ReactNode;
/** Initial mode used when no stored or system preference is found. @default "light" */
defaultMode?: "light" | "dark";
}

useTheme throws if called outside of GoabThemeProvider.

Angular

The Angular package exposes a root-provided service backed by a Signal.

1. Import the dark theme stylesheet

Add the import to your global stylesheet (typically src/styles.css):

@import “@abgov/web-components/index.css”;
@import “@abgov/design-tokens/dist/tokens.css”;
@import “@abgov/design-tokens/dist/dark-theme.css”;

2. Inject GoabThemeService

The service is provided in root, so inject it from any component or service:

// app.component.ts
import { Component, inject } from "@angular/core";
import { GoabThemeService } from "@abgov/angular-components";

@Component({
selector: "app-root",
templateUrl: "./app.component.html",
})
export class AppComponent {
readonly theme = inject(GoabThemeService);

toggleTheme() {
  this.theme.toggle();
}
}

3. Bind to the signal in templates

Read theme.mode() as a signal — Angular tracks the dependency automatically and re-renders when the mode changes:

<!-- app.component.html -->
<goab-button
type="tertiary"
[leadingIcon]="theme.mode() === 'dark' ? 'sunny' : 'moon'"
(onClick)="toggleTheme()"
>
{{ theme.mode() === "dark" ? "Light mode" : "Dark mode" }}
</goab-button>

API

@Injectable({ providedIn: "root" })
class GoabThemeService {
/** Read-only signal — use in templates: theme.mode() === "dark". */
readonly mode: Signal<"light" | "dark">;
/** Set theme to a specific mode. */
setMode(next: "light" | "dark"): void;
/** Flip between light and dark. */
toggle(): void;
}

Custom styling for dark mode

If you’re using design system components, they handle dark mode automatically.

If your custom CSS uses design system tokens, dark mode is handled too. The tokens cascade to their dark values when data-theme=“dark” is active.

.my-card {
background-color: var(--goa-color-surface-card);
}

Always verify in both modes.

If your custom CSS uses values that aren’t from our design tokens (specific colors, backgrounds, hardcoded styles), those won’t switch on their own. You can either swap them out for design tokens where possible, or extend dark mode by setting up your own custom property and overriding it under :root[data-theme=“dark”]:

/* Default (light) value */
:root {
--my-card-background: #f9f9f9;
}

/* Dark mode value */
:root[data-theme="dark"] {
--my-card-background: #1a1a1a;
}

/* Use it in your component */
.my-card {
background-color: var(--my-card-background);
}

Using design tokens in SCSS

Dark theme support requires CSS custom properties. Sass variables ($goa-*) are inlined at compile time and cannot flip when the theme changes. Reference tokens via var(--goa-*) wherever a value should respond to theme switching.

The simplest setup imports only tokens.css and uses var(--goa-*) everywhere — every token automatically participates in theme switching, and there is one syntax to remember.

@import "@abgov/web-components/index.css";
@import "@abgov/design-tokens/dist/tokens.css";
@import "@abgov/design-tokens/dist/dark-theme.css";
.my-card {
background: var(--goa-color-surface-card);
font-family: var(--goa-font-family-sans);
padding: var(--goa-space-m);
transition-duration: var(--goa-motion-duration-medium-2);
}

When you need Sass variables

Some workflows need Sass arithmetic, mixins, or functions over token values (for example $goa-space-m * 2). In that case, import tokens.scss alongside the CSS files and use $goa-* for compile-time values only — never for colors, surfaces, or anything that should flip with theme.

@import "@abgov/web-components/index.css";
@import "@abgov/design-tokens/dist/tokens.css";
@import "@abgov/design-tokens/dist/dark-theme.css";
@import "@abgov/design-tokens/dist/tokens.scss";
.my-card {
// Theme-responsive — must use var()
background: var(--goa-color-surface-card);

// Compile-time only — Sass arithmetic OK
padding: $goa-space-m * 2;
border-radius: $goa-border-radius-m;
}