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.

Dark theme styles are included in @abgov/web-components/index.css. If you completed the developer setup, you do not need another package or stylesheet.

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/web-components/index.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. Add the component stylesheet

The component stylesheet includes the dark theme styles. If you completed the developer setup, you already have this import in your global stylesheet:

@import “@abgov/web-components/index.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);
}