Skip to content

View Switching

When your window has several peer pages and shows one at a time, put the pages in a view stack and give the user a switcher to pick between them. Five switchers are on this page: a segmented bar, the same bar pinned to the bottom edge for a narrow window, a strip of tabs, a compact toolbar control and a swipeable pager. All of them adapt to the window, so a wide desktop gets the full switcher and a narrow phone gets a collapsed one.

Each block below is a stack of windows. The first one RUNS the widget: the preview is the live browser port, so it follows the light and dark toggle the way a real window would, and the HTML tab beside it holds the very markup that paints it — one import '@gjsify/adwaita-web' registers every element in it. Then comes one window per kind of implementation — Native TypeScript, the @girs program that runs unchanged on GJS, Node, Bun and Deno, with the matching Blueprint declaration beside it, UI frameworks, the same widget written in Solid, Vue and React through @gjsify/gtk-host — or, where the widget is built imperatively rather than declared, the recorded reason it has no such snippet — and NativeScript, the port that runs on a phone. That last window splits the way the first one does. An XML template holds the tree, and the TypeScript beside it is the loader.

Libadwaita is the reference here, typed by @girs/adw-1. @gjsify/adwaita-web and @gjsify/adwaita-nativescript follow its naming, and where one of them differs, its own window says so.

The standard switcher: one segmented control whose buttons each carry a page’s icon and label, driving a paired view stack. Its policy decides the button layout. wide puts the icon and label side by side; narrow stacks the icon above the label so the bar fits a slim window. An adaptive layout normally swaps between the two on a breakpoint.

Adw.ViewSwitcher
HTML
<adw-view-switcher policy="wide" style="flex: 1; width: 100%;">
<adw-view-switcher-page name="inbox" title="Inbox" icon-name="mail-unread">
<adw-status-page icon="mail-unread" title="Inbox" description="You have three unread conversations."></adw-status-page>
</adw-view-switcher-page>
<adw-view-switcher-page name="starred" title="Starred" icon-name="starred">
<adw-status-page icon="starred" title="Starred" description="Messages you have marked as important."></adw-status-page>
</adw-view-switcher-page>
<adw-view-switcher-page name="archive" title="Archive" icon-name="folder">
<adw-status-page icon="folder" title="Archive" description="Older conversations kept for reference."></adw-status-page>
</adw-view-switcher-page>
</adw-view-switcher>
<adw-view-switcher> observes these in the browser port. Changing one re-renders the element. The second column is what the preview one tab to the left writes, read across every copy of the element in it. The other windows name their own properties.
Attribute In this preview
policy wide
active not used
Native TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gtk from 'gi://Gtk?version=4.0';
// The view stack holds the pages; each is added with a name, title and icon.
const stack = new Adw.ViewStack({ vexpand: true });
stack.add_titled_with_icon(
new Adw.StatusPage({ iconName: 'mail-unread-symbolic', title: 'Inbox', description: 'You have three unread conversations.' }),
'inbox', 'Inbox', 'mail-unread-symbolic',
);
stack.add_titled_with_icon(
new Adw.StatusPage({ iconName: 'starred-symbolic', title: 'Starred', description: 'Messages you have marked as important.' }),
'starred', 'Starred', 'starred-symbolic',
);
stack.add_titled_with_icon(
new Adw.StatusPage({ iconName: 'folder-symbolic', title: 'Archive', description: 'Older conversations kept for reference.' }),
'archive', 'Archive', 'folder-symbolic',
);
// The switcher drives the stack; policy is Adw.ViewSwitcherPolicy.WIDE | NARROW.
const switcher = new Adw.ViewSwitcher({
stack,
policy: Adw.ViewSwitcherPolicy.WIDE,
halign: Gtk.Align.CENTER,
});
const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 12 });
box.append(switcher);
box.append(stack);
Code
using Gtk 4.0;
using Adw 1;
Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.ViewSwitcher {
policy: wide;
stack: stack;
};
}
content: Adw.ViewStack stack {
Adw.ViewStackPage {
name: "inbox";
title: _("Inbox");
icon-name: "mail-unread-symbolic";
child: Adw.StatusPage {
icon-name: "mail-unread-symbolic";
title: _("Inbox");
description: _("You have three unread conversations.");
};
}
Adw.ViewStackPage {
name: "starred";
title: _("Starred");
icon-name: "starred-symbolic";
child: Adw.StatusPage {
icon-name: "starred-symbolic";
title: _("Starred");
description: _("Messages you have marked as important.");
};
}
Adw.ViewStackPage {
name: "archive";
title: _("Archive");
icon-name: "folder-symbolic";
child: Adw.StatusPage {
icon-name: "folder-symbolic";
title: _("Archive");
description: _("Older conversations kept for reference.");
};
}
};
}
UI frameworks

