Skip to content

Layout

An Adwaita window is a frame plus the containers that give it shape. The window is the frame itself; a bin holds one replaceable widget where a slot calls for it, a clamp holds content at a readable width (and a scrollable clamp when that content scrolls), a header bar is the top chrome, a toolbar view frames content between fixed bars, and a wrap box lets a run of children flow onto new lines. None of them hold data: they position what you put inside and adapt as the space around them changes.

The last three are the adaptive ones. A breakpoint bin gives breakpoints a home without a window, a multi-layout view holds one set of children and several arrangements of them, and a layout slot is the named hole one of those arrangements leaves for a child.

The ports, and what they follow

Libadwaita is the reference, typed by @girs/adw-1. @gjsify/adwaita-web and @gjsify/adwaita-nativescript follow its naming, and where a port differs, the window that shows it says so. Both answer gi://Adw and gi://Gtk. On NativeScript that resolves once you build with gjsify build --app nativescript --gi-renderer.

The previews run the browser port, so behaviour Libadwaita drives natively (a toolbar view revealing each bar independently, a clamp’s tightening threshold) can be simplified here.

The frame everything else goes into. It styles no chrome of its own: you give it a content widget — normally a toolbar view with a header bar — and it asks for the size Adw.Window asks for, 360 × 200 px, unless you set a default size yourself.

The preview is a window drawn in the page

Adw.Window is a toplevel. On the desktop it is a real window that the compositor places, maximises, tiles and closes. A browser document has no window manager, so <adw-window> is a framed box in the page: the frame libadwaita draws for a client-side-decorated window, the content property, the breakpoints and the open-dialog bookkeeping — and none of what a compositor owns, so no titlebar, no minimise button and no window placement. What you put inside it behaves the same.

Adw.Window
Blueprint
Code
using Gtk 4.0;
using Adw 1;
Adw.Window {
default-width: 440;
default-height: 240;
content: Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.WindowTitle {
title: _("Text Editor");
subtitle: _("notes.md");
};
}
content: Adw.StatusPage {
title: _("Your Library");
description: _("Content sits between the toolbars and scrolls independently of them.");
};
};
}
TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
Adw.init();
const headerBar = new Adw.HeaderBar();
headerBar.set_title_widget(new Adw.WindowTitle({ title: 'Text Editor', subtitle: 'notes.md' }));
const toolbarView = new Adw.ToolbarView({ top_bar: headerBar });
toolbarView.content = new Adw.StatusPage({
title: 'Your Library',
description: 'Content sits between the toolbars and scrolls independently of them.',
});
// ONE child, and it is a property: `content` takes the widget itself, so this is
// never an `append()`.
const window = new Adw.Window({ content: toolbarView });
// The size request `adw_window_init()` makes is 360×200 (adw-window.c:344).
window.set_default_size(440, 240);
Web Components
HTML
<adw-window width="440" height="240">
<adw-toolbar-view>
<adw-header-bar slot="top">
<adw-window-title slot="title" title="Text Editor" subtitle="notes.md"></adw-window-title>
</adw-header-bar>
<!-- description: The description markup to be displayed below the title. -->
<adw-status-page
slot="content"
title="Your Library"
description="Content sits between the toolbars and scrolls independently of them."
></adw-status-page>
</adw-toolbar-view>
</adw-window>
Solid
TypeScript
// mount(() => <AdwWindow />, container) — from '@gjsify/gtk-host/solid'
const AdwWindow = () => (
<adw-window
defaultWidth={440}
defaultHeight={240}
>
<adw-toolbar-view>
<adw-header-bar slot="top">
<adw-window-title
slot="title"
title="Text Editor"
subtitle="notes.md"
/>
</adw-header-bar>
<adw-status-page
slot="content"
title="Your Library"
description="Content sits between the toolbars and scrolls independently of them."
/>
</adw-toolbar-view>
</adw-window>
);
Vue
Vue
<!-- AdwWindow.vue — mount(AdwWindow, container) from '@gjsify/gtk-host/vue' -->
<template>
<adw-window
:default-width="440"
:default-height="240"
>
<adw-toolbar-view>
<adw-header-bar slot="top">
<adw-window-title
slot="title"
title="Text Editor"
subtitle="notes.md"
/>
</adw-header-bar>
<adw-status-page
slot="content"
title="Your Library"
description="Content sits between the toolbars and scrolls independently of them."
/>
</adw-toolbar-view>
</adw-window>
</template>
React
TypeScript
// createRoot(container).render(<AdwWindow />) — from '@gjsify/gtk-host/react'
const AdwWindow = () => (
<adw-window
defaultWidth={440}
defaultHeight={240}
>
<adw-toolbar-view>
<adw-header-bar slot="top">
<adw-window-title
slot="title"
title="Text Editor"
subtitle="notes.md"
/>
</adw-header-bar>
<adw-status-page
slot="content"
title="Your Library"
description="Content sits between the toolbars and scrolls independently of them."
/>
</adw-toolbar-view>
</adw-window>
);
The NativeScript tabs were inflated on a device

adwaita-gallery-nativescript loads each template through NativeScript’s own Builder and asserts the view tree it built. That is the only way to tell a template that renders from one that renders something else: an attribute arrives as a string, and a child a widget has no slot for is added to the layout anyway.

A NativeScript template reaches these widgets through its xmlns, and the namespace is a MODULE: NativeScript loads it and reads the element name off its exports, so xmlns:adw="~/adw" needs an app-local app/adw.ts that re-exports what the markup names. A bare package specifier there does not resolve. There is one barrel per library, ~/adw for Adw.* and ~/gtk for Gtk.*, because the prefix is the only thing in XML that can say which library a widget belongs to. Point both at one module and <adw:Button> silently builds the GTK button.

The wrap box only recently joined them. @gjsify/gtk-host had no child policy for AdwWrapBox, so a child placed in one was refused by name. That was deliberate: a renderer guessing the policy would call add, append or set_child and be wrong at exit 0. The policy is now curated against libadwaita, so the block has a tree like the other three.

