Styles & hot-reload

June 28, 2026 · View on GitHub

node-gtk/styles is a small helper for applying CSS to a GTK app — and, in development, hot-reloading it so the window updates as you edit, with no restart. It wraps the usual Gtk.CssProvider + Gtk.StyleContext dance behind a single styles object.

import { styles } from 'node-gtk/styles'

It targets GTK 4 (the version your app already loaded is used automatically).

Apps created with node-gtk create already wire this in — their style.css hot-reloads under npm run dev out of the box.

Quick start

import Gtk from 'gi:Gtk-4.0'
import { styles } from 'node-gtk/styles'

app.on('activate', () => {
  // A .css file (re-read live on edit in development):
  styles.addFile(new URL('../style.css', import.meta.url))

  // Inline CSS:
  styles.add(`button.suggested-action { padding: 0 24px; }`)

  // ...build your window...

  styles.install()   // flush queued styles and start the watcher
  window.present()
})

The two ways to add styles

MethodUse it forHot-reload
styles.addFile(path)a .css stylesheetre-reads the file into its provider
styles.add(css)inline CSS in a source modulere-imports that module (see the caveat below)

Both return a handle{ update(next), refresh(), remove() } — so you can replace, re-apply, or drop a sheet later from code:

const sheet = styles.add(`label { color: red; }`)
sheet.update(`label { color: green; }`)   // replace in place
sheet.remove()                            // remove from the display

Dynamic stylesheets

For CSS built from live state — a theme palette, the current fonts — pass a render function (() => string) to styles.add instead of a string. It runs immediately, again on every hot-reload of its module (so editing the CSS-generating code re-applies it), and on demand when you call the handle's refresh() — which you do whenever the state it reads changes:

const sheet = styles.add(() => `:root { --accent: ${theme.accent}; }`)
// ...later, when the theme changes:
theme.onChange(() => sheet.refresh())     // re-runs the render

refresh() re-applies a sheet from its current source (re-running the render, or re-reading a .css file). update(css) instead pins the sheet to a fixed string, dropping any render function.

A render function still lives in a module that a reload re-imports, so the same side-effect-free rule applies (below). When the dynamic CSS is owned by a stateful module that can't be re-imported safely — its add runs inside a method, or it owns a singleton with listeners — pass { watch: false } so it installs without being watched; you re-apply it yourself via refresh().

When styles install

The default display does not exist at module-init time, so styles added before the app activates are queued. Call styles.install() once from your activate handler to flush the queue (and start the file watcher). Styles added after the display exists install immediately — and the first such call auto-flushes the queue, so an app that does all its styling inside activate never strictly needs install().

priority defaults to Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION; pass { priority } to override it.

Hot-reload

Hot-reload runs only when NODE_ENV=development (and is silently off otherwise — in production nothing is watched). Apps created with node-gtk create set this for you in their npm run dev script. You can opt out with NODE_GTK_STYLE_HOT_RELOAD=0.

Every file that contributes styles is watched — via GLib's own GFileMonitor, not Node's fs.watch, so it's driven by the GTK main loop the app already runs (an fs.watch handle is silently never serviced once that loop is the only one running, and would keep the process alive after the window closes):

  • A .css file is re-read into its existing provider. GTK re-resolves the style cascade live — no flash, no restart. A malformed rule mid-edit is simply skipped by GTK (and logged), leaving the rest applied.

  • A source module that called styles.add() is re-imported with a cache-busting query, so its add() calls reinstall the new CSS; the providers from the previous run are then removed. New sheets go up before the old come down, so there's no unstyled flash. If the module fails to load mid-edit (e.g. a syntax error), it rolls back to the previously working sheets.

Caveat: keep reloadable inline CSS in a side-effect-free module

Reloading inline CSS re-executes the whole module it lives in. So put hot-reloadable styles.add() calls in a module whose top level only registers styles — never next to app.run() / window construction, or a reload would re-run all of that too. A good pattern:

// styles.ts — safe to re-run: it only registers styles
import { styles } from 'node-gtk/styles'
styles.add(`.headline { font-size: 20px; font-weight: bold; }`)
// main.ts — imports the styles module; never put reloadable CSS here
import './styles.ts'

See examples/style-manager.mjs for a runnable demo of both reload paths.

API

MemberDescription
styles.add(css, { priority?, watch? })Inline CSS, or a () => string render function; hot-reloads via module re-import. watch: false opts out of watching. Returns a handle.
styles.addFile(path, { priority?, watch? })A .css file (string, file:// URL, or URL); hot-reloads by re-reading. Idempotent per path. Returns a handle.
styles.install()Install queued styles and start the watcher.

The handle returned by add / addFile is { update(next), refresh(), remove() }. StyleManager (the class) and the shared styles instance are the only exports.