GTKX 1.0
August 4, 2026
GTKX 1.0 is available.
GTKX is the React framework for Linux. It creates GObject instances from JSX using generated GTK4 bindings.
GTKX runs React on Node.js and renders native widgets. Components describe the interface for the current state, React updates the widget tree, and Fast Refresh applies edits to the running app. Node APIs and compatible npm libraries are available alongside GTK and Gio.
This post describes GTKX 1.0. To follow the maintained 1.x documentation, scaffold a 1.x app:
npm create gtkx@1It needs Linux, Node.js 24 or later, and the GTK4 and GLib development packages. Getting Started covers the first run.
A whole application
This is the hello-world example with its entry point folded in:
import * as Gtk from "@gtkx/gi/gtk";
import { GtkApplication, GtkApplicationWindow, GtkBox, GtkButton, GtkLabel } from "@gtkx/jsx/gtk";
import { createRoot, quit } from "@gtkx/react";
import { useState } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
return (
<GtkApplicationWindow title="Hello GTKX" defaultWidth={400} defaultHeight={300} onCloseRequest={quit}>
<GtkBox
orientation={Gtk.Orientation.VERTICAL}
spacing={20}
marginTop={40}
marginBottom={40}
marginStart={40}
marginEnd={40}
valign={Gtk.Align.CENTER}
halign={Gtk.Align.CENTER}
>
<GtkLabel cssClasses={["title-1"]}>Welcome to GTKX!</GtkLabel>
<GtkLabel cssClasses={["title-2"]}>{`Count: ${String(count)}`}</GtkLabel>
<GtkButton
label="Increment"
onClicked={() => {
setCount((c) => c + 1);
}}
cssClasses={["suggested-action", "pill"]}
/>
</GtkBox>
</GtkApplicationWindow>
);
};
const App = () => (
<GtkApplication>
<Counter />
</GtkApplication>
);
createRoot().render(<App />);GTKX uses React 19, including hooks, context, Suspense, portals, and the React Compiler.
The app is an ordinary Node.js process. The Tasks app in the tutorial keeps state in zustand and writes it to disk with node:fs; Gio provides settings, notifications, and actions. Packages that require the browser DOM need adaptation. GTK and application JavaScript share one thread.