React Native primitives go in these containers too

The Adwaita element is the same one the React tab shows, and a <View> or <Text> sits in its default slot. The clamp, the header bar and the toolbar view carried a tab for that pairing, and they were the only three blocks on the site with one, so the React Native page carries it in full instead. Two facts from there are worth having before you try it. The primitives need their token scales installed with configureStyle({ tokens }) before the first render, because a class naming a token you have not declared throws out of the render and leaves an empty window at exit 0 with nothing in the log. And such a tree is a GTK window rather than a phone screen, which is why it was never the NativeScript tab.

The window with one thing added: the menubar its application installed. Turn show menubar off and the bar goes away; turn it on and the window draws it, which is all Gtk.ApplicationWindow:show-menubar decides.

A browser has no application

On GTK the model belongs to a Gtk.Application, which installs it into every application window it opens — a document has no application, so <adw-application-window> takes the menu model itself, in the same portable form <gtk-menu-button> takes, and reads it with the same parser. A menubar is a row of menu links: each one becomes a button that opens its menu, and a link’s own items are what that menu holds.

Blueprint
Code
using Gtk 4.0;
using Adw 1;
Adw.ApplicationWindow {
// The menubar is the APPLICATION's, so a `.blp` cannot spell it: a section of a
// Gio.Menu is a `menu` object and the window takes a PopoverMenuBar, and both are
// installed by the code that owns the window. The flag is all markup carries.
show-menubar: true;
default-width: 440;
default-height: 240;
content: Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.WindowTitle {
title: _("Text Editor");
subtitle: _("notes.md");
};
}
content: Adw.StatusPage {
title: _("Your Library");
description: _("Content sits between the toolbars and scrolls independently of them.");
};
};
}
TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gio from 'gi://Gio?version=2.0';
import Gtk from 'gi://Gtk?version=4.0';
Adw.init();
// The menubar model: a row of LINKS, which is what `Gtk.Application:menubar` is.
const menu = new Gio.Menu();
const file = new Gio.Menu();
file.append('New Window', 'app.new-window');
file.append('Quit', 'app.quit');
menu.append_section(null, file);
// Gtk.MenuBar is gone from GTK 4; the row is a Gtk.PopoverMenuBar. An app never
// adds it — `Gtk.Application` installs the model at construction — and this boolean
// is what decides whether the window DRAWS it.
const menubar = new Gtk.PopoverMenuBar({ menu_model: menu });
// The window has ONE child, its content. The menubar is the toolbar view's second
// top bar, which is where an application window's own menubar sits.
const toolbarView = new Adw.ToolbarView();
toolbarView.add_top_bar(new Adw.HeaderBar());
toolbarView.add_top_bar(menubar);
toolbarView.content = new Adw.StatusPage({ title: 'Your Library' });
const window = new Adw.ApplicationWindow({ content: toolbarView, show_menubar: true });
window.set_default_size(440, 240);
Web Components
HTML
<!-- show-menubar: If this property is true, the window will display a menubar unless it is shown by the desktop shell. -->
<adw-application-window width="440" height="240" show-menubar menu-model='[{"label":"File","submenu":[{"label":"New Window","action":"app.new-window"},{"label":"Quit","action":"app.quit"}]},{"label":"Help","submenu":[{"label":"Keyboard Shortcuts","action":"app.shortcuts"},{"label":"About","action":"app.about"}]}]'>
<adw-toolbar-view>
<adw-header-bar slot="top">
<adw-window-title slot="title" title="Text Editor" subtitle="notes.md"></adw-window-title>
</adw-header-bar>
<!-- description: The description markup to be displayed below the title. -->
<adw-status-page
slot="content"
title="Your Library"
description="Content sits between the toolbars and scrolls independently of them."
></adw-status-page>
</adw-toolbar-view>
</adw-application-window>

The smallest container libadwaita ships: a widget that holds one child and nothing else. It draws no frame of its own and hands the child its whole allocation, which makes it the thing to reach for when a slot needs a single replaceable widget. Setting a new child replaces the old one rather than stacking beside it, and a child that already has a parent is refused.

Adw.Bin
Blueprint
Code
using Gtk 4.0;
using Adw 1;
Adw.Bin {
Gtk.Label {
label: _("The one child of this bin.");
}
}
TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gtk from 'gi://Gtk?version=4.0';
const bin = new Adw.Bin({ child: new Gtk.Label({ label: 'The one child of this bin.' }) });
// A second child REPLACES the first.
bin.child = new Gtk.Label({ label: 'The first child is gone.' });
Web Components
HTML
<adw-bin style="width:260px;">
<gtk-label label="The one child of this bin."></gtk-label>
</adw-bin>
Solid
TypeScript
// mount(() => <AdwBin />, container) — from '@gjsify/gtk-host/solid'
const AdwBin = () => (
<adw-bin>
<gtk-label
label="The one child of this bin."
wrap
xalign={0}
cssClasses={['card']}
/>
</adw-bin>
);
Vue
Vue
<!-- AdwBin.vue — mount(AdwBin, container) from '@gjsify/gtk-host/vue' -->
<template>
<adw-bin>
<gtk-label
label="The one child of this bin."
:wrap="true"
:xalign="0"
:css-classes="['card']"
/>
</adw-bin>
</template>
React
TypeScript
// createRoot(container).render(<AdwBin />) — from '@gjsify/gtk-host/react'
const AdwBin = () => (
<adw-bin>
<gtk-label
label="The one child of this bin."
wrap
xalign={0}
cssClasses={['card']}
/>
</adw-bin>
);

Wrap long-form content in a clamp so it stops growing at a maximum width and stays centred in anything wider. Below the tightening threshold the clamp starts tightening the child, which keeps text at a comfortable measure on a large display without wasting room on a small one.

Why this block is shaped differently