No Solid, Vue or React snippet for Adw.ViewSwitcher: uncurated-placement — and its `stack` is a widget REFERENCE, where the three dialects diverge.

Every snippet in this gallery was compiled and run before it was written down. Where a widget is built imperatively rather than declared — a Gio.MenuModel, a dialog opened with present(), a container with no child policy — there is nothing for the three dialects to differ about, so the reason is recorded here rather than the tab being quietly absent.

NativeScript
TypeScript
import { AdwViewSwitcher, AdwStatusPage, type AdwViewPage } from '@gjsify/adwaita-nativescript';
import { folderSymbolic } from '@gjsify/adwaita-icons/places';
import { mailUnreadSymbolic, starredSymbolic } from '@gjsify/adwaita-icons/status';
const page = (icon: string, title: string, description: string): AdwStatusPage => {
const status = new AdwStatusPage();
status.iconName = icon; // a real Adwaita symbolic icon (SVG string)
status.title = title;
status.description = description;
return status;
};
const switcher = new AdwViewSwitcher();
// Each view carries a title + icon (shown on the switcher button) and its content.
switcher.setViews([
{ title: 'Inbox', icon: mailUnreadSymbolic, content: page(mailUnreadSymbolic, 'Inbox', 'You have three unread conversations.') },
{ title: 'Starred', icon: starredSymbolic, content: page(starredSymbolic, 'Starred', 'Messages you have marked as important.') },
{ title: 'Archive', icon: folderSymbolic, content: page(folderSymbolic, 'Archive', 'Older conversations kept for reference.') },
] satisfies AdwViewPage[]);

No NativeScript XML template for Adw.ViewSwitcher: AdwViewSwitcher.views is an array of page descriptors; an XML attribute is a string.

NativeScript’s Builder reaches a widget through an attribute, which is always a string, and through a child, which lands only where the widget declares a slot for it. A property that is an array of options or a reference to another view fits through neither, so the TypeScript beside this window is where that widget is built.

The narrow-window half of the same pattern. When the window is too slim for a switcher in the header bar, put one at the bottom edge instead — that is a view switcher bar, pinned into a toolbar view’s bottom slot and bound to the same stack. reveal is a request, not a state: a stack with fewer than two pages stays collapsed however loudly it is asked, so a bar over a one-page stack simply does not appear.

