Overview
react-native-gtkx renders the react-native API as real GTK4 and libadwaita
widgets. Two dependencies do the heavy lifting: gtkx
is a React reconciler for GTK4 with an in-process FFI into libgtk (NAPI-RS),
and Yoga — the same flexbox engine React Native
itself uses — computes layout. This package is the layer between them: it
gives GTK widgets React Native's component shapes, drives their position and
size from Yoga, and turns the visual half of a React Native style into GTK
CSS.
The path from JSX to a window
A View, a Text, a Pressable are ordinary React function components —
this package has no reconciler of its own. Each one renders a gtkx host
element (a GtkBox, in View's case) that gtkx's own reconciler mounts as a
real GTK widget through the FFI: a widget call is a synchronous, in-process
call, with no serialization step between it and the code that made it. That
removes the "bridge tax" that shaped classic React Native's architecture —
there is no batched JSON channel to a separate UI thread to cross.
Position and size are a second, independent handoff. Every RN-shaped
container widget installs a custom Gtk.LayoutManager subclass, registered
from JS, whose measure() and allocate() vfuncs do nothing but ask this
package's own layout engine for a number and a set of child rectangles. GTK's
layout cycle is not synchronized with Yoga after the fact — for these
widgets, Yoga computes the layout GTK's allocation pass performs. The engine
that does this, the shadow tree it keeps, and the style split that feeds it
are the subject of Layout and styling.
Three subpaths beneath the portable surface
Everything reachable from plain "react-native" is portable. Underneath it,
three subpaths give you the platform itself, with nothing filtered out:
your app
├── react-native portable components
├── react-native-gtkx/navigation react-navigation adapter (optional)
├── react-native-gtkx/common what this package wrote itself
├── react-native-gtkx/adw libadwaita widgets, bound directly
└── react-native-gtkx/gtk GTK widgets, bound directly
Three rules make the rest of this page easy to reason about:
- The import says what you're opting into. Anything from
react-native-gtkx/gtkorreact-native-gtkx/adwis Linux-only, which shows up in review as a decision, not an accident. - A prefix tells you whose widget it is.
AdwHeaderBar,GtkButton,AdwNavigationView— that IS the widget, bound by gtkx. No prefix —NavigationStack,SlotContent,Widget— means this package wrote it. A wrapper of ours never makes the underlying widget unreachable. - None of this knows about react-navigation. No router is involved and
none is required.
react-native-gtkx/navigationis a thin adapter built on these primitives, the same way@react-navigation/native-stackis built onreact-native-screens— an app can skip the adapter entirely and drive anAdw.NavigationViewfrom its own state. See Window, navigation, and settings for that adapter and for everything the three subpaths expose beyond a single widget.
The widget surface: wrapped, raw, and auxiliary
Every Gtk.Widget and Adw.Widget subclass gtkx binds is exported — 86 GTK
widgets and 46 Adwaita widgets at present, from GtkBox and GtkButton to
GtkColumnView and AdwToolbarView. The list is generated, not hand-picked:
scripts/generate-widget-surface.ts classifies gtkx's full binding by real
GObject inheritance, and the classification is committed
(scripts/widget-surface/classification.json) so it stays exact between
gtkx upgrades — re-run the generator after one to pick up new widgets; it
diffs against its own previous output.
Most of that surface is wrapped: it keeps every prop gtkx binds and gains
style/onLayout, exactly like any other React Native component.
<GtkEntry
style={{ flex: 1 }}
placeholderText="Filter"
/>
<GtkButton
style={{ width: 72, backgroundColor: "#3584e4", borderRadius: 6 }}
label="Go"
/>
The entry flexes, the button takes its own width and color — the layout half of the style drives Yoga, the visual half becomes a GTK CSS class on the widget itself, so the button really is blue rather than a blue box sitting behind one.
Two families are exported raw instead, because a wrapper box around them would be invalid GTK rather than a convenience:
- Toplevels — everything implementing
GtkRoot:GtkWindowand everyGtk*Dialog,GtkApplicationWindow,GtkAssistant,GtkShortcutsWindowand their Adwaita counterparts (AdwWindow,AdwApplicationWindow,AdwAboutWindow,AdwMessageDialog,AdwPreferencesWindow), plusGtkDragIcon— which derivesGtk.Widgetdirectly and is a toplevel all the same, which is why the rule is written against theGtkRootcapability rather than againstGtk.Windowas one familiar instance of it.GtkPopoversits on the other side of that line — aGtkNativebut not aGtkRoot, parented withgtk_popover_set_parent— and stays wrapped. - Child-only widgets — valid solely as the direct child of one specific
parent.
GtkListBoxRowandGtkFlowBoxChild(plus everything deriving them — every Adwaita preferences row,AdwActionRowincluded) are caught mechanically, by inheritance.AdwNavigationPageandAdwPreferencesPagederiveGtk.Widgetdirectly with no shared base to catch them the same way, so they're a small, doc-verified denylist instead — seescripts/widget-surface/classify.ts.
Every raw export above is still exported, by name, from react-native-gtkx/gtk
or /adw, exactly as gtkx binds it — reach the widget with a ref where you
need one directly. GtkGestureClick is a third, simpler case: an event
controller, not a widget at all, so it was never a candidate for wrapping.
A further set of exports are not Gtk.Widget/Adw.Widget subclasses at all,
so the generator never sees them either: actions and menus (GSimpleAction,
GMenu), a responsive breakpoint (AdwBreakpoint, detailed in
Layout and styling), one option of an AdwToggleGroup
(AdwToggle), the two leaf elements an AdwShortcutsDialog is built from,
a text buffer and an adjustment (GtkTextBuffer, GtkAdjustment), keyboard
shortcuts (GtkShortcut, GtkShortcutController), and the two drag-and-drop
controllers (GtkDragSource, GtkDropTarget):
<GtkApplicationWindow
actions={
<GSimpleAction
name="new"
onActivate={onNew}
/>
}
breakpoints={
<AdwBreakpoint
condition={Adw.BreakpointCondition.parse("max-width: 500sp")}
onApply={() => setCollapsed(true)}
onUnapply={() => setCollapsed(false)}
/>
}
/>
react-native-gtkx/common: what has no upstream counterpart
Nothing in this subpath carries an Adw/Gtk prefix, because none of it is
a binding — it's the plumbing between the two worlds:
Icon— a named icon resolved against the desktop icon theme at paint time, not a bundled asset like RN'sImage. It recolors itself with the label color and follows the user's theme, which nothing inImage's contract can express, behind the same shape RN apps already use (<Icon name size />).SlotContent/IntrinsicContent— the boundary that lets React Native content live inside a GTK widget's slot or content area. Detailed in Layout and styling, which is where the boundary actually matters.Widget/wrapReactNative/useWidgetLayout— the reverse direction: giving a raw GTK widget a place in React Native layout. Also in Layout and styling.NavigationStack/NavigationStackPage— a declarative layer overAdw.NavigationView, which is imperative (push/pop/pop_to_tag) where React is not. Detailed in Window, navigation, and settings.
This platform does not re-implement Adwaita chrome in React Native — reach
for AdwActionRow and friends from react-native-gtkx/adw, inside a
GtkListBox with cssClasses={["boxed-list"]} (see the
Reference for the full row family). The style layer's
boxShadow, outline* and textDecorationLine properties exist precisely
so that an Adwaita-looking list stays expressible in a plain StyleSheet
when you do want to build one by hand — the frame is a three-part
box-shadow rather than a border, the focus ring an outline, which takes
no layout space. Drag-to-reorder goes through one module,
react-native-gtkx/dnd (see the Reference) — a Droppable around a
Draggable per row inside one DropProvider — rather than a bespoke
reorder prop.
Namespaces
Adw, Gdk, Gio, Gtk and Pango are exported as values from both
react-native-gtkx/gtk and /adw, because code needs both the runtime enums
and the types:
const scroller = <GtkScrolledWindow hscrollbarPolicy={Gtk.PolicyType.NEVER} />
const viewRef = useRef<Adw.NavigationView | null>(null)
Wrapping a widget this package hasn't caught up to yet
scripts/generate-widget-surface.ts covers every Gtk.Widget/Adw.Widget
subclass gtkx binds as of its last run, but a gtkx release can add a widget
before the generator has been re-run for it, and a non-widget GI class (an
event controller, a filter, an adjustment) was never a generator candidate in
the first place. wrapReactNative reaches either without waiting — it's
generic, so the widget's own prop types survive:
import { GtkPopover } from "@gtkx/jsx/gtk"
import { wrapReactNative } from "react-native-gtkx/common"
const Popover = wrapReactNative(GtkPopover)
// <Popover style={{ width: 240 }} autohide … /> — `autohide` still typed
That's the same mechanism the generated surface itself uses under the hood,
applied by hand. Two lower-level forms exist for cases even that doesn't fit:
<Widget style={…}> wraps an element already in hand, and
useWidgetLayout(ref, { style }) attaches layout to a widget whose ref you
already own, with no wrapper component at all.
The escape hatch
If something is still missing, reach the widget directly — every wrapper
here forwards its ref to the real GObject:
const viewRef = useRef<Adw.NavigationView | null>(null)
<NavigationStack ref={viewRef} stack={stack}>…</NavigationStack>
// viewRef.current is the real Adw.NavigationView
There is deliberately no wall. A missing convenience should cost one line, not a fork.
Measured numbers behind these decisions (the Yoga/GTK feasibility spike,
frame-budget studies, the navigation research) live in docs/research/ —
repo-only working notes, not published here. The standing gtkx upstream
agenda is docs/upstream-gtkx.md, and every workaround the bridge carries
is cataloged in docs/gtkx-1.2-notes.md.
Related
- Reference — the full component and API surface, GTK/Adw by badge.
- Layout and styling — the Yoga shadow tree, the layout/visual style split, and the two ways to react to a resize.
- Window, navigation, and settings —
NavigationStack, window actions and controllers,GSettings. - Gestures — the responder system and
PanResponderon GTK event controllers.