This is the first proper entry in A Field Guide to GTK Widgets — a series about which widget to reach for, when, and what bites you when you do. We start where every app starts: the window. Each post stands on its own, and the complete, runnable code for this one lives in the companion repo.
Two windows, and the one you should reach for
You sit down to write a GNOME app. The very first widget you need is the window, and the toolkit immediately hands you a choice it doesn’t explain: GtkApplicationWindow, from GTK itself, or AdwApplicationWindow, from Libadwaita. The reference describes both accurately and tells you nothing about which one you want.
Here’s the short answer, so the rest of this post is the why: in 2026, for anything you intend to ship as a GNOME app, reach for AdwApplicationWindow. The reason isn’t cosmetics — it’s structural, and it changes how you build everything above it. The difference comes down to one thing: where the titlebar lives.
GtkApplicationWindow: the honest baseline
GtkApplicationWindow is the plain GTK window, and there’s nothing wrong with it. It has a content area and a dedicated titlebar slot, and you fill the slot with a header bar:
let header = gtk::HeaderBar::new();
let window = gtk::ApplicationWindow::builder()
.application(app)
.title("Application Shell")
.default_width(420)
.build();
window.set_titlebar(Some(&header));
window.set_child(Some(&content));
window.present();
Two slots, two setters: set_titlebar for the bar across the top, set_child for everything underneath. If you don’t call set_titlebar at all, GTK gives you a default one so the window is still draggable and closable. This works, it ships, and for a tool that doesn’t care about looking GNOME-native it’s entirely fine.
The limitation is baked into that titlebar slot. It’s a special region the window manages separately from its content — which means your header bar can never be anything other than a strip pinned to the very top, the full width of the window, for the whole life of the app. The moment you want a layout where the header is part of a collapsible sidebar, or where two panes each carry their own bar, the slot is in your way. That’s not a rare want in modern GNOME; it’s the standard adaptive pattern. So the slot that makes GtkApplicationWindow simple is the same slot that makes it a dead end.
AdwApplicationWindow: no titlebar slot at all
AdwApplicationWindow removes the slot. That sounds like a downgrade and is actually the whole point. There is no separate titlebar region — the window has a single content child that fills it edge to edge, and the header bar becomes an ordinary widget you place inside that content, like any other.
let window = adw::ApplicationWindow::builder()
.application(app)
.title("Application Shell")
.content(&content) // one child, no titlebar slot
.build();
Notice the method is content, not child plus titlebar. That single change is what unlocks adaptive layouts. Because the header bar is now just a widget in the tree, you can put it wherever the layout needs it: at the top of the window, yes, but equally at the top of one pane in a split view, or inside a page that slides away on a phone. The window stops dictating where chrome can live. Libadwaita’s rounded corners, its dialogs that draw as sheets attached to the window, and the adaptive split views the whole platform is built around all assume this shape — content all the way out, chrome composed inside it.
This is the part I’d most want a newcomer to internalise, because it inverts the mental model you arrive with. The window is not a frame with a title bar and a body. It’s an empty box you fill with one widget, and you assemble the chrome inside it. Once that clicks, the rest of Libadwaita stops looking like a pile of special cases and starts looking like a kit.
If the window is just an empty box, you need two more pieces to fill it: the header bar, and something to manage the relationship between it and your content.
AdwHeaderBar: the bar itself
The header bar goes inside the content, and you almost always want the Libadwaita one:
let header = adw::HeaderBar::new();
AdwHeaderBar and GtkHeaderBar look nearly identical and share most of their API, so it’s fair to ask why bother. AdwHeaderBar is built to live inside the content rather than in a titlebar slot, it knows how to integrate with the adaptive containers (a split view can tell each pane’s header bar to show or hide its window controls so you never get two close buttons), and it carries the platform’s responsive behaviour for free. GtkHeaderBar predates all of that and assumes it’s being set as a titlebar.
By default a header bar shows the window’s title in the centre and the window controls — close, and minimise/maximise where the desktop uses them — at the ends. That’s why the example sets .title(...) on the window: the header bar reads it. When you want a title and subtitle, or a custom widget in the centre, you set a title widget (AdwWindowTitle is the usual one) explicitly — a detail for a later post. For now, an empty AdwHeaderBar already gives you a draggable, closable, titled bar.
The rule of thumb: in a GNOME app, reach for AdwHeaderBar. The only time you’d drop to GtkHeaderBar is if you’re deliberately not building on Libadwaita — and if that’s true, you wouldn’t be using AdwApplicationWindow either.
AdwToolbarView: the chrome manager
You could pack the header bar and your content into a GtkBox and call it done. It would even look right — at first. But you’d be hand-rolling something Libadwaita does properly, and AdwToolbarView is the widget for it:
let toolbar_view = adw::ToolbarView::new();
toolbar_view.add_top_bar(&header);
toolbar_view.set_content(Some(&content));
AdwToolbarView manages the bars around your content — any number across the top, any number across the bottom — and the content between them. The reason to use it over a plain box is everything it does that a box doesn’t. It styles the bars to match the platform, and more importantly, it reacts to the content scrolling underneath: with its default flat style, a header bar sits borderless while the content is at the top and gains a subtle undershoot shadow the instant the content scrolls beneath it, so the user always knows there’s more above. Bottom bars get the same treatment in reverse. (Want a persistent line instead of a shadow — useful when the bar and the content have different backgrounds? That’s the top-bar-style property set to RaisedBorder; the default is Flat.) That scroll-aware shadow is the kind of thing you only notice when it’s missing — and it costs nothing if you’re already using AdwToolbarView.
A bottom bar — an action bar, a media control strip — is the same call:
toolbar_view.add_bottom_bar(&controls);
The mistake to avoid is putting the header bar into set_content instead of add_top_bar. It compiles, and it even renders something bar-shaped at the top of your content — but now it scrolls away with the content and gets none of the bar styling, because you’ve told the toolbar view it’s page material, not chrome. The bar methods are how the widget knows the difference.
Putting the shell together
Three widgets, nested in one clear order — window holds a toolbar view, toolbar view holds a header bar and a page:
fn build_ui(app: &adw::Application) {
let header = adw::HeaderBar::new();
let content = gtk::Label::builder()
.label("The shell is the chrome around this label.")
.build();
let toolbar_view = adw::ToolbarView::new();
toolbar_view.add_top_bar(&header);
toolbar_view.set_content(Some(&content));
let window = adw::ApplicationWindow::builder()
.application(app)
.title("Application Shell")
.default_width(420)
.default_height(320)
.content(&toolbar_view)
.build();
window.present();
}
Build your Application as an adw::Application, not a gtk::Application — it’s easy to miss, and the full example makes it explicit at the top. Constructing the Adwaita application type is what initialises Libadwaita — its stylesheet, its settings, the dark-mode plumbing. Use a plain gtk::Application with Adwaita widgets and you’ll get a window that’s subtly unstyled at best and a panic at worst. It’s a one-word change that’s easy to forget and annoying to diagnose, so make it a habit.
That’s the shell. From here out, we’re just filling it.
Sharp edges
A few things that bite, all of them on the seam between the GTK window you might expect and the Adwaita one you’re actually using.
AdwApplicationWindow doesn’t take set_child or set_titlebar
It inherits them from GtkWindow, so they compile — but it overrides their behaviour and the docs are explicit that you must use set_content instead. Call set_child on an AdwApplicationWindow and you’ll get a runtime warning and a window that doesn’t lay out the way you meant. If you’ve ported code from GtkApplicationWindow, this is the first thing to fix: one content child, no titlebar.
A window with no header bar has no controls
Because AdwApplicationWindow has no default titlebar, forgetting to add a header bar entirely gives you a window with no title, no close button, and nothing obvious to grab. On GtkApplicationWindow the default titlebar hides this mistake; here, nothing does. If your window comes up looking stranded, check that something in the content tree is actually an AdwHeaderBar.
The header bar goes in add_top_bar, not the content
Worth repeating because it’s the one that wastes an afternoon: a header bar handed to set_content (or buried in a GtkBox that you then set as content) loses its bar styling and scrolls away with the page. If your header looks flat and slides off the top when you scroll, that’s this.
The title is blank until you set the window title
An empty AdwHeaderBar shows the window’s title. If the centre of your bar is empty, you probably never called .title(...) on the window. Set it there, or give the header bar an explicit title widget — but don’t expect a title to appear from nowhere.
Next up
You’ve got a window with proper chrome and an empty content area staring back at you. Everything from here is filling that area — and the first job is arranging more than one thing in it without resorting to fixed coordinates. The next post takes on layout: GtkBox, GtkGrid, and why pixel-positioning a UI is a trap GTK won’t even let you fall into.
The runnable version of this shell is in the companion repo — cd 01-application-shell && cargo run. One widget at a time. See you in the next one.