Adw.ViewSwitcherBar
HTML
<!-- `stack` takes the id of the <adw-view-stack> to bind to. -->
<adw-toolbar-view style="flex: 1; width: 100%;">
<adw-view-stack id="gallery-switcher-bar-stack" style="flex: 1;">
<adw-view-stack-page name="inbox" title="Inbox" icon-name="mail-unread">
<adw-status-page icon="mail-unread" title="Inbox" description="The inbox page."></adw-status-page>
</adw-view-stack-page>
<adw-view-stack-page name="starred" title="Starred" icon-name="starred">
<adw-status-page icon="starred" title="Starred" description="The starred page."></adw-status-page>
</adw-view-stack-page>
<adw-view-stack-page name="archive" title="Archive" icon-name="folder">
<adw-status-page icon="folder" title="Archive" description="The archive page."></adw-status-page>
</adw-view-stack-page>
</adw-view-stack>
<adw-view-switcher-bar slot="bottom" stack="gallery-switcher-bar-stack" reveal></adw-view-switcher-bar>
</adw-toolbar-view>
<adw-view-switcher-bar> observes these in the browser port. Changing one re-renders the element. The second column is what the preview one tab to the left writes, read across every copy of the element in it. The other windows name their own properties.
Attribute In this preview
stack gallery-switcher-bar-stack
reveal set
revealed not used
Native TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
const stack = new Adw.ViewStack();
stack.add_titled_with_icon(
new Adw.StatusPage({ iconName: 'mail-unread-symbolic', title: 'Inbox', description: 'The inbox page.' }),
'inbox', 'Inbox', 'mail-unread-symbolic',
);
stack.add_titled_with_icon(
new Adw.StatusPage({ iconName: 'starred-symbolic', title: 'Starred', description: 'The starred page.' }),
'starred', 'Starred', 'starred-symbolic',
);
stack.add_titled_with_icon(
new Adw.StatusPage({ iconName: 'folder-symbolic', title: 'Archive', description: 'The archive page.' }),
'archive', 'Archive', 'folder-symbolic',
);
// `reveal` ASKS for the bar; the widget still hides it below two pages.
const bar = new Adw.ViewSwitcherBar({ stack, reveal: true });
const view = new Adw.ToolbarView({ content: stack });
view.add_bottom_bar(bar);
Code
using Gtk 4.0;
using Adw 1;
Adw.ToolbarView {
content: Adw.ViewStack stack {
Adw.ViewStackPage {
name: "inbox";
title: _("Inbox");
icon-name: "mail-unread-symbolic";
child: Adw.StatusPage {
icon-name: "mail-unread-symbolic";
title: _("Inbox");
description: _("The inbox page.");
};
}
Adw.ViewStackPage {
name: "starred";
title: _("Starred");
icon-name: "starred-symbolic";
child: Adw.StatusPage {
icon-name: "starred-symbolic";
title: _("Starred");
description: _("The starred page.");
};
}
Adw.ViewStackPage {
name: "archive";
title: _("Archive");
icon-name: "folder-symbolic";
child: Adw.StatusPage {
icon-name: "folder-symbolic";
title: _("Archive");
description: _("The archive page.");
};
}
};
[bottom]
Adw.ViewSwitcherBar {
stack: stack;
reveal: true;
}
}
UI frameworks

No Solid, Vue or React snippet for Adw.ViewSwitcherBar: its `stack` is a widget reference, and a ref is spelled differently in all three dialects.

Every snippet in this gallery was compiled and run before it was written down. Where a widget is built imperatively rather than declared — a Gio.MenuModel, a dialog opened with present(), a container with no child policy — there is nothing for the three dialects to differ about, so the reason is recorded here rather than the tab being quietly absent.

NativeScript
TypeScript
import { AdwStatusPage, AdwToolbarView, AdwViewStack, AdwViewSwitcherBar } from '@gjsify/adwaita-nativescript';
import { folderSymbolic } from '@gjsify/adwaita-icons/places';
import { mailUnreadSymbolic, starredSymbolic } from '@gjsify/adwaita-icons/status';
const stack = new AdwViewStack();
for (const page of [
{ name: 'inbox', title: 'Inbox', icon: mailUnreadSymbolic },
{ name: 'starred', title: 'Starred', icon: starredSymbolic },
{ name: 'archive', title: 'Archive', icon: folderSymbolic },
]) {
const status = new AdwStatusPage();
status.iconName = page.icon; // an Adwaita symbolic icon (SVG string)
status.title = page.title;
stack.add(status, page.name, page.title, page.icon);
}
const bar = new AdwViewSwitcherBar();
bar.stack = stack;
// The NativeScript stack has no `items-changed`, so a bar bound before the pages
// exist would render no buttons. `refresh()` is that missing signal, by hand.
bar.refresh();
bar.reveal = true;
const view = new AdwToolbarView();
view.setContent(stack);
view.addBottomBar(bar);

