Skip to content

Scrolling

Everything that scrolls in GTK is one of four widgets. Gtk.ScrolledWindow is the container you actually reach for; it puts its child in a Gtk.Viewport and draws up to two **Gtk.Scrollbar**s beside it. Gtk.WindowHandle is not part of that group at all — it is the titlebar strip — and it is here because it is the fourth of the widgets this page covers.

None of the four is an Adwaita widget: libadwaita ships no scrolling widget and styles only the scrollbars, which is what Gtk.Scrollbar’s block below shows most of.

What the NativeScript port has

The previews run the browser port, which scrolls through the platform. The NativeScript port has none of these four widgets yet — no container, no viewport, no scrollbar and no titlebar handle — so the XML template tab of every block here says so instead of showing one.

What a scrolled window needs

GTK gives this widget an allocation from its parent and asks for very little itself, so it is its parent that decides how tall it is. A browser scrolled window needs the same from its author: give it a height, or it grows with its content and has nothing to scroll. Every preview below does, with style="height:…" on the scrolled window itself.

One child, two adjustments, and a scrollbar per axis that appears only when there is something to scroll. The two policies decide whether a bar appears, window-placement which edge it takes, and overlay-scrolling turns the bars into the auto-hiding indicators GNOME draws over the content instead of beside it.

The keys are GTK’s, and they are not the obvious ones: every arrow and every Home/End needs Control, and Page Up/Down is the vertical axis bare while Ctrl+Page Up/Down is the horizontal one.