The layout exists once, as a Blueprint file, and every surface builds that file: GTK compiles it to GtkBuilder XML, and the web and NativeScript ports build its ?shared-tree projection. The preview is that projection, built by @gjsify/adwaita-web, and Blueprint under it is the file. The Web Components and NativeScript tabs of Code hold the files of a project, picked in the small row under the tab bar. The first is the same layout in the port’s own markup, index.html or views/clamp.xml, generated from the Blueprint file when the site is built, so the ids match. The code that goes with it reaches an element by its id, which is where the platforms differ: the web clamp takes its size as an attribute, the other two as a property. The file marked (Blueprint) is that same code building the layout from the Blueprint file instead of the markup.

The first tab is one program for four runtimes. Built with gjsify build --app gjs it runs on GJS, and built with --app node the same source runs on Node.js, Deno and Bun through node-gi. build() calls Adw.init() itself, so the builder knows Adw.Clamp in a bare script too; inside an Adw.Application the call is harmless.

Adw.Clamp
Blueprint
Code
using Gtk 4.0;
using Adw 1;
Adw.Clamp clamp {
maximum-size: 400;
tightening-threshold: 300;
child: Gtk.Label label {
label: _("This content is clamped: it stops growing past the maximum size and stays centred.");
wrap: true;
styles ["card"]
};
}
TypeScript
TypeScript
import { build } from './clamp.blp';
const { clamp, label } = build();
label.label = 'Reached by its Blueprint id.';
clamp.maximumSize = 560;
Web Components
index.html
HTML
<adw-clamp id="clamp" maximum-size="400" tightening-threshold="300">
<gtk-label
id="label"
label="This content is clamped: it stops growing past the maximum size and stays centred."
wrap
class="card"
slot="child"
></gtk-label>
</adw-clamp>
<script type="module">
import '@gjsify/adwaita-web';
const clamp = document.querySelector('#clamp');
const label = document.querySelector('#label');
label.label = 'Reached by its id.';
clamp.setAttribute('maximum-size', '560');
</script>
main.js (Blueprint)
JavaScript
import { mountSharedTree } from '@gjsify/adwaita-web';
import tree from './clamp.blp?shared-tree';
mountSharedTree(tree);
const clamp = document.querySelector('#clamp');
const label = document.querySelector('#label');
label.label = 'Reached by its id.';
clamp.setAttribute('maximum-size', '560');
NativeScript
views/clamp.xml
XML
<adw:Clamp
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:adw="~/adw"
xmlns:gtk="~/gtk"
id="clamp"
maximumSize="400"
tighteningThreshold="300"
>
<adw:Clamp.child>
<gtk:Label
id="label"
label="This content is clamped: it stops growing past the maximum size and stays centred."
wrap="true"
styleClasses="card"
/>
</adw:Clamp.child>
</adw:Clamp>
app.ts
TypeScript
import { type Adw, type Gtk } from '@gjsify/adwaita-nativescript';
import { Builder } from '@nativescript/core';
const clamp = Builder.load({ path: '~/views', name: 'clamp' }) as Adw.Clamp;
const label = clamp.getViewById<Gtk.Label>('label')!;
label.label = 'Reached by its id.';
clamp.maximumSize = 560;
app.ts (Blueprint)
TypeScript
import { type Adw, type Gtk } from '@gjsify/adwaita-nativescript';
import { build } from '@gjsify/adwaita-nativescript/builder';
import tree from './clamp.blp?shared-tree';
const clamp = build(tree) as Adw.Clamp;
const label = clamp.getViewById<Gtk.Label>('label')!;
label.label = 'Reached by its id.';
clamp.maximumSize = 560;
Solid
TypeScript
// mount(() => <AdwClamp />, container) — from '@gjsify/gtk-host/solid'
const AdwClamp = () => (
<adw-clamp
maximumSize={400}
tighteningThreshold={300}
>
<gtk-label
label="This content is clamped: it stops growing past the maximum size and stays centred."
wrap
xalign={0}
cssClasses={['card']}
/>
</adw-clamp>
);
Vue
Vue
<!-- AdwClamp.vue — mount(AdwClamp, container) from '@gjsify/gtk-host/vue' -->
<template>
<adw-clamp
:maximum-size="400"
:tightening-threshold="300"
>
<gtk-label
label="This content is clamped: it stops growing past the maximum size and stays centred."
:wrap="true"
:xalign="0"
:css-classes="['card']"
/>
</adw-clamp>
</template>
React
TypeScript
// createRoot(container).render(<AdwClamp />) — from '@gjsify/gtk-host/react'
const AdwClamp = () => (
<adw-clamp
maximumSize={400}
tighteningThreshold={300}
>
<gtk-label
label="This content is clamped: it stops growing past the maximum size and stays centred."
wrap
xalign={0}
cssClasses={['card']}
/>
</adw-clamp>
);

A clamp for a child that scrolls. A plain clamp would cut a list or a text view off at its maximum width, scrollbar included; the scrollable clamp keeps the scrollbar at the edge of the window and holds only the content at the maximum size, with the same tightening threshold curve as the clamp above. Its child has to be a scrollable one, a list view or a text view.

Blueprint
Code
using Gtk 4.0;
using Adw 1;
Gtk.ScrolledWindow {
Adw.ClampScrollable {
maximum-size: 320;
tightening-threshold: 240;
Gtk.TextView {
editable: false;
}
}
}
TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gtk from 'gi://Gtk?version=4.0';
const text = new Gtk.TextView({ editable: false });
const clamp = new Adw.ClampScrollable({ maximumSize: 320, tighteningThreshold: 240, child: text });
// The scrolled window owns the scrollbars; the clamp only limits the content.
const scrolled = new Gtk.ScrolledWindow({ child: clamp });
Web Components
HTML
<!-- maximum-size: The maximum size allocated to the child. -->
<!-- tightening-threshold: The size above which the child is clamped. -->
<adw-clamp-scrollable maximum-size="320" tightening-threshold="240" style="width:100%;height:120px;">
<gtk-box orientation="vertical" spacing="6">
<gtk-label label="Line 1: held at the clamp's width."></gtk-label>
<gtk-label label="Line 2: while the rest scrolls."></gtk-label>
<gtk-label label="Line 3: and the scrollbar stays at the edge."></gtk-label>
<gtk-label label="Line 4: of the window."></gtk-label>
<gtk-label label="Line 5: not at the edge of the content."></gtk-label>
</gtk-box>
</adw-clamp-scrollable>