No NativeScript XML template for Adw.ViewSwitcherBar: AdwViewSwitcherBar.stack points at another VIEW, which no attribute can name.

NativeScript’s Builder reaches a widget through an attribute, which is always a string, and through a child, which lands only where the widget declares a slot for it. A property that is an array of options or a reference to another view fits through neither, so the TypeScript beside this window is where that widget is built.

Reach for a tab view when the pages are not fixed. Unlike a view switcher, tabs are meant to be added, closed and reordered while the app runs. The tab bar draws one chip per page, each with a title and a close button, and raises the selected chip onto the view background. Turn on autohide and the bar disappears once a single page is left, so a one-tab document looks chrome-free.

Adw.TabView
HTML
<adw-tab-view style="flex: 1; width: 100%;">
<adw-tab-page title="Overview">
<adw-status-page icon="view-paged" title="Overview" description="A summary of everything at a glance."></adw-status-page>
</adw-tab-page>
<adw-tab-page title="Details">
<adw-status-page icon="view-paged" title="Details" description="The specifics, broken down line by line."></adw-status-page>
</adw-tab-page>
<adw-tab-page title="History">
<adw-status-page icon="view-paged" title="History" description="A log of every change made so far."></adw-status-page>
</adw-tab-page>
</adw-tab-view>
<adw-tab-view> observes these in the browser port. Changing one re-renders the element. The second column is what the preview one tab to the left writes, read across every copy of the element in it. The other windows name their own properties.
Attribute In this preview
selected not used
autohide not used
expand-tabs not used
no-close not used
Native TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gtk from 'gi://Gtk?version=4.0';
const tabView = new Adw.TabView({ vexpand: true });
for (const [title, body] of [
['Overview', 'A summary of everything at a glance.'],
['Details', 'The specifics, broken down line by line.'],
['History', 'A log of every change made so far.'],
] as const) {
const page = tabView.append(new Adw.StatusPage({ iconName: 'view-paged-symbolic', title, description: body }));
page.title = title;
}
// The tab bar renders the chips; autohide collapses it down to a single tab.
const tabBar = new Adw.TabBar({ view: tabView, autohide: false });
const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL });
box.append(tabBar);
box.append(tabView);
Code
using Gtk 4.0;
using Adw 1;
Gtk.Box {
orientation: vertical;
Adw.TabBar {
view: tabs;
}
Adw.TabView tabs {
vexpand: true;
}
}
UI frameworks

No Solid, Vue or React snippet for Adw.TabView: uncurated-placement — and its pages are AdwTabPage GObjects, which have no tag either.

Every snippet in this gallery was compiled and run before it was written down. Where a widget is built imperatively rather than declared — a Gio.MenuModel, a dialog opened with present(), a container with no child policy — there is nothing for the three dialects to differ about, so the reason is recorded here rather than the tab being quietly absent.

NativeScript
TypeScript
import { AdwTabView, AdwStatusPage, type AdwViewPage } from '@gjsify/adwaita-nativescript';
import { viewGridSymbolic } from '@gjsify/adwaita-icons/actions';
const page = (title: string, body: string): AdwStatusPage => {
const status = new AdwStatusPage();
status.iconName = viewGridSymbolic; // a real Adwaita symbolic icon (SVG string)
status.title = title;
status.description = body;
return status;
};
const tabView = new AdwTabView();
tabView.setViews([
{ title: 'Overview', content: page('Overview', 'A summary of everything at a glance.') },
{ title: 'Details', content: page('Details', 'The specifics, broken down line by line.') },
{ title: 'History', content: page('History', 'A log of every change made so far.') },
] satisfies AdwViewPage[]);

No NativeScript XML template for Adw.TabView: AdwTabView.views and .tabs are arrays; an XML attribute is a string.