Gtk.ScrolledWindow
Blueprint
Code
using Gtk 4.0;
Gtk.ScrolledWindow {
vexpand: true;
Gtk.Box {
orientation: vertical;
spacing: 6;
Gtk.Label {
label: _("Row 1");
}
Gtk.Label {
label: _("Row 2");
}
Gtk.Label {
label: _("Row 3");
}
Gtk.Label {
label: _("Row 4");
}
}
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
const column = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 6 });
for (let row = 1; row <= 8; row++) column.append(new Gtk.Label({ label: `Row ${row}` }));
const scrolled = new Gtk.ScrolledWindow({ child: column, vexpand: true });
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
scrolled.set_overlay_scrolling(true);
Web Components
HTML
<!-- vscrollbar-policy: When the vertical scrollbar is displayed. -->
<!-- hscrollbar-policy: When the horizontal scrollbar is displayed. -->
<!-- overlay-scrolling: Whether overlay scrolling is enabled or not. -->
<gtk-scrolled-window vscrollbar-policy="automatic" hscrollbar-policy="automatic" overlay-scrolling="true" style="width:100%;max-width:320px;height:160px;">
<gtk-box orientation="vertical" spacing="6">
<gtk-label label="Row 1"></gtk-label>
<gtk-label label="Row 2"></gtk-label>
<gtk-label label="Row 3"></gtk-label>
<gtk-label label="Row 4"></gtk-label>
<gtk-label label="Row 5"></gtk-label>
<gtk-label label="Row 6"></gtk-label>
<gtk-label label="Row 7"></gtk-label>
<gtk-label label="Row 8"></gtk-label>
</gtk-box>
</gtk-scrolled-window>
Solid
TypeScript
// mount(() => <GtkScrolledWindow />, container) — from '@gjsify/gtk-host/solid'
const GtkScrolledWindow = () => (
<gtk-scrolled-window
hscrollbarPolicy="automatic"
vscrollbarPolicy="automatic"
hasFrame={false}
>
<gtk-box
orientation="vertical"
spacing={8}
>
<gtk-label label="Row 1" />
<gtk-label label="Row 2" />
<gtk-label label="Row 3" />
</gtk-box>
</gtk-scrolled-window>
);
Vue
Vue
<!-- GtkScrolledWindow.vue — mount(GtkScrolledWindow, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-scrolled-window
hscrollbar-policy="automatic"
vscrollbar-policy="automatic"
:has-frame="false"
>
<gtk-box
orientation="vertical"
:spacing="8"
>
<gtk-label label="Row 1" />
<gtk-label label="Row 2" />
<gtk-label label="Row 3" />
</gtk-box>
</gtk-scrolled-window>
</template>
React
TypeScript
// createRoot(container).render(<GtkScrolledWindow />) — from '@gjsify/gtk-host/react'
const GtkScrolledWindow = () => (
<gtk-scrolled-window
hscrollbarPolicy="automatic"
vscrollbarPolicy="automatic"
hasFrame={false}
>
<gtk-box
orientation="vertical"
spacing={8}
>
<gtk-label label="Row 1" />
<gtk-label label="Row 2" />
<gtk-label label="Row 3" />
</gtk-box>
</gtk-scrolled-window>
);

A trough and a slider, positioned by an adjustment. Its geometry is GtkRange’s and the two numbers it is made of do not share a denominator: the slider is page_size over the WHOLE range long, and it sits at the position over the SCROLLABLE range — the whole one less the page. That is why a long adjustment keeps a grabbable thumb instead of shrinking to nothing.

GtkScrollbar has no adjustment of its own — unlike GtkRange, it makes none — so the third bar below, which is given none, is a bar over an empty range: the thumb fills the trough and cannot move.

Gtk.Scrollbar
Blueprint
Code
using Gtk 4.0;
Gtk.Box {
orientation: horizontal;
spacing: 24;
Gtk.Scrollbar {
orientation: vertical;
valign: center;
adjustment: Adjustment {
value: 80;
lower: 0;
upper: 400;
step-increment: 10;
page-increment: 80;
page-size: 120;
};
}
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
// `GtkScrollbar` creates no adjustment of its own, so one is handed to it.
const adjustment = new Gtk.Adjustment({
value: 80,
lower: 0,
upper: 400,
step_increment: 10,
page_increment: 80,
page_size: 120,
});
const vertical = new Gtk.Scrollbar({ adjustment, orientation: Gtk.Orientation.VERTICAL, valign: Gtk.Align.CENTER });
const horizontal = new Gtk.Scrollbar({ adjustment, orientation: Gtk.Orientation.HORIZONTAL });
Web Components
HTML
<gtk-box orientation="horizontal" spacing="24" style="justify-content:center;align-items:center;">
<!-- adjustment: The `GtkAdjustment` controlled by this scrollbar. -->
<gtk-scrollbar orientation="vertical" adjustment='{"value":80,"lower":0,"upper":400,"stepIncrement":10,"pageIncrement":80,"pageSize":120}' style="height:200px;"></gtk-scrollbar>
<!-- adjustment: The `GtkAdjustment` controlled by this scrollbar. -->
<gtk-scrollbar orientation="horizontal" adjustment='{"value":80,"lower":0,"upper":400,"stepIncrement":10,"pageIncrement":80,"pageSize":120}' style="width:200px;"></gtk-scrollbar>
<gtk-scrollbar orientation="vertical" style="height:200px;"></gtk-scrollbar>
</gtk-box>
Solid
TypeScript
// mount(() => <GtkScrollbar />, container) — from '@gjsify/gtk-host/solid'
const GtkScrollbar = () => (
<gtk-scrollbar
orientation="vertical"
valign="center"
adjustment={{ value: 80, lower: 0, upper: 400, stepIncrement: 10, pageIncrement: 80, pageSize: 120 }}
/>
);
Vue
Vue
<!-- GtkScrollbar.vue — mount(GtkScrollbar, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-scrollbar
orientation="vertical"
valign="center"
:adjustment="{ value: 80, lower: 0, upper: 400, stepIncrement: 10, pageIncrement: 80, pageSize: 120 }"
/>
</template>
React
TypeScript
// createRoot(container).render(<GtkScrollbar />) — from '@gjsify/gtk-host/react'
const GtkScrollbar = () => (
<gtk-scrollbar
orientation="vertical"
valign="center"
adjustment={{ value: 80, lower: 0, upper: 400, stepIncrement: 10, pageIncrement: 80, pageSize: 120 }}
/>
);

The widget under a scrolled window’s child: “implements scrollability for widgets that don’t support scrolling on their own”. It has one property of its own, scroll-to-focus — whether a widget that takes the focus is scrolled into view — and its adjustments are its own: a step is a tenth of the window and a page nine tenths of it, whatever it wraps.

The child is allocated its natural size and never shrunk, so content wider than the window scrolls sideways instead of being squeezed into it.

Gtk.Viewport
Blueprint
Code
using Gtk 4.0;
Gtk.Viewport {
scroll-to-focus: true;
Gtk.Box {
orientation: vertical;
spacing: 6;
Gtk.Label {
label: _("Row 1");
}
Gtk.Label {
label: _("Row 2");
}
Gtk.Button {
label: _("Focus me");
}
}
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
const column = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 6 });
for (let row = 1; row <= 6; row++) column.append(new Gtk.Label({ label: `Row ${row}` }));
column.append(new Gtk.Button({ label: 'Focus me' }));
const viewport = new Gtk.Viewport({ child: column });
viewport.set_scroll_to_focus(true);
Web Components
HTML
<!-- scroll-to-focus: Whether to scroll when the focus changes. -->
<gtk-viewport scroll-to-focus="true" style="width:100%;max-width:320px;height:160px;">
<gtk-box orientation="vertical" spacing="6">
<gtk-label label="Row 1"></gtk-label>
<gtk-label label="Row 2"></gtk-label>
<gtk-label label="Row 3"></gtk-label>
<gtk-label label="Row 4"></gtk-label>
<gtk-label label="Row 5"></gtk-label>
<gtk-label label="Row 6"></gtk-label>
<gtk-button label="Focus me"></gtk-button>
</gtk-box>
</gtk-viewport>
Solid
TypeScript
// mount(() => <GtkViewport />, container) — from '@gjsify/gtk-host/solid'
const GtkViewport = () => (
<gtk-viewport scrollToFocus>
<gtk-box
orientation="vertical"
spacing={6}
>
<gtk-label label="Row 1" />
<gtk-label label="Row 2" />
<gtk-label label="Row 3" />
</gtk-box>
</gtk-viewport>
);
Vue
Vue
<!-- GtkViewport.vue — mount(GtkViewport, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-viewport :scroll-to-focus="true">
<gtk-box
orientation="vertical"
:spacing="6"
>
<gtk-label label="Row 1" />
<gtk-label label="Row 2" />
<gtk-label label="Row 3" />
</gtk-box>
</gtk-viewport>
</template>
React
TypeScript
// createRoot(container).render(<GtkViewport />) — from '@gjsify/gtk-host/react'
const GtkViewport = () => (
<gtk-viewport scrollToFocus>
<gtk-box
orientation="vertical"
spacing={6}
>
<gtk-label label="Row 1" />
<gtk-label label="Row 2" />
<gtk-label label="Row 3" />
</gtk-box>
</gtk-viewport>
);

The titlebar strip. It is the fourth widget on this page and the only one that is not about scrolling: dragging it moves the window, and a double click, a middle click and a right click raise the window actions the compositor names — or, where there is none, the gtk-titlebar-double-click, -middle-click and -right-click settings, whose values are toggle-maximize, lower, minimize, menu and none.

A document cannot move or unmaximize the window it is rendered in, and no web API asks it to. The web port therefore reads the same three settings as attributes and raises the same action names as events: a titlebar-action event for every gesture, plus one event named for the action it chose.

Gtk.WindowHandle
Blueprint
Code
using Gtk 4.0;
using Adw 1;
Gtk.WindowHandle {
Adw.HeaderBar {
Adw.WindowTitle {
title: _("Notes");
}
}
}
TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gtk from 'gi://Gtk?version=4.0';
Adw.init();
const bar = new Adw.HeaderBar();
bar.set_title_widget(new Adw.WindowTitle({ title: 'Notes' }));
// A `GtkBinLayout` with one child: the handle itself draws nothing.
const handle = new Gtk.WindowHandle({ child: bar });
Web Components
HTML
<gtk-window-handle double-click-action="toggle-maximize" style="width:100%;max-width:360px;">
<adw-header-bar>
<adw-window-title slot="title" title="Notes"></adw-window-title>
</adw-header-bar>
</gtk-window-handle>
Solid
TypeScript
// mount(() => <GtkWindowHandle />, container) — from '@gjsify/gtk-host/solid'
const GtkWindowHandle = () => (
<gtk-window-handle>
<adw-header-bar>
<adw-window-title title="Notes" />
</adw-header-bar>
</gtk-window-handle>
);
Vue
Vue
<!-- GtkWindowHandle.vue — mount(GtkWindowHandle, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-window-handle>
<adw-header-bar>
<adw-window-title title="Notes" />
</adw-header-bar>
</gtk-window-handle>
</template>
React
TypeScript
// createRoot(container).render(<GtkWindowHandle />) — from '@gjsify/gtk-host/react'
const GtkWindowHandle = () => (
<gtk-window-handle>
<adw-header-bar>
<adw-window-title title="Notes" />
</adw-header-bar>
</gtk-window-handle>
);
  • Layout: the separator and the layout widgets libadwaita only styles.
  • Buttons: the button, the toggle button and the menu button.
  • Adwaita Layout: header bars, clamps and toolbar views — and the toolbar view, whose content area is a scrolled window.
  • Adwaita Storybook: the same widgets in a live component browser you can poke at.