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
| Method | Use it for | Hot-reload |
|---|---|---|
styles.addFile(path) | a .css stylesheet | re-reads the file into its provider |
styles.add(css) | inline CSS in a source module | re-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
.cssfile 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 itsadd()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
| Member | Description |
|---|---|
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.