NativeScript’s Builder reaches a widget through an attribute, which is always a string, and through a child, which lands only where the widget declares a slot for it. A property that is an array of options or a reference to another view fits through neither, so the TypeScript beside this window is where that widget is built.

When a full switcher is too wide for the space, use the inline one. It is built from a linked toggle group and drives the same kind of view stack, but its display mode lets each toggle show labels, icons or both. That makes it fit a header bar or a tight toolbar.

Adw.InlineViewSwitcher
HTML
<adw-inline-view-switcher display-mode="both" style="flex: 1; width: 100%;">
<adw-view-stack-page title="Overview" icon-name="view-grid">
<adw-status-page id="overview" icon="view-grid" title="Overview" description="A quick summary of your project."></adw-status-page>
</adw-view-stack-page>
<adw-view-stack-page title="Activity" icon-name="document-edit">
<adw-status-page id="activity" icon="document-edit" title="Activity" description="Recent edits and changes."></adw-status-page>
</adw-view-stack-page>
<adw-view-stack-page title="Settings" icon-name="emblem-system">
<adw-status-page id="settings" icon="emblem-system" title="Settings" description="Configure how things behave."></adw-status-page>
</adw-view-stack-page>
</adw-inline-view-switcher>
<adw-inline-view-switcher> observes these in the browser port. Changing one re-renders the element. The second column is what the preview one tab to the left writes, read across every copy of the element in it. The other windows name their own properties.
Attribute In this preview
active not used
display-mode both
flat not used
round not used
osd not used
Native TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gtk from 'gi://Gtk?version=4.0';
const stack = new Adw.ViewStack({ vexpand: true });
stack.add_titled_with_icon(
new Adw.StatusPage({ iconName: 'view-grid-symbolic', title: 'Overview', description: 'A quick summary of your project.' }),
'overview', 'Overview', 'view-grid-symbolic',
);
stack.add_titled_with_icon(
new Adw.StatusPage({ iconName: 'document-edit-symbolic', title: 'Activity', description: 'Recent edits and changes.' }),
'activity', 'Activity', 'document-edit-symbolic',
);
stack.add_titled_with_icon(
new Adw.StatusPage({ iconName: 'emblem-system-symbolic', title: 'Settings', description: 'Configure how things behave.' }),
'settings', 'Settings', 'emblem-system-symbolic',
);
// displayMode is LABELS | ICONS | BOTH.
const switcher = new Adw.InlineViewSwitcher({
stack,
displayMode: Adw.InlineViewSwitcherDisplayMode.BOTH,
halign: Gtk.Align.CENTER,
});
const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 12 });
box.append(switcher);
box.append(stack);
Code
using Gtk 4.0;
using Adw 1;
Gtk.Box {
orientation: vertical;
spacing: 12;
Adw.InlineViewSwitcher {
display-mode: both;
stack: stack;
halign: center;
}
Adw.ViewStack stack {
vexpand: true;
Adw.ViewStackPage {
title: _("Overview");
icon-name: "view-grid-symbolic";
child: Adw.StatusPage {
icon-name: "view-grid-symbolic";
title: _("Overview");
description: _("A quick summary of your project.");
};
}
Adw.ViewStackPage {
title: _("Activity");
icon-name: "document-edit-symbolic";
child: Adw.StatusPage {
icon-name: "document-edit-symbolic";
title: _("Activity");
description: _("Recent edits and changes.");
};
}
Adw.ViewStackPage {
title: _("Settings");
icon-name: "emblem-system-symbolic";
child: Adw.StatusPage {
icon-name: "emblem-system-symbolic";
title: _("Settings");
description: _("Configure how things behave.");
};
}
}
}
UI frameworks

No Solid, Vue or React snippet for Adw.InlineViewSwitcher: its `stack` is a widget reference, and a ref is spelled differently in all three dialects.

Every snippet in this gallery was compiled and run before it was written down. Where a widget is built imperatively rather than declared — a Gio.MenuModel, a dialog opened with present(), a container with no child policy — there is nothing for the three dialects to differ about, so the reason is recorded here rather than the tab being quietly absent.

