Skip to content

Buttons

Libadwaita adds three button widgets to GTK’s own. Put an icon plus a label on a button with button content, use a split button to pair an action with a menu of alternatives, or reach for a toggle group when a row of linked buttons should act as a single-choice selector.

The plain button and the menu button belong to GTK. They are Gtk.Button and Gtk.MenuButton, documented under Gtk Buttons with the style classes that shape them.

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.

To put an icon and text on a button, make an Adw.ButtonContent its child. It pairs a symbolic icon with a label, so the action reads at a glance and in words. Set can-shrink if the label may ellipsize when horizontal space runs short.

Adw.ButtonContent
HTML
<button class="adw-button suggested-action pill">
<adw-button-content label="Download" icon-name="folder-download"></adw-button-content>
</button>
<adw-button-content> 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
icon-name folder-download
label Download
use-underline not used
can-shrink not used
Native TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gtk from 'gi://Gtk?version=4.0';
const content = new Adw.ButtonContent({
label: 'Download',
iconName: 'folder-download-symbolic',
canShrink: false,
});
const button = new Gtk.Button({ child: content });
button.add_css_class('suggested-action');
button.add_css_class('pill');
Code
using Gtk 4.0;
using Adw 1;
Gtk.Button {
child: Adw.ButtonContent {
label: _("Download");
icon-name: "folder-download-symbolic";
};
styles ["suggested-action", "pill"]
}
UI frameworks
TypeScript
// mount(() => <AdwButtonContent />, container) — from '@gjsify/gtk-host/solid'
const AdwButtonContent = () => (
<gtk-button cssClasses={['suggested-action', 'pill']}>
<adw-button-content
label="Download"
iconName="folder-download-symbolic"
/>
</gtk-button>
);
Vue
<!-- AdwButtonContent.vue — mount(AdwButtonContent, container) from '@gjsify/gtk-host/vue' -->
<template>
<gtk-button :css-classes="['suggested-action', 'pill']">
<adw-button-content
label="Download"
icon-name="folder-download-symbolic"
/>
</gtk-button>
</template>
TypeScript
// createRoot(container).render(<AdwButtonContent />) — from '@gjsify/gtk-host/react'
const AdwButtonContent = () => (
<gtk-button cssClasses={['suggested-action', 'pill']}>
<adw-button-content
label="Download"
iconName="folder-download-symbolic"
/>
</gtk-button>
);
NativeScript
TypeScript
import { AdwButtonContent } from '@gjsify/adwaita-nativescript';
import { folderDownloadSymbolic } from '@gjsify/adwaita-icons/places';
import { StackLayout } from '@nativescript/core';
const content = new AdwButtonContent();
content.iconColor = '#ffffff';
content.label = 'Download';
content.iconName = folderDownloadSymbolic; // an Adwaita symbolic icon (SVG string)
// GtkButton is text-only, so a styled layout wraps the content.
const button = new StackLayout();
button.orientation = 'horizontal';
button.className = 'adw-button suggested-action pill';
button.addChild(content);
XML
<adw:AdwButtonContent
xmlns="http://schemas.nativescript.org/tns.xsd"
xmlns:adw="~/adwaita"
id="download"
label="Download"
/>

Use a split button when one action is the obvious default and the rest are variations on it. Clicking the main half runs the default; the attached arrow opens a menu with the alternatives. Add .flat for the variant that belongs in a header bar or toolbar, where a raised button looks too heavy.

Adw.SplitButton
HTML
<adw-split-button
label="Save"
icon-name="document-save"
menu='[{"label":"Save as…","action":"app.save-as"},{"label":"Export","action":"app.export"},{"label":"Print","action":"app.print"}]'
></adw-split-button>
<adw-split-button> 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
label Save
icon-name document-save
use-underline not used
tooltip not used
dropdown-tooltip not used
direction not used
menu [{"label":"Save as…","action":"app.save-as"},{"label":"Export","action":"app.export"},{"label":"Print","action":"app.print"}]
disabled not used
flat not used
suggested not used
destructive not used
Native TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
import Gio from 'gi://Gio?version=2.0';
const menu = new Gio.Menu();
menu.append('Save as…', 'app.save-as');
menu.append('Export', 'app.export');
menu.append('Print', 'app.print');
const button = new Adw.SplitButton({ label: 'Save', menuModel: menu });
button.iconName = 'document-save-symbolic';
// button.add_css_class('flat'); // the header-bar / toolbar variant
Code
using Gtk 4.0;
using Adw 1;
Adw.SplitButton {
label: _("Save");
icon-name: "document-save-symbolic";
menu-model: save-menu;
}
menu save-menu {
section {
item {
label: _("Save as…");
action: "app.save-as";
}
item {
label: _("Export");
action: "app.export";
}
item {
label: _("Print");
action: "app.print";
}
}
}
UI frameworks

