Skip to content

Theming

Every colour, radius, spacing step and font in @gjsify/adwaita-web comes from a CSS custom property. Set one above the widgets and everything below picks it up. That is the whole theming API, and it is the part that keeps working across upgrades.

Put the tokens on a container that wraps your widgets. This is the route that behaves the same in light and dark mode: the only rules that give a token a second, scheme-dependent value sit on :root and on the .theme-dark / .theme-light elements, so a container below them keeps your values in both.

CSS
.app {
--accent-bg-color: #9141ac;
--accent-fg-color: #ffffff;
--window-bg-color: #f6f5f4;
--card-radius: 16px;
}
HTML
<div class="app">
<adw-preferences-group title="Purple everything">
<adw-switch-row title="Wi-Fi" active></adw-switch-row>
</adw-preferences-group>
</div>

:root works too, with one thing to watch: most of the colour tokens get a second value in dark mode, and that dark rule outranks a plain :root declaration. Tokens that mean the same thing in both schemes are safe on :root: the accent fill, the radii, the spacing scale, the fonts and the opacity tokens.

CSS
:root {
--accent-bg-color: #9141ac;
--font-family: 'Inter', sans-serif;
--card-radius: 16px;
}

If you want to move one of the scheme-dependent colours globally, give it a dark counterpart as well, or move the override into a container as above.

Dark mode applies on its own under prefers-color-scheme: dark. To pin a scheme for part of the page, put .theme-dark or .theme-light on any element, not only on <html>:

HTML
<div class="theme-dark">
<adw-preferences-group title="Always dark">
<adw-switch-row title="Wi-Fi" active></adw-switch-row>
</adw-preferences-group>
</div>

Those two classes re-declare the full palette on the element they sit on. So if one element carries both your theme and a .theme-* class, write the selector so yours wins:

CSS
.app.theme-dark {
--window-bg-color: #201d18;
}

Adwaita has nine named accents: blue (the default), teal, green, yellow, orange, red, pink, purple and slate. Applying one sets both accent properties for you, picking the standalone variant that matches the target’s current scheme:

TypeScript
import { applyAdwaitaAccent, clearAdwaitaAccent } from '@gjsify/adwaita-web';
applyAdwaitaAccent('purple'); // on <html>
applyAdwaitaAccent('teal', { target: panel }); // on one element
applyAdwaitaAccent('teal', { target: panel, dark: true });
clearAdwaitaAccent(); // back to the stylesheet's own value

The values below are the light defaults, read out of the stylesheet that declares them when this page is built.

Window

TokenLight default
--window-bg-color#fafafb
--window-fg-colorrgba(0, 0, 6, 0.8)

Views

TokenLight default
--view-bg-color#ffffff
--view-fg-colorrgba(0, 0, 6, 0.8)

Header bar

TokenLight default
--headerbar-bg-color#ffffff
--headerbar-fg-colorrgba(0, 0, 6, 0.8)
--headerbar-shade-colorrgba(0, 0, 6, 0.12)

Hairline separators (header underline, sidebar/pane dividers)

TokenLight default
--separator-colorrgba(0, 0, 6, 0.1)

The GENERIC shade the whole `*-shade-color` family below specialises (`_colors.scss:79,141`)

TokenLight default
--shade-colorrgba(0, 0, 6, 0.07)
TokenLight default
--sidebar-bg-color#ebebed
--sidebar-fg-colorrgba(0, 0, 6, 0.8)
--sidebar-backdrop-color#f2f2f4
--sidebar-shade-colorrgba(0, 0, 6, 0.07)
--sidebar-border-colorrgba(0, 0, 6, 0.07)

The MIDDLE pane of a three-pane setup

TokenLight default
--secondary-sidebar-bg-color#f3f3f5
--secondary-sidebar-fg-colorrgba(0, 0, 6, 0.8)
--secondary-sidebar-backdrop-color#f6f6fa
--secondary-sidebar-shade-colorrgba(0, 0, 6, 0.07)
--secondary-sidebar-border-colorrgba(0, 0, 6, 0.07)

Cards / boxed lists

TokenLight default
--card-bg-color#ffffff
--card-fg-colorrgba(0, 0, 6, 0.8)
--card-shade-colorrgba(0, 0, 6, 0.07)

Accent

TokenLight default
--accent-bg-color#3584e4
--accent-fg-color#ffffff
--accent-color#1c71d8

Popover (menus, dropdowns) — an elevated surface above the window

