Declarative Widgets
GTKX provides declarative child components for various GTK widgets, allowing you to configure widget internals using JSX instead of imperative APIs.
Scale with Marks
Add marks to a GtkScale slider using the marks prop, and configure the adjustment with direct props:
import { GtkScale } from "@gtkx/react";
import * as Gtk from "@gtkx/ffi/gtk";
import { useState } from "react";
const VolumeSlider = () => {
const [volume, setVolume] = useState(50);
return (
<GtkScale
hexpand
value={volume}
lower={0}
upper={100}
stepIncrement={1}
pageIncrement={10}
onValueChanged={setVolume}
marks={[
{ value: 0, label: "0", position: Gtk.PositionType.BOTTOM },
{ value: 25, position: Gtk.PositionType.BOTTOM },
{ value: 50, label: "50", position: Gtk.PositionType.BOTTOM },
{ value: 75, position: Gtk.PositionType.BOTTOM },
{ value: 100, label: "100", position: Gtk.PositionType.BOTTOM },
]}
/>
);
};
Calendar with Marks
Mark specific days on a GtkCalendar using the markedDays prop:
import { GtkCalendar } from "@gtkx/react";
const EventCalendar = () => {
const today = new Date();
const eventDays = [5, 10, 15, 20, 25];
return (
<GtkCalendar
year={today.getFullYear()}
month={today.getMonth()}
day={today.getDate()}
markedDays={eventDays}
/>
);
};
LevelBar with Offsets
Define color thresholds on a GtkLevelBar using the offsets prop:
import { GtkLevelBar } from "@gtkx/react";
import { useState } from "react";
const BatteryIndicator = () => {
const [level, setLevel] = useState(0.6);
return (
<GtkLevelBar
value={level}
minValue={0}
maxValue={1}
offsets={[
{ id: "low", value: 0.25 },
{ id: "high", value: 0.75 },
{ id: "full", value: 1.0 },
]}
/>
);
};
The level bar changes color at each offset threshold: below 25% shows critical (red), 25-75% is normal, and above 75% shows high (green).
ToggleGroup with Toggles
Create toggle button groups using AdwToggleGroup with x.Toggle children:
import { x, AdwToggleGroup } from "@gtkx/react";
<AdwToggleGroup activeName="list">
<x.Toggle id="list" iconName="view-list-symbolic" tooltip="List View" />
<x.Toggle id="grid" iconName="view-grid-symbolic" tooltip="Grid View" />
<x.Toggle id="flow" iconName="view-continuous-symbolic" tooltip="Flow View" />
</AdwToggleGroup>
Use onActiveChanged to respond to selection changes:
import { x, AdwToggleGroup } from "@gtkx/react";
import { useState } from "react";
const ViewModeSelector = () => {
const [mode, setMode] = useState("list");
return (
<AdwToggleGroup
activeName={mode}
onActiveChanged={(_index, name) => setMode(name ?? "list")}
>
<x.Toggle id="list" iconName="view-list-symbolic" />
<x.Toggle id="grid" iconName="view-grid-symbolic" />
</AdwToggleGroup>
);
};
Text labels work too:
<AdwToggleGroup>
<x.Toggle id="day" label="Day" />
<x.Toggle id="week" label="Week" />
<x.Toggle id="month" label="Month" />
</AdwToggleGroup>
ExpanderRow Children
Use x.ExpanderRowRow and x.ExpanderRowAction for declarative expander row content:
import {
x,
AdwExpanderRow,
AdwActionRow,
GtkListBox,
GtkButton,
} from "@gtkx/react";
const SettingsExpander = () => (
<GtkListBox cssClasses={["boxed-list"]}>
<AdwExpanderRow
title="Advanced Settings"
subtitle="Additional configuration options"
>
<x.ExpanderRowAction>
<GtkButton iconName="emblem-system-symbolic" cssClasses={["flat"]} />
</x.ExpanderRowAction>
<x.ExpanderRowRow>
<AdwActionRow title="Option 1" />
<AdwActionRow title="Option 2" />
<AdwActionRow title="Option 3" />
</x.ExpanderRowRow>
</AdwExpanderRow>
</GtkListBox>
);
x.ExpanderRowAction places a widget in the expander row header (next to the expand arrow), while x.ExpanderRowRow contains the rows that appear when expanded.
Grid Layout
Position children in a GtkGrid using x.GridChild:
import { x, GtkGrid, GtkLabel, GtkEntry, GtkButton } from "@gtkx/react";
import * as Gtk from "@gtkx/ffi/gtk";
const FormGrid = () => (
<GtkGrid rowSpacing={8} columnSpacing={12}>
<x.GridChild column={0} row={0}>
<GtkLabel label="Name:" halign={Gtk.Align.END} />
</x.GridChild>
<x.GridChild column={1} row={0}>
<GtkEntry hexpand />
</x.GridChild>
<x.GridChild column={0} row={1}>
<GtkLabel label="Email:" halign={Gtk.Align.END} />
</x.GridChild>
<x.GridChild column={1} row={1}>
<GtkEntry hexpand />
</x.GridChild>
<x.GridChild column={0} row={2} columnSpan={2}>
<GtkButton label="Submit" halign={Gtk.Align.END} />
</x.GridChild>
</GtkGrid>
);
Fixed Positioning
Position children absolutely in a GtkFixed using x.FixedChild:
import { x, GtkFixed, GtkLabel } from "@gtkx/react";
const AbsoluteLayout = () => (
<GtkFixed>
<x.FixedChild x={20} y={30}>
<GtkLabel label="Top Left" />
</x.FixedChild>
<x.FixedChild x={200} y={100}>
<GtkLabel label="Middle" />
</x.FixedChild>
</GtkFixed>
);
Overlay Children
Layer widgets on top of each other using x.OverlayChild. You can include multiple children in a single overlay:
import { x, GtkOverlay, GtkImage, GtkLabel } from "@gtkx/react";
import * as Gtk from "@gtkx/ffi/gtk";
const BadgedImage = () => (
<GtkOverlay>
<GtkImage iconName="folder-symbolic" pixelSize={48} />
<x.OverlayChild>
<GtkLabel
label="3"
cssClasses={["badge"]}
halign={Gtk.Align.END}
valign={Gtk.Align.START}
/>
<GtkLabel
label="New"
cssClasses={["badge"]}
halign={Gtk.Align.START}
valign={Gtk.Align.END}
/>
</x.OverlayChild>
</GtkOverlay>
);
Notebook Pages
Create tabbed interfaces with x.NotebookPage and optional custom tab widgets:
import { x, GtkNotebook, GtkBox, GtkImage, GtkLabel } from "@gtkx/react";
import * as Gtk from "@gtkx/ffi/gtk";
const TabbedView = () => (
<GtkNotebook>
<x.NotebookPage label="Documents">
<GtkLabel label="Documents content" vexpand />
</x.NotebookPage>
<x.NotebookPage tabExpand tabFill>
<x.NotebookPageTab>
<GtkBox spacing={4}>
<GtkImage iconName="folder-symbolic" />
<GtkLabel label="Files" />
</GtkBox>
</x.NotebookPageTab>
<GtkLabel label="Files content" vexpand />
</x.NotebookPage>
</GtkNotebook>
);
Use x.NotebookPageTab for a custom widget as the tab label instead of text.
Drag and Drop
All widgets support drag-and-drop through props. Use onDragPrepare, onDragBegin, and onDragEnd to make a widget draggable, and dropTypes, onDrop, onDropEnter, and onDropLeave to accept drops.
import * as Gdk from "@gtkx/ffi/gdk";
import { Type, Value } from "@gtkx/ffi/gobject";
import { GtkButton, GtkBox, GtkLabel } from "@gtkx/react";
import { useState } from "react";
const DraggableButton = ({ label }: { label: string }) => {
return (
<GtkButton
label={label}
onDragPrepare={() =>
Gdk.ContentProvider.newForValue(Value.newFromString(label))
}
dragIcon={Gdk.Texture.newFromFilename("/path/to/icon.png")}
dragIconHotX={16}
dragIconHotY={16}
/>
);
};
const DropZone = () => {
const [dropped, setDropped] = useState<string | null>(null);
return (
<GtkBox
dropTypes={[Type.STRING]}
onDrop={(value: Value) => {
setDropped(value.getString());
return true;
}}
>
<GtkLabel label={dropped ?? "Drop here"} />
</GtkBox>
);
};
For a complete example with visual feedback, see the drag-and-drop demo in examples/gtk-demo/src/demos/gestures/dnd.tsx.
Custom Drawing
Render custom graphics using GtkDrawingArea with the onDraw callback. The callback receives a Cairo context for 2D drawing:
import { GtkDrawingArea } from "@gtkx/react";
import type { Context } from "@gtkx/ffi/cairo";
import * as Gtk from "@gtkx/ffi/gtk";
const CustomCanvas = () => {
const handleDraw = (
self: Gtk.DrawingArea,
cr: Context,
width: number,
height: number,
) => {
cr.setSourceRgb(0.2, 0.4, 0.8);
cr.rectangle(10, 10, width - 20, height - 20);
cr.fill();
cr.setSourceRgb(1, 1, 1);
cr.moveTo(width / 2, 20);
cr.lineTo(width - 20, height - 20);
cr.lineTo(20, height - 20);
cr.closePath();
cr.fill();
};
return (
<GtkDrawingArea
contentWidth={400}
contentHeight={300}
onDraw={handleDraw}
/>
);
};
Interactive Drawing
Combine onDraw with a GtkGestureDrag controller for interactive applications like paint programs:
import { GtkDrawingArea, GtkGestureDrag } from "@gtkx/react";
import type { Context } from "@gtkx/ffi/cairo";
import * as Gtk from "@gtkx/ffi/gtk";
import { useRef, useState } from "react";
interface Point {
x: number;
y: number;
}
const PaintCanvas = () => {
const ref = useRef<Gtk.DrawingArea | null>(null);
const [points, setPoints] = useState<Point[]>([]);
const startRef = useRef<Point | null>(null);
const handleDraw = (
self: Gtk.DrawingArea,
cr: Context,
width: number,
height: number,
) => {
cr.setSourceRgb(1, 1, 1);
cr.rectangle(0, 0, width, height);
cr.fill();
if (points.length > 1) {
cr.setSourceRgb(0, 0, 0);
cr.setLineWidth(2);
cr.moveTo(points[0].x, points[0].y);
for (const point of points.slice(1)) {
cr.lineTo(point.x, point.y);
}
cr.stroke();
}
};
return (
<GtkDrawingArea
ref={ref}
contentWidth={400}
contentHeight={300}
onDraw={handleDraw}
>
<GtkGestureDrag
onDragBegin={(startX, startY) => {
startRef.current = { x: startX, y: startY };
setPoints([{ x: startX, y: startY }]);
}}
onDragUpdate={(offsetX, offsetY) => {
if (startRef.current) {
const x = startRef.current.x + offsetX;
const y = startRef.current.y + offsetY;
setPoints((prev) => [...prev, { x, y }]);
ref.current?.queueDraw();
}
}}
onDragEnd={() => {
startRef.current = null;
}}
/>
</GtkDrawingArea>
);
};
Call widget.queueDraw() to request a redraw when state changes outside of React's render cycle.
For a complete painting application with colors and brush sizes, see examples/gtk-demo/src/demos/drawing/paint.tsx.
TextView with Rich Text
Configure a GtkTextView with rich text content using x.TextTag children for formatting and x.TextAnchor for embedded widgets.
Basic Usage
import { x, GtkTextView, GtkScrolledWindow } from "@gtkx/react";
import * as Gtk from "@gtkx/ffi/gtk";
const TextEditor = () => {
return (
<GtkScrolledWindow minContentHeight={200}>
<GtkTextView
wrapMode={Gtk.WrapMode.WORD_CHAR}
enableUndo
onBufferChanged={(text) => console.log(text)}
>
Hello, World!
</GtkTextView>
</GtkScrolledWindow>
);
};
When enableUndo is true, the built-in keyboard shortcuts Ctrl+Z (undo) and Ctrl+Shift+Z (redo) are automatically available.
Rich Text with TextTag
Use x.TextTag to apply formatting to portions of text. Tags can be nested for combined styling:
import { x, GtkTextView, GtkScrolledWindow } from "@gtkx/react";
import * as Pango from "@gtkx/ffi/pango";
import * as Gtk from "@gtkx/ffi/gtk";
const RichTextEditor = () => {
return (
<GtkScrolledWindow minContentHeight={200}>
<GtkTextView wrapMode={Gtk.WrapMode.WORD_CHAR}>
Normal text,{" "}
<x.TextTag id="bold" weight={Pango.Weight.BOLD}>
bold text
</x.TextTag>
,{" "}
<x.TextTag id="italic" style={Pango.Style.ITALIC}>
italic text
</x.TextTag>
, and{" "}
<x.TextTag id="colored" foreground="red">
<x.TextTag id="underlined" underline={Pango.Underline.SINGLE}>
nested red underlined
</x.TextTag>
</x.TextTag>{" "}
text.
</GtkTextView>
</GtkScrolledWindow>
);
};
Embedded Widgets with TextAnchor
Use x.TextAnchor to embed widgets inline with text content:
import { x, GtkTextView, GtkScrolledWindow, GtkButton } from "@gtkx/react";
const TextWithWidgets = () => {
return (
<GtkScrolledWindow minContentHeight={200}>
<GtkTextView>
Click here:{" "}
<x.TextAnchor>
<GtkButton
label="Click me"
onClicked={() => console.log("Clicked!")}
/>
</x.TextAnchor>{" "}
to continue.
</GtkTextView>
</GtkScrolledWindow>
);
};
Inline Images with TextPaintable
Use x.TextPaintable to embed inline images or icons in text:
import { x, GtkTextView, GtkScrolledWindow } from "@gtkx/react";
import * as Gtk from "@gtkx/ffi/gtk";
const TextWithIcons = () => {
const iconTheme = Gtk.IconTheme.getForDisplay(Gdk.Display.getDefault()!);
const icon = iconTheme.lookupIcon("starred-symbolic", null, 16, 1, Gtk.TextDirection.LTR, Gtk.IconLookupFlags.NONE);
return (
<GtkScrolledWindow minContentHeight={200}>
<GtkTextView>
This is a <x.TextPaintable paintable={icon} /> star icon inline with text.
</GtkTextView>
</GtkScrolledWindow>
);
};
SourceView for Code Editing
Configure a GtkSourceView for syntax-highlighted code editing:
import { GtkSourceView, GtkScrolledWindow } from "@gtkx/react";
import * as Gtk from "@gtkx/ffi/gtk";
import { useState } from "react";
const CodeEditor = () => {
const [code, setCode] = useState('console.log("Hello, World!");');
return (
<GtkScrolledWindow minContentHeight={300}>
<GtkSourceView
showLineNumbers
highlightCurrentLine
tabWidth={4}
indentWidth={4}
autoIndent
language="typescript"
styleScheme="Adwaita-dark"
highlightSyntax
highlightMatchingBrackets
enableUndo
onBufferChanged={setCode}
>
{code}
</GtkSourceView>
</GtkScrolledWindow>
);
};
Keyboard Shortcuts
Attach keyboard shortcuts to widgets using x.ShortcutController with x.Shortcut children:
import { x, GtkBox, GtkLabel } from "@gtkx/react";
import * as Gtk from "@gtkx/ffi/gtk";
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
const [searchMode, setSearchMode] = useState(false);
return (
<GtkBox orientation={Gtk.Orientation.VERTICAL} spacing={12} focusable>
<x.ShortcutController scope={Gtk.ShortcutScope.LOCAL}>
<x.Shortcut
trigger="<Control>equal"
onActivate={() => setCount((c) => c + 1)}
/>
<x.Shortcut
trigger="<Control>minus"
onActivate={() => setCount((c) => c - 1)}
/>
<x.Shortcut
trigger="<Control>f"
onActivate={() => setSearchMode((s) => !s)}
/>
</x.ShortcutController>
<GtkLabel label={`Count: ${count}`} />
<GtkLabel label={searchMode ? "Search mode ON" : "Search mode OFF"} />
</GtkBox>
);
};
The scope prop controls when shortcuts are active: LOCAL (only when focused), MANAGED (managed by parent), or GLOBAL (anywhere in window).
The trigger prop accepts GTK accelerator strings (e.g., "<Control>s", "<Alt>F4"). Pass an array for multiple triggers:
<x.Shortcut trigger={["F5", "<Control>r"]} onActivate={refresh} />
Use disabled to temporarily disable a shortcut:
<x.Shortcut trigger="<Control>s" onActivate={save} disabled={!hasChanges} />
SearchBar
GtkSearchBar provides a search interface with controlled search mode state:
import { GtkSearchBar, GtkSearchEntry, GtkBox, GtkToggleButton } from "@gtkx/react";
import * as Gtk from "@gtkx/ffi/gtk";
import { useState } from "react";
const SearchExample = () => {
const [searchActive, setSearchActive] = useState(false);
const [query, setQuery] = useState("");
return (
<GtkBox orientation={Gtk.Orientation.VERTICAL}>
<GtkToggleButton
label="Search"
active={searchActive}
onToggled={() => setSearchActive(!searchActive)}
/>
<GtkSearchBar
searchModeEnabled={searchActive}
onSearchModeChanged={setSearchActive}
>
<GtkSearchEntry
text={query}
onSearchChanged={(entry) => setQuery(entry.getText())}
/>
</GtkSearchBar>
</GtkBox>
);
};
The onSearchModeChanged callback is called when the search mode changes, whether from keyboard shortcuts (Escape to close) or programmatically.
Alert Dialog Responses
Create alert dialogs with AdwAlertDialog and x.AlertDialogResponse children:
import { x, AdwAlertDialog, GtkButton } from "@gtkx/react";
import { useState } from "react";
import * as Adw from "@gtkx/ffi/adw";
const DeleteConfirmation = () => {
const [showDialog, setShowDialog] = useState(false);
return (
<>
<GtkButton label="Delete" onClicked={() => setShowDialog(true)} />
{showDialog && (
<AdwAlertDialog
heading="Delete File?"
body="This action cannot be undone."
onResponse={(id) => {
if (id === "delete") {
console.log("Deleting...");
}
setShowDialog(false);
}}
>
<x.AlertDialogResponse id="cancel" label="Cancel" />
<x.AlertDialogResponse
id="delete"
label="Delete"
appearance={Adw.ResponseAppearance.DESTRUCTIVE}
/>
</AdwAlertDialog>
)}
</>
);
};
Color and Font Dialog Buttons
Color Dialog Button
import { GtkColorDialogButton } from "@gtkx/react";
import * as Gdk from "@gtkx/ffi/gdk";
import { useState } from "react";
const ColorPicker = () => {
const [color, setColor] = useState(new Gdk.RGBA({ red: 1, green: 0, blue: 0, alpha: 1 }));
return (
<GtkColorDialogButton
rgba={color}
onRgbaChanged={setColor}
title="Select Color"
modal
withAlpha
/>
);
};
Font Dialog Button
import { GtkFontDialogButton } from "@gtkx/react";
import * as Pango from "@gtkx/ffi/pango";
import { useState } from "react";
const FontPicker = () => {
const [font, setFont] = useState(Pango.FontDescription.fromString("Sans 12"));
return (
<GtkFontDialogButton
fontDesc={font}
onFontDescChanged={setFont}
title="Select Font"
modal
useFont
useSize
/>
);
};