NativeScript
TypeScript
import { AdwInlineViewSwitcher, AdwStatusPage, type AdwViewPage } from '@gjsify/adwaita-nativescript';
import { documentEditSymbolic, viewGridSymbolic } from '@gjsify/adwaita-icons/actions';
import { preferencesSystemSymbolic } from '@gjsify/adwaita-icons/categories';
const page = (icon: string, title: string, body: string): AdwStatusPage => {
const status = new AdwStatusPage();
status.iconName = icon; // a real Adwaita symbolic icon (SVG string)
status.title = title;
status.description = body;
return status;
};
const switcher = new AdwInlineViewSwitcher();
// display mode "both": pass '' as the title for icons-only, omit icon for labels-only.
switcher.setViews([
{ title: 'Overview', icon: viewGridSymbolic, content: page(viewGridSymbolic, 'Overview', 'A quick summary of your project.') },
{ title: 'Activity', icon: documentEditSymbolic, content: page(documentEditSymbolic, 'Activity', 'Recent edits and changes.') },
{ title: 'Settings', icon: preferencesSystemSymbolic, content: page(preferencesSystemSymbolic, 'Settings', 'Configure how things behave.') },
] satisfies AdwViewPage[]);

No NativeScript XML template for Adw.InlineViewSwitcher: AdwInlineViewSwitcher.views is an array of page descriptors; an XML attribute is a string.

NativeScript’s Builder reaches a widget through an attribute, which is always a string, and through a child, which lands only where the widget declares a slot for it. A property that is an array of options or a reference to another view fits through neither, so the TypeScript beside this window is where that widget is built.

For onboarding screens and galleries, use a carousel: pages sit side by side and the user flicks, scrolls or clicks a page indicator to move between them. Two indicator widgets bind to a carousel, dots (shown here) and lines. Both track the fractional scroll position and grow the marker nearest the current page.

A carousel answers to three gestures. Touch and a touchpad scroll it natively — the browser owns those, momentum and snapping included. A mouse drag is the one the browser has no gesture for, so it is implemented: press inside the carousel and pull, and the pages follow the cursor. Where it lands is Adw.SwipeTracker’s decision, not the browser’s — the drag’s velocity is projected and the nearest reachable page wins, so a short flick pages and a slow half-drag settles back.

Three knobs are worth knowing. The scroll wheel pages the carousel by default; set allow-scroll-wheel="false" to leave wheel events to the page instead. Mouse drag is on by default; allow-mouse-drag="false" leaves dragging to touch, as it does upstream. Long swipes are off by default; turn them on to let one flick skip several pages — with the caveat that a touchpad flick can already cross more than one page here. The setting is applied (each page gets scroll-snap-stop: always when long swipes are off), but the browser does not honour it for that gesture; GTK, which runs the same input through its own tracker, does.

