Layout & Navigation
Adaptive layouts, navigation patterns, dialogs, empty states, and settings pages for Linux desktop apps — with GNOME/libadwaita and KDE/Kirigami patterns.
Layout is where “native” becomes visible. A Linux app can use the right widgets and still feel wrong if the navigation model fights the desktop, the window does not adapt, or the settings page looks like a web form.
Start with the content model, not the widgets. What is the user looking at? Is it a list, a document, a canvas, a timeline, or a set of independent tools? The answer determines the navigation pattern.
Pick the pattern
Section titled “Pick the pattern”| Pattern | Use when | GNOME/libadwaita | KDE/Kirigami |
|---|---|---|---|
| Single view | One task, one document | Adw.ApplicationWindow + content |
Kirigami.Page |
| Master–detail | A list and a selected item | Adw.NavigationSplitView |
Kirigami.PageRow |
| Collapsible sidebar | Many sections, content-first | Adw.OverlaySplitView + Adw.Sidebar |
Kirigami.GlobalDrawer |
| View switcher | 2–5 peer views | Adw.ViewStack + Adw.ViewSwitcher |
Kirigami.PageRow / tabs |
| Tabs | Multiple open documents | Adw.TabView + Adw.TabBar |
QTabWidget / Kirigami.PageRow |
| Focused task | Modal editing or confirmation | Adw.Dialog / Adw.AlertDialog |
Kirigami.Dialog / Kirigami.PromptDialog |
| Settings | Preferences | Adw.PreferencesPage / Adw.PreferencesDialog |
KConfigDialog / Kirigami.ScrollablePage |
Master–detail
Section titled “Master–detail”The canonical desktop layout: a list on the left, content on the right. On narrow windows, the list becomes the first page and the content slides in.
Wide: [ sidebar ][ detail ]Narrow: [ list ] → [ detail ] → backUse Adw.NavigationSplitView when the user navigates into an item and expects a back button. Use Adw.OverlaySplitView when the sidebar is a persistent navigation surface that can collapse into an overlay.
Sidebars and view switchers
Section titled “Sidebars and view switchers”- A sidebar is for navigation between sections that persist. Use
Adw.Sidebar(libadwaita 1.9) or aGtk.ListBoxwith thenavigation-sidebarstyle class. - A view switcher is for 2–5 peer views inside the same context. Use
Adw.ViewSwitcherin the headerbar, not a row of custom tabs. - A tab view is for multiple open documents. Use
Adw.TabView; do not reimplement tabs with buttons.
If the sidebar contains more than ~10 items without grouping, the information architecture is probably wrong. Group with sections and labels.
Make it adaptive
Section titled “Make it adaptive”Test at three widths:
| Width | Typical layout |
|---|---|
| < 600 px | Single pane; navigation pushes a new page |
| 600–900 px | Collapsed sidebar or two panes with narrow content |
| > 900 px | Sidebar + content, or three panes |
Use Adw.Breakpoint to change layout properties at a width threshold. In Blueprint:
Adw.ApplicationWindow { content: Adw.ToolbarView { [top] Adw.HeaderBar {}
content: Adw.OverlaySplitView split { sidebar: Gtk.ScrolledWindow { child: Gtk.ListBox { ... }; };
content: Gtk.Stack { ... }; }; };
Adw.Breakpoint { condition ("max-width: 600sp") setters { split.collapsed: true; } }}In Python:
breakpoint = Adw.Breakpoint.new( Adw.BreakpointCondition.parse("max-width: 600sp"))breakpoint.add_setter(split, "collapsed", True)self.add_breakpoint(breakpoint)Do not use breakpoints for everything. Prefer widgets that provide the adaptive primitives (Adw.OverlaySplitView, Adw.NavigationSplitView, Adw.ViewStack), and pair them with a breakpoint when they need to collapse or change structure.
Window size and state
Section titled “Window size and state”Wayland does not let apps position windows. Save the size, maximized state, and fullscreen state; let the compositor decide where the window goes.
self.settings = Gio.Settings.new("io.github.yourname.Notes")
# Restore the initial size.self.set_default_size(self.settings.get_int("window-width"), self.settings.get_int("window-height"))
# Save the actual size when the window closes.def on_close_request(self, _window): self.settings.set_int("window-width", self.get_width()) self.settings.set_int("window-height", self.get_height()) return Falsedefault-width/default-height bindings only set the initial size; they do not track a user resize.
Provide a sensible default size for the first launch, and a minimum size that still works at 200% scaling. Do not force a size that only fits your monitor.
Empty, loading, and error states
Section titled “Empty, loading, and error states”Every view that can be empty needs three states:
| State | What to show |
|---|---|
| Empty | What this view is for, why it is empty, and one primary action |
| Loading | A skeleton or spinner that does not shift the layout; cancel if long |
| Error | What failed, why, and what the user can do; a retry action |
Use Adw.StatusPage for empty and error states. It gives you the right spacing, icon size, and typography.
page = Adw.StatusPage()page.set_icon_name("document-edit-symbolic")page.set_title("No notes yet")page.set_description("Create your first note to get started.")button = Gtk.Button(label="New note")button.add_css_class("suggested-action")page.set_child(button)Do not use a blank white window with a tiny “No data” label. The empty state is the first impression of the feature.
Settings
Section titled “Settings”Settings are part of the UI, not a config file with a window around it.
- Group by task: “Appearance”, “Editing”, “Sync”, “Privacy”.
- Use
Adw.PreferencesPagewithAdw.PreferencesGroupand rows (Adw.SwitchRow,Adw.EntryRow,Adw.SpinRow,Adw.ComboRow). - Apply changes immediately; no “Save” button.
- Provide search if there are more than ~15 settings.
- Use a separate dialog for window-specific settings; use the app’s preferences for global settings.
- Provide “Reset to defaults” and a way to export/import settings.
On KDE, use KConfigDialog or a Kirigami settings page with the same grouping principles.
Dialogs
Section titled “Dialogs”Use a dialog when the user must finish a focused task before returning, or when a decision is destructive and irreversible.
Adw.AlertDialogfor confirmations and simple choices.Adw.Dialogfor a small form or a focused task.Adw.PreferencesDialogfor settings that belong to a window.- Avoid dialogs that merely display information; use inline UI.
- Do not stack dialogs. If a dialog opens another dialog, reconsider the flow.
- Destructive actions use a clear verb (“Delete note”), not “OK”.
- The escape route is always available (
Esc), and the primary action is reachable withEnter.
Onboarding
Section titled “Onboarding”Keep it to one screen or none. The app should be usable immediately; onboarding explains the why, not the how.
- Show the core value in one sentence.
- Offer one primary action (“Create your first note”).
- Do not require an account, a tour, or a permission before the user sees value.
- Explain permissions in context, when the feature is used.
- Make onboarding dismissible and re-openable from Help/About.
Menus and discoverability
Section titled “Menus and discoverability”- Put primary actions in the headerbar as buttons with labels or well-known icons.
- Put secondary actions in the primary menu (the hamburger or gear), grouped with separators.
- Show shortcuts in menu items.
- Use
Gtk.PopoverMenu/Adw.HeaderBarconventions; do not build a custom dropdown. - Do not hide the only way to do something in a context menu. Context menus are accelerators, not the primary interface.
Kirigami notes
Section titled “Kirigami notes”If you are building for KDE:
- Use
Kirigami.ApplicationWindowandKirigami.Page. Kirigami.PageRowgives you adaptive master–detail with a back button on narrow screens.Kirigami.OverlaySheetis the mobile-friendly alternative to a dialog; use it for quick actions, not for long forms.- Use
Kirigami.Units.gridUnitfor spacing instead of hardcoded pixels. - Use
Kirigami.Themecolors; respect the system color scheme. - Use Breeze icons and
Kirigami.Iconfor consistency.
Layout checklist
Section titled “Layout checklist”- ☐ The navigation model matches the content: list/detail, sections, documents, or a single task.
- ☐ The app works at 360 px, 720 px, and 1280 px widths.
- ☐ The sidebar collapses or becomes a navigation page on narrow windows.
- ☐ Window size and maximized state persist; position does not.
- ☐ Empty, loading, and error states exist for every dynamic view.
- ☐ Settings use
Adw.PreferencesPage/KConfigDialog, grouped by task. - ☐ Dialogs are focused, dismissible, and never stacked.
- ☐ Onboarding is one screen and skippable.
- ☐ Primary actions are visible; secondary actions are in a menu with shortcuts.
- ☐ The layout works at 200% text and display scaling.
Sources
Section titled “Sources”Next: Interaction & Feedback — keyboard, focus, undo, search, and the small behaviors that make an app feel alive.