The standard top chrome of an Adwaita window. Put a title widget in the centre, usually a window title with a subtitle, and pack controls to the start and end edges: navigation on the leading side, menu and action buttons on the trailing one.

Adw.HeaderBar
Blueprint
Code
using Gtk 4.0;
using Adw 1;
Adw.HeaderBar header_bar {
title-widget: Adw.WindowTitle window_title {
title: _("Text Editor");
subtitle: "notes.md";
};
[start]
Gtk.Button back_button {
icon-name: "go-previous-symbolic";
styles ["flat"]
}
[end]
Gtk.MenuButton menu_button {
icon-name: "open-menu-symbolic";
styles ["flat"]
}
}
TypeScript
TypeScript
import Gio from 'gi://Gio?version=2.0';
import { build } from './header-bar.blp';
const { window_title, menu_button } = build();
window_title.subtitle = 'draft.md';
const menu = new Gio.Menu();
menu.append('New Window', 'app.new-window');
menu.append('Preferences', 'app.preferences');
menu.append('About', 'app.about');
menu_button.menuModel = menu;
Web Components
index.html
HTML
<adw-header-bar id="header_bar">
<adw-window-title
id="window_title"
title="Text Editor"
subtitle="notes.md"
slot="title-widget"
></adw-window-title>
<gtk-button
id="back_button"
icon-name="go-previous-symbolic"
class="flat"
slot="start"
></gtk-button>
<gtk-menu-button
id="menu_button"
icon-name="open-menu-symbolic"
class="flat"
slot="end"
></gtk-menu-button>
</adw-header-bar>
<script type="module">
import '@gjsify/adwaita-web';
const title = document.querySelector('#window_title');
const menuButton = document.querySelector('#menu_button');
title.setAttribute('subtitle', 'draft.md');
menuButton.menuModel = [
{ label: 'New Window', action: 'app.new-window' },
{ label: 'Preferences', action: 'app.preferences' },
{ label: 'About', action: 'app.about' },
];
</script>
main.js (Blueprint)
JavaScript
import { mountSharedTree } from '@gjsify/adwaita-web';
import tree from './header-bar.blp?shared-tree';
mountSharedTree(tree);
const title = document.querySelector('#window_title');
const menuButton = document.querySelector('#menu_button');
title.setAttribute('subtitle', 'draft.md');
menuButton.menuModel = [
{ label: 'New Window', action: 'app.new-window' },
{ label: 'Preferences', action: 'app.preferences' },
{ label: 'About', action: 'app.about' },
];
NativeScript
views/header-bar.xml
XML
<adw:HeaderBar
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:adw="~/adw"
xmlns:gtk="~/gtk"
id="header_bar"
>
<adw:HeaderBar.title-widget>
<adw:WindowTitle
id="window_title"
title="Text Editor"
subtitle="notes.md"
/>
</adw:HeaderBar.title-widget>
<adw:HeaderBar.start>
<gtk:Button
id="back_button"
iconName="go-previous-symbolic"
styleClasses="flat"
/>
</adw:HeaderBar.start>
<adw:HeaderBar.end>
<gtk:MenuButton
id="menu_button"
iconName="open-menu-symbolic"
styleClasses="flat"
/>
</adw:HeaderBar.end>
</adw:HeaderBar>
app.ts
TypeScript
import { type Adw, type Gtk } from '@gjsify/adwaita-nativescript';
import { Builder } from '@nativescript/core';
const headerBar = Builder.load({ path: '~/views', name: 'header-bar' }) as Adw.HeaderBar;
const title = headerBar.getViewById<Adw.WindowTitle>('window_title')!;
const menuButton = headerBar.getViewById<Gtk.MenuButton>('menu_button')!;
title.subtitle = 'draft.md';
menuButton.menuModel = [
{ label: 'New Window', action: 'app.new-window' },
{ label: 'Preferences', action: 'app.preferences' },
{ label: 'About', action: 'app.about' },
];
app.ts (Blueprint)
TypeScript
import { type Adw, type Gtk } from '@gjsify/adwaita-nativescript';
import { build } from '@gjsify/adwaita-nativescript/builder';
import tree from './header-bar.blp?shared-tree';
const headerBar = build(tree) as Adw.HeaderBar;
const title = headerBar.getViewById<Adw.WindowTitle>('window_title')!;
const menuButton = headerBar.getViewById<Gtk.MenuButton>('menu_button')!;
title.subtitle = 'draft.md';
menuButton.menuModel = [
{ label: 'New Window', action: 'app.new-window' },
{ label: 'Preferences', action: 'app.preferences' },
{ label: 'About', action: 'app.about' },
];
Solid
TypeScript
// mount(() => <AdwHeaderBar />, container) — from '@gjsify/gtk-host/solid'
const AdwHeaderBar = () => (
<adw-header-bar>
<gtk-button
slot="start"
iconName="go-previous-symbolic"
cssClasses={['flat']}
/>
<adw-window-title
slot="title"
title="Text Editor"
subtitle="notes.md"
/>
<gtk-menu-button
slot="end"
iconName="open-menu-symbolic"
cssClasses={['flat']}
/>
</adw-header-bar>
);
Vue
Vue
<!-- AdwHeaderBar.vue — mount(AdwHeaderBar, container) from '@gjsify/gtk-host/vue' -->
<template>
<adw-header-bar>
<gtk-button
slot="start"
icon-name="go-previous-symbolic"
:css-classes="['flat']"
/>
<adw-window-title
slot="title"
title="Text Editor"
subtitle="notes.md"
/>
<gtk-menu-button
slot="end"
icon-name="open-menu-symbolic"
:css-classes="['flat']"
/>
</adw-header-bar>
</template>
React
TypeScript
// createRoot(container).render(<AdwHeaderBar />) — from '@gjsify/gtk-host/react'
const AdwHeaderBar = () => (
<adw-header-bar>
<gtk-button
slot="start"
iconName="go-previous-symbolic"
cssClasses={['flat']}
/>
<adw-window-title
slot="title"
title="Text Editor"
subtitle="notes.md"
/>
<gtk-menu-button
slot="end"
iconName="open-menu-symbolic"
cssClasses={['flat']}
/>
</adw-header-bar>
);

