docs

Dark Mode and Color Schemes

Configure light and dark color schemes, track system appearance, and prevent white loading flashes.

On this page

You can adapt pages to the user's preferred light or dark appearance. Pages style themselves using standard CSS media queries— the same way they do in a desktop browser.

You tell the library which color scheme is active, and views update whenever that setting changes.

Styling Pages for Each Scheme

Pages define default styles for the light theme and override them inside the CSS prefers-color-scheme media query:

CSS
body {
  background: white;
  color: #222;
}

@media (prefers-color-scheme: dark) {
  body {
    background: #1e1e1e;
    color: #ddd;
  }
}

🚧 Built-in Elements Stay Light

The color scheme is only a signal to the page. Built-in elements like form controls, scrollbars, and the default canvas keep their light appearance under a dark scheme. The page must style those controls itself.

Managing the System Scheme

Each Renderer maintains a single system color scheme, which defaults to ColorScheme::Light.

By default, every View uses ColorScheme::Auto and follows the system scheme. The library never inspects the OS theme directly— without AppCore, pages remain light until you set a new scheme.

When the system scheme changes, the renderer re-evaluates media queries across every view following the system setting. Styles recalculate and matchMedia change listeners fire during the next rendering update.

Automatic Tracking in AppCore

When you use AppCore, App::Create() reads the OS appearance at startup and forwards it to the renderer. On Windows and macOS, AppCore monitors appearance changes and updates the system scheme automatically.

For details on what App::Create() initializes, see App Lifecycle and Settings.

Tracking behavior varies by OS:

Setting the Scheme Manually

If you don't use AppCore, or when handling theme changes on Linux, call Renderer::set_system_color_scheme():

C++
#include <Ultralight/Ultralight.h>

using namespace ultralight;

RefPtr<Renderer> renderer;

void ApplySystemTheme(bool is_dark) {
  ///
  /// Call this at startup and again whenever the OS tells us the theme
  /// changed. Every View still on Auto picks up the new scheme.
  ///
  renderer->set_system_color_scheme(
      is_dark ? ColorScheme::Dark : ColorScheme::Light);
}

🚧 AppCore Overwrites Manual Changes

On Windows and macOS, AppCore overwrites any scheme you set here on the next OS appearance change. To force a scheme under AppCore, override the view instead— see Overriding the View Scheme.

Overriding the View Scheme

To override the color scheme for an individual view (such as a dark tool panel or preview pane), call View::set_preferred_color_scheme():

C++
#include <Ultralight/Ultralight.h>

using namespace ultralight;

///
/// Called from an in-app Appearance setting. Passing ColorScheme::Auto
/// hands the View back to the system scheme.
///
void SetAppearance(View* view, ColorScheme scheme) {
  view->set_preferred_color_scheme(scheme);
}

To set the scheme when creating a view, assign ColorScheme::Light or ColorScheme::Dark to ViewConfig::preferred_color_scheme.

A view forced to light or dark ignores changes to the system scheme— changing the view's scheme re-evaluates media queries on that page during the next rendering update.

👍 Matching Window Backdrops

When using an AppCore window with a backdrop material, BackdropOptions::theme sets the material's light or dark look and controls the title bar on Windows and macOS. The backdrop setting is separate from the page scheme— force both together so the material and the page match. For a walkthrough, see Native Look and Feel.

Preventing White Loading Flashes

To prevent a view from flashing white while a page loads, configure a dark base background color:

C++
#include <Ultralight/Ultralight.h>

using namespace ultralight;

ViewConfig MakeDarkViewConfig() {
  ///
  /// A dark base under the page, so nothing flashes white while it loads.
  ///
  ViewConfig view_config;
  view_config.background_color = "#1e1e1e";
  return view_config;
}

The base color renders beneath the page before styling applies, and remains visible wherever the page paints nothing. If left unset, the base color defaults to opaque white.

If ViewConfig::is_transparent is set to true, transparency takes precedence and the view keeps a fully transparent base.

To style the native window frame and title bar around the view, see Native Window Styling.