TokenLight default
--popover-bg-color#ffffff
--popover-fg-colorrgba(0, 0, 6, 0.8)
--popover-shade-colorrgba(0, 0, 6, 0.07)

Dialog scrim — the dimming wash behind a presented dialog (Adw dims with --shade-color at double alpha; this is the equivalent neutral wash)

TokenLight default
--dialog-scrim-colorrgba(0, 0, 6, 0.32)

Buttons

TokenLight default
--button-bg-colorrgba(0, 0, 6, 0.08)
--button-hover-colorrgba(0, 0, 6, 0.13)
--button-active-colorrgba(0, 0, 6, 0.19)

Entries

TokenLight default
--entry-bg-color#ffffff
--entry-border-colorrgba(0, 0, 6, 0.18)
--entry-border-hover-colorrgba(0, 0, 6, 0.28)

Destructive action

TokenLight default
--destructive-bg-color#e01b24
--destructive-fg-color#ffffff

Semantic states

TokenLight default
--success-bg-color#2ec27e
--success-fg-color#ffffff
--success-coloroklab(from var(--success-bg-color) min(l, 0.5) a b)
--warning-bg-color#e5a50a
--warning-fg-colorrgba(0, 0, 0, 0.8)
--warning-coloroklab(from var(--warning-bg-color) min(l, 0.5) a b)
--error-bg-color#e01b24
--error-fg-color#ffffff
--error-coloroklab(from var(--error-bg-color) min(l, 0.5) a b)

Switch

TokenLight default
--switch-off-bgrgba(0, 0, 0, 0.2)
--switch-knob-bg#ffffff

Layout radii

TokenLight default
--window-radius15px
--card-radius12px
--button-radius9px

Spacing scale

TokenLight default
--spacing-xs6px
--spacing-s9px
--spacing-m12px
--spacing-l18px
--spacing-xl24px

Typography — GNOME default: Adwaita Sans 11

TokenLight default
--font-family'Adwaita Sans', 'Cantarell', 'Inter', 'Segoe UI', sans-serif
--font-size-base11pt
--font-size-small9pt
--font-size-heading12pt
--dim-opacity0.55

Borders and disabled dimming, as libadwaita's own two-step (_colors.scss:263, 317-329): an OPACITY token the high-contrast query moves, and a colour DERIVED from it against `currentColor` — so a border follows the text colour of the surface it is drawn on instead of being a fixed grey

TokenLight default
--border-opacity15%
--border-colorcolor-mix(in srgb, currentColor var(--border-opacity), transparent)
--disabled-opacity0.5

Document + monospace families, behind `.document` / `.monospace` (_labels.scss:75-84)

TokenLight default
--document-font-familyvar(--font-family)
--document-font-sizevar(--font-size-base)
--monospace-font-family'Adwaita Mono', ui-monospace, 'SF Mono', 'Menlo', monospace
--monospace-font-sizevar(--font-size-base)

68 tokens in 21 groups.

Three of them move on their own: under prefers-contrast: more the stylesheet changes --border-opacity, --dim-opacity and --disabled-opacity. Every colour derived from those follows, so --border-color sharpens without a query of your own.

You can also write plain CSS. The components render in the light DOM, so a higher-specificity rule of yours wins the way it normally would. Tokens are the route that survives upgrades; a rule targeting internal markup is not. See How It Works for what that boundary does and does not guarantee.

If you would rather build a skin from the source, the SCSS partials ship with the package at @gjsify/adwaita-web/scss/…, with @gjsify/adwaita-web/scss as the entry point.

On the desktop, the accent is not yours to set through AdwStyleManager

Section titled “On the desktop, the accent is not yours to set through AdwStyleManager”

A native GTK app takes its accent from the user’s GNOME setting, and AdwStyleManager:accent-color is the wrong handle for an app’s own accent on every runtime: it is an enum (AdwAccentColor — nine named accents), so a token colour like #9141ac has no representation in it. It is read-only as well — libadwaita installs the property readable-only, and every closure measured agrees — but the enum is the reason that holds whatever a flag says, which is why it is the one given.

A native app can still override the palette, and by the same mechanism this page uses on the web: redefining --accent-bg-color and its siblings on :root, in a Gtk.CssProvider above libadwaita’s own. What is different on the desktop is the provider priority — see Styling on GTK for the three measurements and the registry that wraps them.

Whether you should is a separate question, and the default answer is no: someone who picked an accent in Settings expects your app to use it. If your app renders on both, set the token for the web and leave the desktop alone unless the look is the point.