The Tasks app built in the tutorial.
GTKX targets Linux. It exposes native library APIs through generated bindings and GTKX integration code; it does not provide a renderer for other desktop platforms.
The elements are generated on your machine
The CLI reads the .gir files installed on your system and emits @gtkx/gi/<namespace> (typed classes, enums, and functions) and @gtkx/jsx/<namespace> (elements and their props) into node_modules/.gtkx. Neither is published to npm. You declare what you want bound:
import { defineConfig } from "@gtkx/config";
export default defineConfig({
libraries: ["Gtk-4.0", "Adw-1"],
applicationId: "com.gtkx.tutorial",
});Adding WebKit is one entry in that array, plus the development package:
libraries: ["Gtk-4.0", "Adw-1", "WebKit-6.0"],import { WebKitWebView } from "@gtkx/jsx/webkit" then resolves, with WebKit.WebView's full method set behind its ref; the browser example is that one entry grown into a small web browser.
Generated elements and props
Generated elements expose native properties and signals as JSX props. This row from the tutorial's task list uses a check button and drag-to-reorder controllers:
<AdwActionRow
title={title}
useMarkup
subtitle={formatDue(task.due) ?? undefined}
activatable
onActivated={() => openTask(task.id)}
prefix={
<GtkCheckButton
valign={Gtk.Align.CENTER}
active={task.done}
accessibleLabel="Mark complete"
onToggled={(self) => setDone(task.id, self.active)}
/>
}
controllers={
<>
<GtkDragSource
actions={Gdk.DragAction.MOVE}
onPrepare={(x, y, self) => {
const row = self.getWidget();
if (row) self.setIcon(Gtk.WidgetPaintable.new(row), Math.round(x), Math.round(y));
return Gdk.ContentProvider.newForValue(
GObject.buildValue(GObject.TYPE_STRING, (value) => value.setString(task.id)),
);
}}
/>
<GtkDropTarget
actions={Gdk.DragAction.MOVE}
types={[GObject.TYPE_STRING]}
onDrop={(value) => {
const draggedId = value.getString();
if (draggedId) reorder(draggedId, task.id);
return true;
}}
/>
</>
}
/>The row and controllers are declared in JSX. The synchronous drag handler creates the content provider it must return to GTK.
Where the generated surface stops
Property setting cannot express everything GTK4 does. Adding a child is insertChildAfter on a GtkBox and addTopBar on an AdwToolbarView; a scale's marks have no property at all. Behaviors cover those cases, and you can write your own: a behavior attaches to a GLib type, supplies the lifecycle hooks, and subtypes inherit it. GTK4's named-cursor API is a method with no property behind it, and teaching it to every widget is a behavior and a prop declaration:
// src/elements.ts
import type * as Gtk from "@gtkx/gi/gtk";
import { defineBehavior, defineElements } from "@gtkx/react/config";
export default defineElements({
GtkWidget: {
behaviors: [
defineBehavior<Gtk.Widget>({
update: (widget, prev, next) => {
if (!Object.is(prev.cursorName, next.cursorName) && typeof next.cursorName === "string") {
widget.setCursorFromName(next.cursorName);
}
return ["cursorName"];
},
}),
],
},
});declare module "@gtkx/jsx/gtk" {
interface GtkWidgetProps {
cursorName?: string | null | undefined;
}
}Point elements.behaviors at that module and every widget accepts cursorName. Your behaviors are consulted before the built-in ones, so you can override how GTKX already handles a prop.
Below JSX, registerClass from @gtkx/runtime turns a TypeScript class extending a generated wrapper into a GType of its own, with GObject properties and vfunc overrides that chain up through super. The subclassing guide covers it.
What GTKX still writes by hand
Some widgets carry an API that is too complex to be handled by element behaviors, so for those you use a component instead of the raw element. The collection views replace a model plus factories with data and a renderer, and keep selection in React state:
<ListView<Task>
items={tasks.map((task) => ({ id: task.id, value: task }))}
selectionMode={Gtk.SelectionMode.MULTIPLE}
selectedIds={selectedIds}
onSelectionChanged={setSelectedIds}
estimatedItemHeight={56}
renderItem={({ item }) => <GtkLabel halign={Gtk.Align.START}>{item.title}</GtkLabel>}
/>The collection views create widgets for visible items and recycle them as the user scrolls. They still retain the item data supplied by the application.
GridView, ColumnView, and DropDown get the same treatment. ListView and ColumnView also group items under section headers and expand nested ones into a tree; DropDown takes the section headers, and GridView stays flat. Cell recycling stays native, and React state inside a cell behaves normally. @gtkx/components also carries Adwaita's combo row and a toast provider.
Testing native widgets
@gtkx/testing queries the accessibility tree using a Testing Library-style API. Before interacting, userEvent checks that a widget is sensitive, mapped, allocated, and in an active window. If a condition is unmet, the test reports it:
it("adds a task from the entry row", async () => {
await render(<App />, { container: rootElement });
const entry = await screen.findByRole(Gtk.AccessibleRole.TEXT_BOX);
await userEvent.type(entry, "Book flights");
await userEvent.keyboard(entry, "{Enter}");
expect(await screen.findByRole(Gtk.AccessibleRole.LIST_ITEM, { name: "Book flights" })).toBeDefined();
});The drag and drop from the task row above is testable the same way, with userEvent.dragAndDrop(source, target, "t2"). @gtkx/vitest gives every worker its own headless Wayland compositor, session bus, and stub notification service, so test files cannot interfere through shared display state. The testing guide has the query and matcher surface.
The dev loop
gtkx dev provides Fast Refresh and restarts the app when a change is not patchable. gtkx build bundles to dist/bundle.mjs. React Compiler runs over your sources unless you turn it off.
Assets imported through #data/* are staged, compiled with glib-compile-resources, and registered at startup, so an import gives you a resource:// URI and changing the file re-registers the bundle without a restart. A GSettings schema is an import too, and its keys are typed:
import { useSetting } from "@gtkx/react";
import schema from "#data/com.gtkx.tutorial.gschema.xml";
import { type SortOrder, SortValue } from "../types.js";
export const useSortOrder = (): [SortOrder, (order: SortOrder) => void] => {
const [value, setValue] = useSetting(schema, "sort-order");
return [SortValue[value] as SortOrder, (order) => setValue(SortValue[order])];
};gtkx docs emits reference pages for the project's generated elements, including props, signal handlers, and ref methods. The @gtkx/mcp server exposes those pages to coding agents and provides tools to inspect and interact with the running app. The tutorial covers desktop entries and AppStream metadata, then Flatpak packaging and submission to Flathub.
What 1.0 means
The 1.0 release establishes the public APIs for generated elements, configuration, element behaviors, rendering, components, testing, styling, OpenGL, and subclassing. Later 1.x releases extend these APIs and bind additional GIR declarations. Check each release's upgrade notes for compatibility changes, including safety-related removals.
Coming from v0
Almost every import path moved. Widgets come from @gtkx/jsx/<namespace> instead of @gtkx/react, classes and enums from @gtkx/gi/<namespace> instead of @gtkx/ffi/<namespace>, and configuration from gtkx.config.ts instead of a package.json field. The minimum Node.js is 24. Mounting changed shape:
-import { render } from "@gtkx/react";
-import pkg from "../package.json" with { type: "json" };
+import { createRoot } from "@gtkx/react";
import { App } from "./app.js";
-render(<App />, pkg.gtkx.appId);
+createRoot().render(<App />);The application itself is now an element you render. The v1.0.0 release notes list the migrations, and Getting Started documents the maintained 1.x setup.
What's next
The original roadmap items have since shipped: gtkx deploy for application packaging in 1.1, @gtkx/animated for React Spring integration in 1.3, @gtkx/navigation for Adwaita and React Navigation in 1.4, and @gtkx/forms for React Hook Form integration in 1.5. Planned work is tracked in issues.
Thanks, and where to start
GTKX builds on GTK and the GNOME platform, GObject-Introspection, libffi, React and react-reconciler, and napi-rs. Thanks to their contributors and to everyone who tested and reported issues in the release candidates.
The guide covers the rest, and the tutorial builds a task manager through packaging and preparation for Flathub. Submission still requires a tested package and Flathub review; the time depends on the application and requested changes.
If you already have a GTK4 application, try porting one dialog and report problems. Questions go to GitHub Discussions; CONTRIBUTING.md explains how to contribute.