Color scheme

ColorScheme reports whether the user has the system in light or dark mode.

Import
import { ColorScheme } from '@ng-native/device';
Inject
inject(ColorScheme)
import { Component, computed, inject } from '@angular/core';
import { Image } from '@ng-native/components';
import { ColorScheme } from '@ng-native/device';

@Component({ selector: 'app-logo', imports: [Image], template: `<image [source]="logo()" />` })
export class Logo {
  private readonly scheme = inject(ColorScheme);
  protected readonly logo = computed(() =>
    this.scheme.current() === 'dark' ? require('./logo-dark.png') : require('./logo-light.png'),
  );
}

Styling should almost never touch this directly: @media (prefers-color-scheme: dark) and Tailwind's dark: variant are resolved by the engine without anything being injected, and they are the right tool for "this text is a different color in dark mode." ColorScheme is for the decisions a stylesheet cannot make - which image asset to load, which of two distinct native components to render, which status bar style to request.

Overriding the system

set() puts the whole app in one scheme, whatever the system says. It is the call behind an in-app theme switch: pass 'light' or 'dark' to force one, or null to follow the system again.

import { Component, inject, signal } from '@angular/core';
import { Pressable, Text } from '@ng-native/components';
import { ColorScheme, type Scheme } from '@ng-native/device';

@Component({
  selector: 'app-theme-toggle',
  imports: [Pressable, Text],
  template: `
    <pressable (press)="choose('light')"><text>Light</text></pressable>
    <pressable (press)="choose('dark')"><text>Dark</text></pressable>
    <pressable (press)="choose(null)"><text>System</text></pressable>
    <text>Showing {{ scheme.current() }}, chosen: {{ choice() ?? 'system' }}</text>
  `,
})
export class ThemeToggle {
  protected readonly scheme = inject(ColorScheme);
  protected readonly choice = signal<Scheme | null>(null);

  protected choose(scheme: Scheme | null): void {
    this.choice.set(scheme);
    this.scheme.set(scheme);
  }
}

On a device this calls React Native's Appearance.setColorScheme, with null sent as 'unspecified'. That changes the window's own appearance rather than a class on the root, so the native chrome follows along with the app's CSS: @media (prefers-color-scheme: dark), light-dark() and the root's dark class from watchConditions all switch, and so does current().

A few things to know:

  • app.json has to leave the appearance to the system, with "userInterfaceStyle": "automatic", as the template and the generators write it. A fixed "light" or "dark" locks the app to that style. In Expo Go on iOS set() then changes nothing, and set(null) returns to the fixed style rather than the system's.

  • set('light') and set('dark') update current() and .dark styles at once. When Appearance reports the same scheme afterwards, subscribers ignore it. On iOS that report comes only from the root view, which a full-screen <modal> takes out of the window. set(null) and a system change wait for the report, so inside a full-screen modal they take effect when it closes.

  • current() reports what the app is showing, not what the user chose. Once the app is forced into a scheme it no longer tells you what the system is set to, and it cannot tell "forced dark" from "following a dark system." Keep the choice in a signal of your own, as choice does above.

  • Nothing is saved. The next launch follows the system until the app calls set() again, so store the choice (for example with Storage) and apply it at startup.

  • If the installed React Native has no Appearance.setColorScheme, set() does nothing.

Off a device and on the web

Off a device current() reports light and never changes, because there is no Appearance module underneath it to ask - a platform's absence of a preference is treated as light, which is the same rule React Native's own null result follows. On the web the engine answers prefers-color-scheme itself, so an app rarely needs to inject ColorScheme there at all.

set() does nothing in either place: neither the off-device source nor the browser one can change the scheme, so a web build's theme switch needs a class on the root instead, as theming describes.

Reference

ColorSchemeservice@ng-native/device
import { ColorScheme } from '@ng-native/device';

Methods

set(scheme: Scheme | null): void

Put the whole app in scheme , whatever the system says, or back in the system's with null: an in-app theme switch. The native chrome follows as well as the CSS, since it is the window's own appearance that changes, and current and prefers-color-scheme follow the change.

Angular components as native iOS and Android views, built and shipped with Expo.

An alpha. MIT licensed. Sponsor its development.

An independent project, not affiliated with or endorsed by Google, the Angular team or Expo. Angular is a trademark of Google LLC.