Skip to content

Controls

Two GTK controls stand on their own rather than inside a boxed list: a single-line entry for typed text, and a drop down that picks one value from a list. Libadwaita ships neither. Each has a boxed-list twin — Adw.EntryRow and Adw.ComboRow — under Boxed Lists, and which one you want is a question about the surrounding layout.

The two booleans sit here too: the checkbox with its third, half-made state, and the light switch whose knob and trough are deliberately two different things. A boolean in a row is a row widget instead — Adw.SwitchRow, Adw.ActionRow.

What the preview does not show

The previews run the browser port, so behaviour the toolkits drive natively can be simplified here. Four divergences are worth knowing: <gtk-entry> has no editable, so a field you may read but not change is spelled disabled, which also greys it; the NativeScript drop down opens the platform action sheet, which has no search field; <gtk-check-button> spells GTK’s active and inconsistent as checked and indeterminate, the two words the stylesheet selects on; and the checkbox clears its half-made state on a click, which is the HTML rule rather than GTK’s.

A single-line text field. Give it a placeholder to say what belongs in it while it is empty, a value for the text itself, and disabled when it should be read-only. For a field that belongs in a preferences group, use Entry Row instead: the same control in row presentation, with the label as the row title.

Gtk.Entry
Blueprint
Code
using Gtk 4.0;
Gtk.Entry {
placeholder-text: _("Search files…");
width-request: 280;
halign: center;
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
const entry = new Gtk.Entry({
placeholderText: 'Search files…',
widthRequest: 280,
halign: Gtk.Align.CENTER,
});
entry.text = 'notes.md';
entry.editable = false; // read but do not change
Web Components
HTML
<gtk-entry
placeholder-text="Search files…"
width-request="280"
style="min-width: 280px;"
></gtk-entry>
NativeScript
XML
<gtk:Entry
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:gtk="~/gtk"
placeholderText="Search files…"
/>
Solid
TypeScript
// mount(() => <GtkEntry />, container) — from '@gjsify/gtk-host/solid'
const GtkEntry = () => (
<gtk-entry
placeholderText="Search files…"
widthRequest={280}
/>
);
Vue
Vue
<!-- GtkEntry.vue — mount(GtkEntry, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-entry
placeholder-text="Search files…"
:width-request="280"
/>
</template>
React
TypeScript
// createRoot(container).render(<GtkEntry />) — from '@gjsify/gtk-host/react'
const GtkEntry = () => (
<gtk-entry
placeholderText="Search files…"
widthRequest={280}
/>
);

Pick one value from a list. selected is the zero-based index of the chosen option, and enable-search adds a filter field for a long list. For the same choice presented as a boxed-list row, use Combo Row.

The model property

The list is the model property — an array of plain strings, or of { value, label } objects — and it is the same array the combo row takes, so a model written for one works on the other unchanged. Assign a new one at any time: the popover follows, and the rows that did not change keep their place.

Gtk.DropDown
Blueprint
Code
using Gtk 4.0;
Gtk.DropDown {
selected: 0;
halign: center;
model: Gtk.StringList {
strings [_("Automatic"), _("Always"), _("Never"), _("When busy")]
};
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
const dropDown = new Gtk.DropDown({
model: new Gtk.StringList({ strings: ['Automatic', 'Always', 'Never', 'When busy'] }),
selected: 0,
});
// Without an expression the search field has nothing to match against.
dropDown.expression = Gtk.PropertyExpression.new(Gtk.StringObject.$gtype, null, 'string');
dropDown.enableSearch = false;
dropDown.halign = Gtk.Align.CENTER;
Web Components
HTML
<!-- `model` is a JSON array of strings, or of { label, value } objects. -->
<gtk-drop-down model='["Automatic","Always","Never","When busy"]' selected="0"></gtk-drop-down>
Solid
TypeScript
// mount(() => <GtkDropDown />, container) — from '@gjsify/gtk-host/solid'
const GtkDropDown = () => (
<gtk-drop-down
model={['Automatic', 'Always', 'Never', 'When busy']}
selected={0}
halign="center"
/>
);
Vue
Vue
<!-- GtkDropDown.vue — mount(GtkDropDown, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-drop-down
:model="['Automatic', 'Always', 'Never', 'When busy']"
:selected="0"
halign="center"
/>
</template>
React
TypeScript
// createRoot(container).render(<GtkDropDown />) — from '@gjsify/gtk-host/react'
const GtkDropDown = () => (
<gtk-drop-down
model={['Automatic', 'Always', 'Never', 'When busy']}
selected={0}
halign="center"
/>
);

A label beside an indicator you can tick. On top of on and off it has a third state, indeterminate, for a selection that is only partly made — some of a range is affected — and it outranks on, exactly as it does for assistive technology. The grouped, single-choice form of a check button is a radio button; the browser port spells that Adw.Radio.

Gtk.CheckButton
Blueprint
Code
using Gtk 4.0;
Gtk.Box {
orientation: vertical;
spacing: 8;
Gtk.CheckButton {
label: _("Enable networking");
active: true;
}
Gtk.CheckButton {
label: _("Include hidden files");
}
Gtk.CheckButton {
label: _("Some of the selection");
inconsistent: true;
}
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
const networking = new Gtk.CheckButton({ label: 'Enable networking', active: true });
const hidden = new Gtk.CheckButton({ label: 'Include hidden files' });
// "Some of the range is affected" — the third state, drawn as a dash and announced
// as `mixed`. GTK does NOT clear it when the user ticks the button; the property's
// own documentation says that is the application's job.
const partial = new Gtk.CheckButton({ label: 'Some of the selection', inconsistent: true });
Web Components
HTML
<div style="display:flex;flex-direction:column;gap:8px;">
<gtk-check-button label="Enable networking" checked></gtk-check-button>
<gtk-check-button label="Include hidden files"></gtk-check-button>
<gtk-check-button label="Some of the selection" indeterminate></gtk-check-button>
</div>
Solid
TypeScript
// mount(() => <GtkCheckButton />, container) — from '@gjsify/gtk-host/solid'
const GtkCheckButton = () => (
<gtk-check-button
label="Enable networking"
active
/>
);
Vue
Vue
<!-- GtkCheckButton.vue — mount(GtkCheckButton, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-check-button
label="Enable networking"
:active="true"
/>
</template>
React
TypeScript
// createRoot(container).render(<GtkCheckButton />) — from '@gjsify/gtk-host/react'
const GtkCheckButton = () => (
<gtk-check-button
label="Enable networking"
active
/>
);
checked and indeterminate

GTK names the two states active and inconsistent; the browser element names them checked and indeterminate, because it wraps a real <input type="checkbox"> and those are the words an author already writes and the ones libadwaita’s stylesheet selects on. Nothing else about the widget differs between the two renderings — except that a click clears indeterminate here, which is the HTML rule rather than GTK’s.

A light switch, for a boolean that is on its own rather than in a row. Its two properties are not the same thing, and that is the whole of Gtk.Switch: active is where the user put it, state is what the thing being switched currently is. Keep them equal and the switch is an ordinary toggle; let them differ and it is the honest picture of a change that has not landed yet — the knob where you left it, the trough still the old colour. Setting active on its own sets state too, because GTK’s default ::state-set handler does exactly that; write state yourself, after active, when you are driving the other half.

For a boolean with a title beside it, use Switch Row.

Gtk.Switch
Blueprint
Code
using Gtk 4.0;
Gtk.Box {
spacing: 24;
halign: center;
Gtk.Switch {
active: true;
state: true;
}
Gtk.Switch {
active: true;
}
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
const notifications = new Gtk.Switch({ active: true });
// A change that takes a while: move the knob now, and the trough when it lands.
// `set_state` is what repaints the trough alone; `set_active` would take `state`
// with it, because its default `::state-set` handler runs.
const syncing = new Gtk.Switch({ active: true, state: false });
Web Components
HTML
<div style="display:flex;gap:24px;align-items:center;justify-content:center;">
<!-- active: Whether the `GtkSwitch` widget is in its on or off state. -->
<!-- state: The backend state that is controlled by the switch. -->
<gtk-switch active state></gtk-switch>
<!-- active: Whether the `GtkSwitch` widget is in its on or off state. -->
<gtk-switch active></gtk-switch>
</div>
Solid
TypeScript
// mount(() => <GtkSwitch />, container) — from '@gjsify/gtk-host/solid'
const GtkSwitch = () => (
<gtk-switch
active
state
halign="center"
/>
);
Vue
Vue
<!-- GtkSwitch.vue — mount(GtkSwitch, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-switch
:active="true"
:state="true"
halign="center"
/>
</template>
React
TypeScript
// createRoot(container).render(<GtkSwitch />) — from '@gjsify/gtk-host/react'
const GtkSwitch = () => (
<gtk-switch
active
state
halign="center"
/>
);

A slider. Gtk.Scale extends Gtk.Range, and Gtk.Range has no page of its own: it is concrete (gtk_range_get_type() is a bare G_DEFINE_TYPE_WITH_CODE, gtkrange.c:267), but GTK only ever builds one as the base of Gtk.Scale and Gtk.Scrollbar. Every property it declares is therefore documented on one of those two — which is why this block reads adjustment rather than a range.

The adjustment is the range: a JSON object with lower, upper, stepIncrement, pageIncrement, pageSize and value. The last one is the value, so it belongs inside the object rather than beside it. Dragging, the arrow keys and Page Up/Down all move by stepIncrement and pageIncrement respectively, and Ctrl with an arrow pages.

digits is how many decimal places the value text carries, draw-value shows it at all, and value-pos puts it top (the default), bottom, left or right. inverted runs the scale the other way; for a horizontal scale it already is inverted in a right-to-left document, which is what Gtk.Range’s flippable flag is for.

Marks and fine-tune

Gtk.Scale also has marks — tick lines with labels — and a Shift-held drag that enters fine-tune mode. Both have no browser counterpart here: add_mark() is deprecated since GTK 4.10, and fine-tune is a style class Gtk.Range adds for the length of that one gesture.

Gtk.Scale
Blueprint
Code
using Gtk 4.0;
Gtk.Box {
orientation: vertical;
spacing: 16;
Gtk.Scale {
digits: 0;
draw-value: true;
value-pos: top;
adjustment: Gtk.Adjustment {
lower: 0;
upper: 100;
value: 40;
step-increment: 1;
page-increment: 10;
};
}
Gtk.Scale {
digits: 0;
inverted: true;
adjustment: Gtk.Adjustment {
lower: 0;
upper: 100;
value: 25;
step-increment: 1;
page-increment: 10;
};
}
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
const adjustment = new Gtk.Adjustment({
lower: 0,
upper: 100,
value: 40,
stepIncrement: 1,
pageIncrement: 10,
});
const scale = new Gtk.Scale({
adjustment,
digits: 0,
drawValue: true,
valuePos: Gtk.PositionType.TOP,
});
// A vertical scale is the same widget with another orientation.
const mirrored = new Gtk.Scale({ adjustment, inverted: true });
Web Components
HTML
<gtk-box orientation="vertical" spacing="16" style="width:320px;">
<!-- adjustment: The adjustment that is controlled by the range. -->
<!-- digits: The number of decimal places that are displayed in the value. -->
<!-- draw-value: Whether the current value is displayed as a string next to the slider. -->
<gtk-scale
adjustment='{"lower":0,"upper":100,"value":40,"stepIncrement":1,"pageIncrement":10}'
digits="0"
draw-value
value-pos="top">
</gtk-scale>
<!-- The same slider without the value text and filling from the right. -->
<!-- adjustment: The adjustment that is controlled by the range. -->
<!-- digits: The number of decimal places that are displayed in the value. -->
<!-- inverted: If TRUE, the direction in which the slider moves is inverted. -->
<gtk-scale
adjustment='{"lower":0,"upper":100,"value":25,"stepIncrement":1,"pageIncrement":10}'
digits="0"
inverted>
</gtk-scale>
</gtk-box>
Solid
TypeScript
// mount(() => <GtkScale />, container) — from '@gjsify/gtk-host/solid'
const GtkScale = () => (
<gtk-box
orientation="vertical"
spacing={12}
>
<gtk-scale
adjustment={{ lower: 0, upper: 100, value: 40, stepIncrement: 1, pageIncrement: 10 }}
digits={0}
drawValue
valuePos="top"
widthRequest={320}
/>
</gtk-box>
);
Vue
Vue
<!-- GtkScale.vue — mount(GtkScale, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-box
orientation="vertical"
:spacing="12"
>
<gtk-scale
:adjustment="{ lower: 0, upper: 100, value: 40, stepIncrement: 1, pageIncrement: 10 }"
:digits="0"
:draw-value="true"
value-pos="top"
:width-request="320"
/>
</gtk-box>
</template>
React
TypeScript
// createRoot(container).render(<GtkScale />) — from '@gjsify/gtk-host/react'
const GtkScale = () => (
<gtk-box
orientation="vertical"
spacing={12}
>
<gtk-scale
adjustment={{ lower: 0, upper: 100, value: 40, stepIncrement: 1, pageIncrement: 10 }}
digits={0}
drawValue
valuePos="top"
widthRequest={320}
/>
</gtk-box>
);

A number field with an arrow either side of it. Its adjustment is the same JSON object the scale takes, so a range authored here is the one the scale above would use unchanged.

numeric refuses anything that is not a number as it is typed; digits is how many decimal places the field shows and commits; snap-to-ticks moves a typed value onto the step grid rather than leaving it between two steps; and wrap makes an arrow at a bound jump to the other end instead of going insensitive. update-policy decides what a rejected entry does: always clamps it into the range, if-valid leaves the committed value alone and re-formats the field.

Where the arrows sit

The horizontal widget reads text ▼ ▲ and the vertical one ▲ text ▼ — that is the order Gtk.SpinButton builds its children in, and it is why the rounded corner is on the up arrow in both. orientation moves the up arrow the same way C does.

Gtk.SpinButton
Blueprint
Code
using Gtk 4.0;
Gtk.SpinButton {
digits: 0;
numeric: true;
snap-to-ticks: true;
adjustment: Gtk.Adjustment {
lower: 0;
upper: 10;
value: 5;
step-increment: 1;
};
}
Gtk.SpinButton {
orientation: vertical;
digits: 2;
wrap: true;
adjustment: Gtk.Adjustment {
lower: 0;
upper: 1;
value: 0.5;
step-increment: 0.25;
};
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
const count = new Gtk.SpinButton({
adjustment: new Gtk.Adjustment({ lower: 0, upper: 10, value: 5, stepIncrement: 1 }),
digits: 0,
numeric: true,
snap_to_ticks: true,
});
const gain = new Gtk.SpinButton({
adjustment: new Gtk.Adjustment({ lower: 0, upper: 1, value: 0.5, stepIncrement: 0.25 }),
digits: 2,
wrap: true,
orientation: Gtk.Orientation.VERTICAL,
});
Web Components
HTML
<div style="display:flex;flex-wrap:wrap;gap:12px;justify-content:center;align-items:center;">
<!-- adjustment: The adjustment that holds the value of the spin button. -->
<!-- digits: The number of decimal places to display. -->
<!-- numeric: Whether non-numeric characters should be ignored. -->
<!-- snap-to-ticks: Whether erroneous values are automatically changed to the spin buttons nearest step increment. -->
<gtk-spin-button
adjustment='{"lower":0,"upper":10,"value":5,"stepIncrement":1,"pageIncrement":2}'
digits="0"
numeric
snap-to-ticks>
</gtk-spin-button>
<!-- A wrapping field over a fractional range, shown at two places. -->
<!-- adjustment: The adjustment that holds the value of the spin button. -->
<!-- digits: The number of decimal places to display. -->
<!-- wrap: Whether a spin button should wrap upon reaching its limits. -->
<gtk-spin-button
orientation="vertical"
adjustment='{"lower":0,"upper":1,"value":0.5,"stepIncrement":0.25}'
digits="2"
wrap>
</gtk-spin-button>
</div>
Solid
TypeScript
// mount(() => <GtkSpinButton />, container) — from '@gjsify/gtk-host/solid'
const GtkSpinButton = () => (
<gtk-box
orientation="vertical"
spacing={12}
>
<gtk-spin-button
adjustment={{ lower: 0, upper: 10, value: 5, stepIncrement: 1, pageIncrement: 2 }}
digits={0}
numeric
snapToTicks
halign="center"
/>
</gtk-box>
);
Vue
Vue
<!-- GtkSpinButton.vue — mount(GtkSpinButton, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-box
orientation="vertical"
:spacing="12"
>
<gtk-spin-button
:adjustment="{ lower: 0, upper: 10, value: 5, stepIncrement: 1, pageIncrement: 2 }"
:digits="0"
:numeric="true"
:snap-to-ticks="true"
halign="center"
/>
</gtk-box>
</template>
React
TypeScript
// createRoot(container).render(<GtkSpinButton />) — from '@gjsify/gtk-host/react'
const GtkSpinButton = () => (
<gtk-box
orientation="vertical"
spacing={12}
>
<gtk-spin-button
adjustment={{ lower: 0, upper: 10, value: 5, stepIncrement: 1, pageIncrement: 2 }}
digits={0}
numeric
snapToTicks
halign="center"
/>
</gtk-box>
);

A field for a secret: masked, no copy to the clipboard, and a warning while Caps Lock is engaged. show-peek-icon adds the button that reveals the contents for one look — off by default, and the only way to see them at all.

It is not a Gtk.Entry (it is a sibling that implements GtkEditable itself), but it takes the same properties. For the row presentation, with the label as the row title, use Password Entry Row.

Gtk.PasswordEntry
Blueprint
Code
using Gtk 4.0;
Gtk.PasswordEntry {
placeholder-text: _("Password");
show-peek-icon: true;
width-request: 220;
}
Gtk.PasswordEntry {
width-request: 220;
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
const password = new Gtk.PasswordEntry({
placeholderText: 'Password',
showPeekIcon: true,
widthRequest: 220,
});
// The Caps Lock warning needs no property: GDK reads the state off the keyboard.
const withoutPeek = new Gtk.PasswordEntry({ showPeekIcon: false });
Web Components
HTML
<div style="display:flex;flex-wrap:wrap;gap:12px;justify-content:center;align-items:center;">
<!-- show-peek-icon: Whether to show an icon for revealing the content. -->
<gtk-password-entry placeholder="Password" show-peek-icon style="min-width:220px;"></gtk-password-entry>
<gtk-password-entry style="min-width:220px;"></gtk-password-entry>
</div>
Solid
TypeScript
// mount(() => <GtkPasswordEntry />, container) — from '@gjsify/gtk-host/solid'
const GtkPasswordEntry = () => (
<gtk-password-entry
showPeekIcon
placeholderText="Password"
/>
);
Vue
Vue
<!-- GtkPasswordEntry.vue — mount(GtkPasswordEntry, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-password-entry
:show-peek-icon="true"
placeholder-text="Password"
/>
</template>
React
TypeScript
// createRoot(container).render(<GtkPasswordEntry />) — from '@gjsify/gtk-host/react'
const GtkPasswordEntry = () => (
<gtk-password-entry
showPeekIcon
placeholderText="Password"
/>
);

A field for typing a query. A magnifying glass leads while it is empty and a clear button follows once it is not; clicking the clear button empties the field, exactly as C’s edit-clear-symbolic press does.

The point of the widget is search-delay: search-changed waits 150 ms — the default — after the last keypress, so a filter is not driven by every character. Emptying the field is the one exception: that fires the signal at once and cancels whatever was pending.

The keys are the signals: Enter is activate, Escape is stop-search, Ctrl+g is next-match and Ctrl+Shift+g is previous-match.

Gtk.SearchEntry
Blueprint
Code
using Gtk 4.0;
Gtk.SearchEntry {
placeholder-text: _("Search…");
search-delay: 150;
width-request: 260;
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
// The app's own filtering; the widget does not do this for you.
const applyFilter = (query: string) => void query;
const search = new Gtk.SearchEntry({
placeholderText: 'Search…',
searchDelay: 150,
widthRequest: 260,
});
// Fires once the user STOPS typing, not on every keystroke.
search.connect('search-changed', () => applyFilter(search.text));
search.connect('stop-search', () => search.set_visible(false));
Web Components
HTML
<!-- search-delay: The delay in milliseconds from last keypress to the search changed signal. -->
<gtk-search-entry search-delay="150" placeholder="Search…" style="min-width:260px;"></gtk-search-entry>
Solid
TypeScript
// mount(() => <GtkSearchEntry />, container) — from '@gjsify/gtk-host/solid'
const GtkSearchEntry = () => (
<gtk-search-entry
placeholderText="Search…"
searchDelay={150}
/>
);
Vue
Vue
<!-- GtkSearchEntry.vue — mount(GtkSearchEntry, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-search-entry
placeholder-text="Search…"
:search-delay="150"
/>
</template>
React
TypeScript
// createRoot(container).render(<GtkSearchEntry />) — from '@gjsify/gtk-host/react'
const GtkSearchEntry = () => (
<gtk-search-entry
placeholderText="Search…"
searchDelay={150}
/>
);
  • Buttons: the plain button, its style classes and the menu button.
  • Indicators: the progress bar, the level bar and the GTK spinner — how far along something is.
  • Boxed Lists: the row twins of both controls, entry row, password entry row, combo row and spin row.
  • Adwaita Storybook: the same widgets in a live component browser you can poke at.