Frame your content between a top bar and a bottom bar, typically a header bar above and an action bar below. Each set of bars reveals and hides independently, and the content scrolls underneath, so the chrome stays put while the view moves.

Adw.ToolbarView
Blueprint
Code
using Gtk 4.0;
using Adw 1;
Adw.ToolbarView {
[top]
Adw.HeaderBar {
title-widget: Adw.WindowTitle {
title: _("Documents");
subtitle: _("12 items");
};
}
content: Adw.StatusPage {
icon-name: "folder-documents-symbolic";
title: _("Your library");
description: _("Content sits between the toolbars and scrolls independently of them.");
};
[bottom]
Gtk.ActionBar {
[start]
Gtk.Button add_button {
icon-name: "list-add-symbolic";
tooltip-text: _("Add");
styles ["flat"]
}
[start]
Gtk.Button {
icon-name: "list-remove-symbolic";
tooltip-text: _("Remove");
styles ["flat"]
}
[center]
Gtk.Label selection {
label: _("Selection: none");
}
[end]
Gtk.Button {
icon-name: "send-to-symbolic";
tooltip-text: _("Share");
styles ["flat"]
}
}
}
TypeScript
TypeScript
import { build } from './toolbar-view.blp';
const { add_button, selection } = build();
add_button.connect('clicked', () => {
selection.label = 'Selection: 1 item';
});
Web Components
index.html
HTML
<adw-toolbar-view>
<adw-header-bar slot="top">
<adw-window-title
title="Documents"
subtitle="12 items"
slot="title-widget"
></adw-window-title>
</adw-header-bar>
<adw-status-page
icon-name="folder-documents-symbolic"
title="Your library"
description="Content sits between the toolbars and scrolls independently of them."
slot="content"
></adw-status-page>
<gtk-action-bar slot="bottom">
<gtk-button
id="add_button"
icon-name="list-add-symbolic"
tooltip-text="Add"
class="flat"
slot="start"
></gtk-button>
<gtk-button
icon-name="list-remove-symbolic"
tooltip-text="Remove"
class="flat"
slot="start"
></gtk-button>
<gtk-label id="selection" label="Selection: none" slot="center"></gtk-label>
<gtk-button
icon-name="send-to-symbolic"
tooltip-text="Share"
class="flat"
slot="end"
></gtk-button>
</gtk-action-bar>
</adw-toolbar-view>
<script type="module">
import '@gjsify/adwaita-web';
const add = document.querySelector('#add_button');
const selection = document.querySelector('#selection');
add.addEventListener('click', () => {
selection.label = 'Selection: 1 item';
});
</script>
main.js (Blueprint)
JavaScript
import { mountSharedTree } from '@gjsify/adwaita-web';
import tree from './toolbar-view.blp?shared-tree';
mountSharedTree(tree);
const add = document.querySelector('#add_button');
const selection = document.querySelector('#selection');
add.addEventListener('click', () => {
selection.label = 'Selection: 1 item';
});
NativeScript
views/toolbar-view.xml
XML
<adw:ToolbarView
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:adw="~/adw"
xmlns:gtk="~/gtk"
>
<adw:ToolbarView.top>
<adw:HeaderBar>
<adw:HeaderBar.title-widget>
<adw:WindowTitle title="Documents" subtitle="12 items" />
</adw:HeaderBar.title-widget>
</adw:HeaderBar>
</adw:ToolbarView.top>
<adw:ToolbarView.content>
<adw:StatusPage
iconName="folder-documents-symbolic"
title="Your library"
description="Content sits between the toolbars and scrolls independently of them."
/>
</adw:ToolbarView.content>
<adw:ToolbarView.bottom>
<gtk:ActionBar>
<gtk:ActionBar.start>
<gtk:Button
id="add_button"
iconName="list-add-symbolic"
tooltipText="Add"
styleClasses="flat"
/>
</gtk:ActionBar.start>
<gtk:ActionBar.start>
<gtk:Button
iconName="list-remove-symbolic"
tooltipText="Remove"
styleClasses="flat"
/>
</gtk:ActionBar.start>
<gtk:ActionBar.center>
<gtk:Label id="selection" label="Selection: none" />
</gtk:ActionBar.center>
<gtk:ActionBar.end>
<gtk:Button
iconName="send-to-symbolic"
tooltipText="Share"
styleClasses="flat"
/>
</gtk:ActionBar.end>
</gtk:ActionBar>
</adw:ToolbarView.bottom>
</adw:ToolbarView>
app.ts
TypeScript
import { type Adw, type Gtk } from '@gjsify/adwaita-nativescript';
import { Builder } from '@nativescript/core';
const view = Builder.load({ path: '~/views', name: 'toolbar-view' }) as Adw.ToolbarView;
const add = view.getViewById<Gtk.Button>('add_button')!;
const selection = view.getViewById<Gtk.Label>('selection')!;
add.connect('clicked', () => {
selection.label = 'Selection: 1 item';
});
app.ts (Blueprint)
TypeScript
import { type Adw, type Gtk } from '@gjsify/adwaita-nativescript';
import { build } from '@gjsify/adwaita-nativescript/builder';
import tree from './toolbar-view.blp?shared-tree';
const view = build(tree) as Adw.ToolbarView;
const add = view.getViewById<Gtk.Button>('add_button')!;
const selection = view.getViewById<Gtk.Label>('selection')!;
add.connect('clicked', () => {
selection.label = 'Selection: 1 item';
});
Solid
TypeScript
// mount(() => <AdwToolbarView />, container) — from '@gjsify/gtk-host/solid'
const AdwToolbarView = () => (
<adw-toolbar-view>
<adw-header-bar slot="top">
<adw-window-title
slot="title"
title="Documents"
subtitle="12 items"
/>
</adw-header-bar>
<adw-status-page
slot="content"
iconName="folder-documents-symbolic"
title="Your library"
description="Content sits between the toolbars and scrolls independently of them."
/>
<gtk-box
slot="bottom"
cssClasses={['toolbar']}
spacing={6}
>
<gtk-button
iconName="list-add-symbolic"
cssClasses={['flat']}
/>
<gtk-button
iconName="list-remove-symbolic"
cssClasses={['flat']}
/>
<gtk-label
label="Selection: none"
hexpand
/>
<gtk-button
iconName="send-to-symbolic"
cssClasses={['flat']}
/>
</gtk-box>
</adw-toolbar-view>
);
Vue
Vue
<!-- AdwToolbarView.vue — mount(AdwToolbarView, container) from '@gjsify/gtk-host/vue' -->
<template>
<adw-toolbar-view>
<adw-header-bar slot="top">
<adw-window-title
slot="title"
title="Documents"
subtitle="12 items"
/>
</adw-header-bar>
<adw-status-page
slot="content"
icon-name="folder-documents-symbolic"
title="Your library"
description="Content sits between the toolbars and scrolls independently of them."
/>
<gtk-box
slot="bottom"
:css-classes="['toolbar']"
:spacing="6"
>
<gtk-button
icon-name="list-add-symbolic"
:css-classes="['flat']"
/>
<gtk-button
icon-name="list-remove-symbolic"
:css-classes="['flat']"
/>
<gtk-label
label="Selection: none"
:hexpand="true"
/>
<gtk-button
icon-name="send-to-symbolic"
:css-classes="['flat']"
/>
</gtk-box>
</adw-toolbar-view>
</template>
React
TypeScript
// createRoot(container).render(<AdwToolbarView />) — from '@gjsify/gtk-host/react'
const AdwToolbarView = () => (
<adw-toolbar-view>
<adw-header-bar slot="top">
<adw-window-title
slot="title"
title="Documents"
subtitle="12 items"
/>
</adw-header-bar>
<adw-status-page
slot="content"
iconName="folder-documents-symbolic"
title="Your library"
description="Content sits between the toolbars and scrolls independently of them."
/>
<gtk-box
slot="bottom"
cssClasses={['toolbar']}
spacing={6}
>
<gtk-button
iconName="list-add-symbolic"
cssClasses={['flat']}
/>
<gtk-button
iconName="list-remove-symbolic"
cssClasses={['flat']}
/>
<gtk-label
label="Selection: none"
hexpand
/>
<gtk-button
iconName="send-to-symbolic"
cssClasses={['flat']}
/>
</gtk-box>
</adw-toolbar-view>
);

