Configuration and Codegen
Codegen is driven from gtkx.config.ts, which declares which libraries to generate bindings for, and your application ID.
The config file
defineConfig from @gtkx/config types your config for editor completion and validates it when the CLI loads the file:
import { defineConfig } from "@gtkx/config";
export default defineConfig({
libraries: ["Gtk-4.0", "Adw-1"],
applicationId: "com.gtkx.tutorial",
});For sharing a base config across packages, mergeConfig(base, override) deep-merges configs with the override winning on conflict. The config is loaded per mode, so a $development or $production block layers over the top-level values when gtkx dev or gtkx build reads the file.
Option reference
defineConfig accepts:
applicationId is the only required option. It must be a valid g_application_id_is_valid ID, reverse-DNS form such as org.example.MyApp. It identifies your app to D-Bus and GNOME, and it flows into your component tree automatically (see How applicationId flows below).
libraries lists GObject-Introspection namespaces in Name-Version form, such as "Gtk-4.0", "Adw-1", or "GtkSource-5". Omitting it gives you ["Gtk-4.0"]; naming libraries explicitly prepends Gtk-4.0 unless your list already contains a Gtk- entry, because the GTK4 bindings are the foundation everything else builds on. The wildcard "*" discovers every .gir file on the search path and binds the highest version of each namespace, which pulls in the entire installed platform at once.
girPath adds directories to the front of the .gir search path. You only need this when your GIR files live somewhere nonstandard, such as a locally built GTK4.
reactCompiler controls the React Compiler, which is enabled by default. Set it to false to disable it, or pass { compilationMode, panicThreshold } to tune it.
elementProps layers your own element prop rules on top of the built-in set, covered in Customizing elements with elementProps below. The rule shapes are typed in the @gtkx/config reference.
codegen controls binding generation, which is on by default. Set it to false for a project that already has a binding store installed, such as an example inside a workspace that shares the store built at the root: the CLI then resolves that installed store instead of generating its own. A project with generation turned off has no GIR data of its own, so gtkx docs has nothing to document there.
What codegen emits
Codegen writes packages into node_modules/.gtkx and links them into node_modules/@gtkx so imports resolve without either package appearing in your package.json:
@gtkx/gi(innode_modules/.gtkx/gi) holds the raw introspected API: one lowercased directory per namespace, exposed as subpath exports such as@gtkx/gi/gtkand@gtkx/gi/adw. These are the classes, enums, and functions you use imperatively, for refs,Gtk.Orientation.VERTICAL, and direct method calls. Reach for them for what only the GNOME platform provides, and use the Node standard library for everything else. The generated TypeScript is type-checked and compiled to JavaScript plus.d.tsinside the store, which codegen builds in a temporary directory and then renames into place, so a crash never leaves half a package.@gtkx/jsx(innode_modules/.gtkx/jsx) holds the React layer: per-namespace modules exporting one PascalCase component per widget (GtkButton,AdwHeaderBar), aPropsinterface for each, and a globalReact.JSX.IntrinsicElementsaugmentation so the elements type-check. It also emits ametadatamodule recording, per element, the signal-handler-to-signal-name map, the construct-only and constructable prop sets, the GIR default values, and the merged element prop rules. The reconciler reads that metadata at runtime through thevirtual:gtkx-configmodule the CLI's Vite plugin serves. The same pass that generates your types generates that metadata, so your types and runtime prop application cannot drift.
Both stores are generated code standing on a hand-written floor: @gtkx/runtime, the dependency the scaffolder installs for you. It owns the type descriptors behind every FFI call, GValue marshalling, signal connection, and the registry that maps a native handle back to its JavaScript object. It is also where you reach when generated bindings are not enough, chiefly registerClass for defining a GObject subclass of your own. Its surface is in the @gtkx/runtime reference.
Alongside the stores, the CLI writes node_modules/.gtkx/env.d.ts with a typed module declaration for every .gschema.xml file under your data directory (the folder your package.json imports field maps #data/* to, data/ in a scaffolded project), keyed by its #data/... import specifier. Each key's GVariant type maps to a natural TypeScript type:
| GVariant type | TypeScript type |
|---|---|
b | boolean |
i, u, x, t, d | number |
s | string |
as | string[] |
| enum key | string-literal union of its nicks |
| flags key | array of that union |
string key with <choices> | union of those choices |
| anything else | GLib.Variant |
Each schema exports a typed const carrying its id and a keys map; a schema declared without a path (a relocatable schema) additionally gets an at(path) method that returns the same typed reference bound to a concrete path. gtkx codegen, gtkx dev, and gtkx build all keep this file in sync.
How applicationId flows
The applicationId you set in config reaches your running app through the build. The CLI's Vite plugin serves a virtual:gtkx-config module that re-exports the jsx metadata together with your resolved applicationId. In @gtkx/react, the application component factory imports that value and uses it as the default for the applicationId prop, and codegen wraps every element descending from GtkApplication in that factory. Both <GtkApplication> and <AdwApplication> therefore carry your configured ID without you passing it, the wrapper runs the application when it mounts and quits it on unmount, and it provides the application instance to the tree via context (retrievable with useApplication). Passing an explicit applicationId prop overrides the default, which is how a test or a secondary tool can run under a different identity.
The ID also anchors the rest of your app's platform identity. GSettings schemas conventionally use it as their schema ID (the tutorial's schema is com.gtkx.tutorial in com.gtkx.tutorial.gschema.xml), desktop notifications and D-Bus activation key off it, and the CLI derives your GResource prefix from it by replacing dots with slashes.
Staleness and regeneration
You rarely run gtkx codegen by hand, because gtkx dev and gtkx build check freshness first and regenerate only when something changed. The check has these layers:
- Structural: if a store directory or its link is missing, a namespace barrel for one of your libraries is absent, or the jsx store lacks its generated modules, the bindings are stale regardless of content.
- Fingerprint: the gi store carries a
.codegen-fingerprint.jsonsentinel holding a SHA-256 hash over the codegen package version, yourelementProps(serialized), the sorted library list, and the path and full contents of every.girfile that fed the last run. On each check the hash is recomputed against the recorded GIR files; any mismatch, including a system GTK4 upgrade that rewrote a.gir, triggers regeneration. A changed library list is stale by definition.
While gtkx dev runs, it also watches gtkx.config.ts. Saving a change regenerates the bindings and restarts the dev runner for you, so adding a library or an element prop rule takes effect without stopping gtkx dev yourself. If codegen fails, the current runner keeps going and the error is printed; fix it and save again.
To force a clean rebuild, run:
gtkx codegen --forceThis deletes both stores and their links before regenerating, which is the right lever when you suspect the store is corrupt rather than stale.
The JSX prop model
Every GIR class whose ancestry reaches GObject becomes an intrinsic element, and its props interface is assembled from the GIR according to a small set of rules:
- Properties become camelCase props. Every introspectable property that is writable, construct, or construct-only becomes an optional prop under its camelCase name: GIR's
show-title-buttonsisshowTitleButtons. Property and signal documentation from the GIR is carried onto the generated props as JSDoc, so hovering a prop in your editor shows the upstream documentation. - Every property gets a notify handler. Each introspectable property gets an
onNotifyXprop whose handler receives(value, self), including read-only ones, except writable object-valued (element-accepting) properties, which expose the value as a child element prop instead. This is how you observe properties GTK4 changes on its own, such as a window'sdefaultWidth. - Object-typed props also accept elements. A writable, non-construct-only property whose type is itself a GObject class accepts a
ReactElementin addition to an instance, so you can writesidebar={<AdwNavigationPage ... />}and let the reconciler construct and manage the child. The one exception ischildon widgets with asetChildmethod, where JSXchildrenalready covers the element case. - Signals become
onhandlers. Every signal becomesonplus the UpperCamelCase signal name (clickedbecomesonClicked,row-activatedbecomesonRowActivated), and the handler receives the signal's parameters followed byself, the widget instance, with parameter types rendered from the GIR. refyields the native instance. Every element acceptsref?: Ref<Self | null>, whereSelfis the@gtkx/giclass (Gtk.Button,Adw.ToastOverlay). This is your escape hatch to the full imperative API.
Generating element reference docs
The same pipeline that generates the bindings can document them. gtkx docs loads the GIR data for your configured libraries, applies your elementProps rules, and writes one markdown page per JSX element:
gtkx docsBy default the pages land in docs/reference, one directory per namespace plus index pages, with cross-page links rooted at /reference so the output drops straight into a static site generator or anything else that renders markdown. Each element page carries the widget's upstream documentation, its hierarchy, and its children and slot rules. It then documents the element's own props with their types and defaults, its own signal handlers with their exact signatures, and its own methods reachable through ref. Members inherited from an ancestor are documented on that ancestor's page, which the hierarchy links to. A manifest.json alongside the pages records the namespace and element lists, which is what you want for generating a sidebar.
Run gtkx docs --help if you need the pages somewhere else or their links rooted elsewhere. Because your elementProps feed the generator, custom rules like the cursorName value prop in Customizing elements with elementProps appear in the generated pages too.
Advanced: Customizing elements with elementProps
Property setting alone cannot express everything GTK4 does. Adding a child is append on a GtkBox but addTopBar on an AdwToolbarView; a GtkScale's marks have no property at all, only addMark and clearMarks. GTKX bridges this with element prop rules: small declarative records that tell the reconciler which method calls realize a given JSX prop. A large built-in set covers GTK4 and Adwaita (containers for many types, controllers, actions, breakpoints, controlled text on GtkEditable, and more), and elementProps in your config layers your own rules on top. The kinds are:
container: children held underprop(usuallychildren) of GObject typechild, attached withappend, detached withremove, optionally supportinginsertandreorder, wrapping each child in anautowrapwidget type, or adopting children the widget creates itself viaadopt. At least one ofappendorremoveis required.value: a scalar prop applied by invokingcallwhenever it changes, optionally followed byafter. The built-inGtkDrawingArearule is{ kind: "value", prop: "drawFunc", call: "setDrawFunc", after: "queueDraw" }.controlled-text: a text property kept in controlled-input sync with the user's edits, as the built-inGtkEditablerule does fortext.lazy: a property applied after construction rather than during it, optionally deferred until alookupmethod succeeds;GtkStack'svisibleChildNamewaits forgetChildByNameto find the named child.list: an array prop mapped to per-item calls:addper item, plus optionalremoveandclear.GtkScale'smarksusesaddMarkandclearMarks.
User rules go through the same machinery. GTK4's named-cursor API is a method with no property behind it: setCursorFromName("pointer") shows the pointer cursor while hovering a widget, whereas the cursor property takes a Gdk.Cursor object. One config entry turns the method into a prop:
import { defineConfig } from "@gtkx/config";
export default defineConfig({
libraries: ["Gtk-4.0", "Adw-1"],
applicationId: "com.gtkx.tutorial",
elementProps: {
GtkWidget: [{ kind: "value", prop: "cursorName", call: "setCursorFromName" }],
},
});After the next codegen, every widget element accepts a cursorName prop, typed string | null straight from the method's parameter, and the reconciler calls setCursorFromName whenever the value changes. A rule declared on a type covers every element descending from it, which is how the built-in controllers rule on GtkWidget reaches all widgets.
Next
With the codegen pipeline in hand, continue to Async Operations.