No Solid, Vue or React snippet for Adw.SplitButton: its menu is a Gio.MenuModel, built imperatively.

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 { AdwSplitButton } from '@gjsify/adwaita-nativescript';
import { documentSaveSymbolic } from '@gjsify/adwaita-icons/actions';
const button = new AdwSplitButton();
button.menu = ['Save as…', 'Export', 'Print'];
button.iconName = documentSaveSymbolic; // an Adwaita symbolic icon (SVG string)
// With no icon set, the label drives the action half: button.label = 'Save';
// button.className = 'adw-split-button flat'; // the header-bar / toolbar variant

No NativeScript XML template for Adw.SplitButton: AdwSplitButton.menu is an array of menu items; 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 a small set of mutually exclusive options, such as a view mode, use a toggle group: a linked row where exactly one toggle is active at a time. Each toggle takes an icon, a label, or both. The style classes .flat and .round adapt the group for a toolbar or give it a pill shape.

One divergence in the block below, and it runs the unusual way round: the third toggle’s view-columns-symbolic is in no icon theme, so only the web pane draws it — from a glyph @gjsify/adwaita-web hand-draws itself. The GJS and Blueprint panes name the same icon and get GTK’s broken-image paintable. It stays that way on purpose: dropping the hand-drawn glyph would put the web pane on image-missing as well, which is parity bought by making both panes worse.

A toggle group is a single tab stop, the way Adw.ToggleGroup routes focus: Tab enters on the active toggle and the next Tab leaves the group, while Left and Right move between toggles. Up and Down stay the page’s, as they are upstream. Home and End jump to the ends — that pair is the WAI-ARIA pattern rather than anything GTK does, since GTK’s focus directions have no Home or End. It announces itself as a radio group with one checked toggle — GTK_ACCESSIBLE_ROLE_RADIO_GROUP upstream — unless the element already carries a role when it connects, which it then keeps; role="tablist" additionally switches the toggles to tabs, the way Adw.InlineViewSwitcher does. Set it in markup or before the element is inserted: it is read once, which is all GTK allows too — GtkAccessible:accessible-role is documented “cannot be changed once set”, and the only setter is class-level.

Adw.ToggleGroup
HTML
<adw-toggle-group active="0">
<adw-toggle label="List" icon-name="view-list"></adw-toggle>
<adw-toggle label="Grid" icon-name="view-grid"></adw-toggle>
<adw-toggle label="Columns" icon-name="view-columns"></adw-toggle>
</adw-toggle-group>
<adw-toggle-group> 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 0
flat not used
round not used
Native TypeScript
TypeScript
import Adw from 'gi://Adw?version=1';
const group = new Adw.ToggleGroup();
group.add(new Adw.Toggle({ label: 'List', iconName: 'view-list-symbolic' }));
group.add(new Adw.Toggle({ label: 'Grid', iconName: 'view-grid-symbolic' }));
group.add(new Adw.Toggle({ label: 'Columns', iconName: 'view-columns-symbolic' }));
group.active = 0;
Code
using Gtk 4.0;
using Adw 1;
Adw.ToggleGroup {
active: 0;
Adw.Toggle {
label: _("List");
icon-name: "view-list-symbolic";
}
Adw.Toggle {
label: _("Grid");
icon-name: "view-grid-symbolic";
}
Adw.Toggle {
label: _("Columns");
icon-name: "view-columns-symbolic";
}
}
UI frameworks

No Solid, Vue or React snippet for Adw.ToggleGroup: uncurated-placement — and its toggles are AdwToggle 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 { AdwToggleGroup } from '@gjsify/adwaita-nativescript';
import { viewGridSymbolic, viewListSymbolic, viewPagedSymbolic } from '@gjsify/adwaita-icons/actions';
const group = new AdwToggleGroup();
group.setToggles([
{ label: 'List', icon: viewListSymbolic }, // Adwaita symbolic icons (SVG strings)
{ label: 'Grid', icon: viewGridSymbolic },
{ label: 'Columns', icon: viewPagedSymbolic },
]);
group.active = 0;

No NativeScript XML template for Adw.ToggleGroup: AdwToggleGroup.options is an array of toggles; 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.

  • Gtk Buttons: the plain button, its style classes and the menu button.
  • Boxed Lists: rows that embed these buttons and reuse the same style classes.
  • Layout: the containers that arrange buttons within a window.
  • Adwaita Storybook: the same widgets in a live component browser you can poke at.