Lay children out in a horizontal line and let them wrap onto new lines when they run out of room. This is what you want for a variable-length run of tags, chips or filter pills. Child spacing sets the gap between items on a line, line spacing the gap between the lines.

Adw.WrapBox
Blueprint
Code
using Gtk 4.0;
using Adw 1;
Adw.WrapBox wrap_box {
child-spacing: 8;
line-spacing: 8;
Gtk.Button {
label: _("Design");
styles ["pill"]
}
Gtk.Button {
label: _("Adwaita");
styles ["pill"]
}
Gtk.Button {
label: _("GNOME");
styles ["pill"]
}
Gtk.Button {
label: _("GTK");
styles ["pill"]
}
Gtk.Button {
label: _("TypeScript");
styles ["pill"]
}
Gtk.Button {
label: _("Storybook");
styles ["pill"]
}
Gtk.Button {
label: _("Wrapping");
styles ["pill"]
}
Gtk.Button {
label: _("Layout");
styles ["pill"]
}
}
TypeScript
TypeScript
import Gtk from 'gi://Gtk?version=4.0';
import { build } from './wrap-box.blp';
const { wrap_box } = build();
const chip = new Gtk.Button({ label: 'Blueprint' });
chip.add_css_class('pill');
wrap_box.append(chip);
wrap_box.childSpacing = 12;
Web Components
index.html
HTML
<adw-wrap-box id="wrap_box" child-spacing="8" line-spacing="8">
<gtk-button label="Design" class="pill"></gtk-button>
<gtk-button label="Adwaita" class="pill"></gtk-button>
<gtk-button label="GNOME" class="pill"></gtk-button>
<gtk-button label="GTK" class="pill"></gtk-button>
<gtk-button label="TypeScript" class="pill"></gtk-button>
<gtk-button label="Storybook" class="pill"></gtk-button>
<gtk-button label="Wrapping" class="pill"></gtk-button>
<gtk-button label="Layout" class="pill"></gtk-button>
</adw-wrap-box>
<script type="module">
import '@gjsify/adwaita-web';
const wrap = document.querySelector('#wrap_box');
const chip = document.createElement('gtk-button');
chip.setAttribute('label', 'Blueprint');
chip.classList.add('pill');
wrap.append(chip);
wrap.childSpacing = 12;
</script>
main.js (Blueprint)
JavaScript
import { mountSharedTree } from '@gjsify/adwaita-web';
import tree from './wrap-box.blp?shared-tree';
mountSharedTree(tree);
const wrap = document.querySelector('#wrap_box');
const chip = document.createElement('gtk-button');
chip.setAttribute('label', 'Blueprint');
chip.classList.add('pill');
wrap.append(chip);
wrap.childSpacing = 12;
NativeScript
views/wrap-box.xml
XML
<adw:WrapBox
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:adw="~/adw"
xmlns:gtk="~/gtk"
id="wrap_box"
childSpacing="8"
lineSpacing="8"
>
<gtk:Button label="Design" styleClasses="pill" />
<gtk:Button label="Adwaita" styleClasses="pill" />
<gtk:Button label="GNOME" styleClasses="pill" />
<gtk:Button label="GTK" styleClasses="pill" />
<gtk:Button label="TypeScript" styleClasses="pill" />
<gtk:Button label="Storybook" styleClasses="pill" />
<gtk:Button label="Wrapping" styleClasses="pill" />
<gtk:Button label="Layout" styleClasses="pill" />
</adw:WrapBox>
app.ts
TypeScript
import { Gtk, type Adw } from '@gjsify/adwaita-nativescript';
import { Builder } from '@nativescript/core';
const wrap = Builder.load({ path: '~/views', name: 'wrap-box' }) as Adw.WrapBox;
const chip = new Gtk.Button({ label: 'Blueprint' });
chip.add_css_class('pill');
wrap.append(chip);
wrap.childSpacing = 12;
app.ts (Blueprint)
TypeScript
import { Gtk, type Adw } from '@gjsify/adwaita-nativescript';
import { build } from '@gjsify/adwaita-nativescript/builder';
import tree from './wrap-box.blp?shared-tree';
const wrap = build(tree) as Adw.WrapBox;
const chip = new Gtk.Button({ label: 'Blueprint' });
chip.add_css_class('pill');
wrap.append(chip);
wrap.childSpacing = 12;
Solid
TypeScript
// mount(() => <AdwWrapBox />, container) — from '@gjsify/gtk-host/solid'
const AdwWrapBox = () => (
<adw-wrap-box
childSpacing={8}
lineSpacing={8}
>
<gtk-button
label="Design"
cssClasses={['pill']}
/>
<gtk-button
label="Adwaita"
cssClasses={['pill']}
/>
<gtk-button
label="GNOME"
cssClasses={['pill']}
/>
<gtk-button
label="GTK"
cssClasses={['pill']}
/>
<gtk-button
label="TypeScript"
cssClasses={['pill']}
/>
<gtk-button
label="Storybook"
cssClasses={['pill']}
/>
<gtk-button
label="Wrapping"
cssClasses={['pill']}
/>
<gtk-button
label="Layout"
cssClasses={['pill']}
/>
</adw-wrap-box>
);
Vue
Vue
<!-- AdwWrapBox.vue — mount(AdwWrapBox, container) from '@gjsify/gtk-host/vue' -->
<template>
<adw-wrap-box
:child-spacing="8"
:line-spacing="8"
>
<gtk-button
label="Design"
:css-classes="['pill']"
/>
<gtk-button
label="Adwaita"
:css-classes="['pill']"
/>
<gtk-button
label="GNOME"
:css-classes="['pill']"
/>
<gtk-button
label="GTK"
:css-classes="['pill']"
/>
<gtk-button
label="TypeScript"
:css-classes="['pill']"
/>
<gtk-button
label="Storybook"
:css-classes="['pill']"
/>
<gtk-button
label="Wrapping"
:css-classes="['pill']"
/>
<gtk-button
label="Layout"
:css-classes="['pill']"
/>
</adw-wrap-box>
</template>
React
TypeScript
// createRoot(container).render(<AdwWrapBox />) — from '@gjsify/gtk-host/react'
const AdwWrapBox = () => (
<adw-wrap-box
childSpacing={8}
lineSpacing={8}
>
<gtk-button
label="Design"
cssClasses={['pill']}
/>
<gtk-button
label="Adwaita"
cssClasses={['pill']}
/>
<gtk-button
label="GNOME"
cssClasses={['pill']}
/>
<gtk-button
label="GTK"
cssClasses={['pill']}
/>
<gtk-button
label="TypeScript"
cssClasses={['pill']}
/>
<gtk-button
label="Storybook"
cssClasses={['pill']}
/>
<gtk-button
label="Wrapping"
cssClasses={['pill']}
/>
<gtk-button
label="Layout"
cssClasses={['pill']}
/>
</adw-wrap-box>
);