Adw.Carousel
HTML
<adw-carousel id="view-switching-carousel" style="width: 100%; max-width: 480px; height: 160px;">
<!-- The wheel pages the carousel by default; allow-scroll-wheel="false" opts out.
Bind an indicator with `for`; swap in adw-carousel-indicator-lines for lines.
No `color` on these three on purpose. Each tints itself with 14% of an
accent over `--window-bg-color`, so in the LIGHT scheme the fill lands near
rgb(222, 234, 248), and the `#ffffff` they carried put white text on that
at about 1.2:1: the labels were invisible under the site's light toggle
while looking right in dark. Inherited, the label takes `--card-fg-color`,
near-black in light and `#ffffff` in dark, so dark is unchanged. -->
<adw-card style="width:440px;height:140px;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb, var(--accent-bg-color) 14%, var(--window-bg-color));box-sizing:border-box;">
<span style="font-size:24pt;font-weight:800;">Welcome</span>
</adw-card>
<adw-card style="width:440px;height:140px;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb, #2ec27e 14%, var(--window-bg-color));box-sizing:border-box;">
<span style="font-size:24pt;font-weight:800;">Discover</span>
</adw-card>
<adw-card style="width:440px;height:140px;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb, #e5a50a 14%, var(--window-bg-color));box-sizing:border-box;">
<span style="font-size:24pt;font-weight:800;">Get started</span>
</adw-card>
</adw-carousel>
<adw-carousel-indicator-dots for="view-switching-carousel"></adw-carousel-indicator-dots>
<adw-carousel> observes these in the browser port. Changing one re-renders the element. The second column is what the preview one tab to the left writes, read across every copy of the element in it. The other windows name their own properties.
Attribute In this preview
allow-scroll-wheel not used
allow-mouse-drag not used
allow-long-swipes not used
interactive not used
spacing not used
position not used
Native TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gtk from 'gi://Gtk?version=4.0';
const carousel = new Adw.Carousel({ allowScrollWheel: true, allowLongSwipes: false, vexpand: true });
for (const [title, style] of [
['Welcome', 'accent'],
['Discover', 'success'],
['Get started', 'warning'],
] as const) {
const card = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, halign: Gtk.Align.CENTER, valign: Gtk.Align.CENTER });
card.add_css_class('card');
card.add_css_class(style);
const label = new Gtk.Label({ label: title });
label.add_css_class('title-1');
card.append(label);
carousel.append(card);
}
// Dot indicators bind to the carousel; Adw.CarouselIndicatorLines is the drop-in alternative.
const dots = new Adw.CarouselIndicatorDots({ carousel });
const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, spacing: 12 });
box.append(carousel);
box.append(dots);
Code
using Gtk 4.0;
using Adw 1;
Gtk.Box {
orientation: vertical;
spacing: 12;
halign: center;
valign: center;
Adw.Carousel carousel {
Gtk.Label {
label: _("Welcome");
styles ["title-1", "card"]
}
Gtk.Label {
label: _("Discover");
styles ["title-1", "card"]
}
Gtk.Label {
label: _("Get started");
styles ["title-1", "card"]
}
}
Adw.CarouselIndicatorDots {
carousel: carousel;
}
}
UI frameworks

No Solid, Vue or React snippet for Adw.Carousel: uncurated-placement: AdwCarousel has no child policy.

Every snippet in this gallery was compiled and run before it was written down. Where a widget is built imperatively rather than declared — a Gio.MenuModel, a dialog opened with present(), a container with no child policy — there is nothing for the three dialects to differ about, so the reason is recorded here rather than the tab being quietly absent.

NativeScript
TypeScript
import { AdwCarousel } from '@gjsify/adwaita-nativescript';
import { Label, StackLayout, type View } from '@nativescript/core';
const page = (title: string, accent: string): View => {
const card = new StackLayout();
card.orientation = 'vertical';
card.height = 260;
card.verticalAlignment = 'middle';
card.className = `adw-card ${accent}`;
const label = new Label();
label.text = title;
label.className = 'title-1';
label.horizontalAlignment = 'center';
label.verticalAlignment = 'middle';
card.addChild(label);
return card;
};
const carousel = new AdwCarousel();
carousel.pageWidth = 440;
carousel.addPage(page('Welcome', 'accent'));
carousel.addPage(page('Discover', 'success'));
carousel.addPage(page('Get started', 'warning'));
// AdwCarousel renders its own dot row; the line-indicator variant maps to the same dots on NativeScript.
XML
<adw:AdwCarousel
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:adw="~/adwaita"
>
<adw:AdwStatusPage
iconText=""
title="Welcome"
/>
<adw:AdwStatusPage
iconText=""
title="Sync"
/>
<adw:AdwStatusPage
iconText=""
title="Done"
/>
</adw:AdwCarousel>
  • Navigation: pages, split views and the navigation stack the switchers sit above.
  • Layout: the Clamp and boxes that size a switched view.
  • Adwaita Storybook: the same widgets in a live component browser you can poke at.