Give breakpoints a home without a window. The bin holds breakpoints, each a condition on its own size plus the properties to write while that condition holds, and on every resize it applies the last one that matches — not the narrowest. Its single child is whatever rearranges itself.

The markup form is breakpoints, a JSON array because Adw.Breakpoint is a GObject rather than a widget and add_breakpoint() is the whole upstream API. Each setter names a target (a selector for a descendant), an attribute property and its value; the value the target held when the setter was registered is what leaving the breakpoint restores.

A bin with breakpoints has no minimum size

Adding a breakpoint makes the bin measure no minimum on either axis, and the AdwBreakpointBin docs say the stage must set width-request / height-request for the smallest size you support. The preview does that with inline styles, which is why the stage under this block is 300px wide: narrow it far enough and the label swaps.

Adw.BreakpointBin
Blueprint
Code
using Gtk 4.0;
using Adw 1;
Adw.BreakpointBin {
width-request: 150;
height-request: 60;
Gtk.Label caption {
label: _("Wide");
ellipsize: end;
styles ["title-1"]
}
}
TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gtk from 'gi://Gtk?version=4.0';
const bin = new Adw.BreakpointBin();
bin.set_size_request(150, 60);
const caption = new Gtk.Label({ label: 'Wide', ellipsize: Gtk.EllipsizeMode.END });
caption.add_css_class('title-1');
bin.set_child(caption);
// A breakpoint writes properties while its condition holds, and the LAST
// one that matches wins. Leaving it restores the values the setters
// captured when they were added.
const narrow = new Adw.Breakpoint(Adw.BreakpointCondition.parse('max-width: 200px'));
narrow.add_setter(caption, 'label', 'Narrow');
bin.add_breakpoint(narrow);
Web Components
HTML
<adw-breakpoint-bin style="min-width: 150px; height: 60px;" breakpoints='[{"condition":"max-width: 200px","setters":[{"target":"#caption","property":"label","value":"Narrow"}]}]'>
<gtk-label id="caption" label="Wide" class="title-1"></gtk-label>
</adw-breakpoint-bin>
Solid
TypeScript
// mount(() => <AdwBreakpointBin />, container) — from '@gjsify/gtk-host/solid'
const AdwBreakpointBin = () => (
<adw-breakpoint-bin>
<gtk-label
label="Wide"
ellipsize="end"
cssClasses={['title-1']}
/>
</adw-breakpoint-bin>
);
Vue
Vue
<!-- AdwBreakpointBin.vue — mount(AdwBreakpointBin, container) from '@gjsify/gtk-host/vue' -->
<template>
<adw-breakpoint-bin>
<gtk-label
label="Wide"
ellipsize="end"
:css-classes="['title-1']"
/>
</adw-breakpoint-bin>
</template>
React
TypeScript
// createRoot(container).render(<AdwBreakpointBin />) — from '@gjsify/gtk-host/react'
const AdwBreakpointBin = () => (
<adw-breakpoint-bin>
<gtk-label
label="Wide"
ellipsize="end"
cssClasses={['title-1']}
/>
</adw-breakpoint-bin>
);

One set of children, several arrangements of them. Every child has an id, every layout has slots with ids, and switching layouts re-inserts each child into the slot of the same id — which is how a secondary pane becomes a sidebar in one arrangement and part of the content in another without the application moving anything. layout-name picks the arrangement; the first one declared is the one in use until you say otherwise.

An Adw.Layout is a GObject, so there is no Adw.Layout tag: here a direct child carrying name is the layout, a direct child carrying slot is a child, and an <adw-layout-slot> inside a layout is where that child lands.

Why this block has no framework snippet

A layout is a GObject upstream and each child is paired with a slot by a method call, so neither half is a static tree in any of the dialects — the gallery ledger says so rather than publishing markup that would render nothing. The browser element carries both in markup, which is what the preview above shows.

Blueprint
Code
using Gtk 4.0;
using Adw 1;
Adw.MultiLayoutView {
layout-name: "wide";
Adw.NavigationSplitView {
min-sidebar-width: 160;
Adw.LayoutSlot {
id: "secondary";
}
Adw.LayoutSlot {
id: "primary";
}
}
}
TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
const wide = new Adw.NavigationSplitView({ minSidebarWidth: 160 });
wide.sidebar = new Adw.LayoutSlot('secondary');
wide.content = new Adw.LayoutSlot('primary');
const narrow = new Adw.NavigationSplitView({ collapsed: true, minSidebarWidth: 160 });
narrow.sidebar = new Adw.LayoutSlot('secondary');
narrow.content = new Adw.LayoutSlot('primary');
const view = new Adw.MultiLayoutView();
// The FIRST layout added is the one in use.
const first = new Adw.Layout(wide);
first.name = 'wide';
view.add_layout(first);
const second = new Adw.Layout(narrow);
second.name = 'narrow';
view.add_layout(second);
view.set_child('primary', new Adw.NavigationPage({ title: 'Inbox', tag: 'inbox' }));
view.set_child('secondary', new Adw.NavigationPage({ title: 'Message', tag: 'message' }));
Web Components
HTML
<adw-multi-layout-view layout-name="wide" style="width: 100%; height: 100%;">
<adw-navigation-split-view name="wide" style="width: 100%; height: 100%;">
<adw-layout-slot id="secondary" slot="sidebar"></adw-layout-slot>
<adw-layout-slot id="primary" slot="content"></adw-layout-slot>
</adw-navigation-split-view>
<adw-navigation-split-view name="narrow" collapsed style="width: 100%; height: 100%;">
<adw-layout-slot id="secondary" slot="content"></adw-layout-slot>
<adw-layout-slot id="primary" slot="content"></adw-layout-slot>
</adw-navigation-split-view>
<adw-navigation-page slot="primary" tag="inbox" title="Inbox">
<gtk-label label="General — 12 unread"></gtk-label>
</adw-navigation-page>
<adw-navigation-page slot="secondary" tag="message" title="Message">
<gtk-label label="Resizing the window"></gtk-label>
</adw-navigation-page>
</adw-multi-layout-view>

The named hole one layout of a multi-layout view leaves for a child. It takes the child whose id matches its own, and its visibility follows that child’s — so hiding the child takes the hole with it and nothing more. The id is construct-only upstream, which is why it is the one thing this element reads.

A slot on its own is a bin, and that is what the preview shows. To see the pairing work, use the view above: the two slots in it are what the secondary and primary children land in.

Duplicate ids across layouts are the widget's own model

The example in the libadwaita documentation puts an AdwLayoutSlot with id="primary" in both layouts of one view — that is how a child follows a layout change. The element resolves slots from the layout it is currently mounting, never by looking an id up in the document, so two slots sharing an id are ordinary.

Blueprint
Code
using Gtk 4.0;
using Adw 1;
Adw.LayoutSlot {
id: "primary";
Gtk.Label {
label: _("The child the view inserted here.");
}
}
TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
// A slot only ever exists inside a layout, and a layout inside a view:
// the view is what inserts the child whose id matches the slot's.
const slot = new Adw.LayoutSlot('primary');
const layout = new Adw.Layout(slot);
layout.name = 'only';
const view = new Adw.MultiLayoutView();
view.add_layout(layout);
view.set_child('primary', new Adw.NavigationPage({ title: 'Inbox', tag: 'inbox' }));
Web Components
HTML
<adw-layout-slot id="primary">
<gtk-label label="The child the view inserted here."></gtk-label>
</adw-layout-slot>