Designing GNOME UI
Design GNOME UIs that are HIG-compliant, polished, and user-centered.
Core principle: No UI code without design decisions. Pattern selection and quality verification happen before implementation.
Quality layers: Compliance (follows HIG) → Polish (feels premium) → Rigor (handles edge cases)
Companion skill: For app architecture (lifecycle, threading, GSettings, actions, packaging), use developing-gtk-apps.
What's New (libadwaita 1.6-1.8)
| Need |
Widget/API |
Notes |
| Exclusive toggles (view mode) |
AdwToggleGroup |
Replaces multiple GtkToggleButton |
| Loading indicator |
AdwSpinner |
Works with animations disabled |
| Persistent bottom controls |
AdwBottomSheet |
Music player, persistent actions |
| Wrapping content (tags) |
AdwWrapBox |
Auto-wraps like text |
| Inline view switching |
AdwInlineViewSwitcher |
For cards, sidebars |
| Keyboard shortcuts |
AdwShortcutsDialog |
Replaces deprecated GtkShortcutsWindow |
| System accent color |
Automatic |
Apps follow desktop preference via portal |
| System fonts |
AdwStyleManager |
Access monospace/document fonts |
Deprecations: .dim-label → use .dimmed class
# AdwToggleGroup - view mode switching
toggle_group = Adw.ToggleGroup()
toggle_group.add(Adw.Toggle(icon_name="view-grid-symbolic", name="grid"))
toggle_group.add(Adw.Toggle(icon_name="view-list-symbolic", name="list"))
toggle_group.connect("notify::active-name", lambda g, p: set_view(g.get_active_name()))
header.pack_start(toggle_group)
# AdwBottomSheet - music player controls
bottom_sheet = Adw.BottomSheet()
bottom_sheet.set_content(main_content)
bottom_sheet.set_sheet(player_controls)
bottom_sheet.set_open(True) # Show sheet
window.set_content(bottom_sheet)
# AdwWrapBox - tag display
wrap_box = Adw.WrapBox(spacing=6)
for tag in ["Python", "GTK", "GNOME", "libadwaita"]:
chip = Gtk.Label(label=tag)
chip.add_css_class("chip") # Custom styling
wrap_box.append(chip)
# System fonts (1.7+) - for code editors, document views
style_manager = Adw.StyleManager.get_default()
mono_font = style_manager.get_monospace_font_name() # User's preferred mono font
doc_font = style_manager.get_document_font_name() # User's preferred document font
# Also available as CSS: --monospace-font-family, --document-font-family
The Process
digraph gnome_ui_process {
rankdir=LR;
node [shape=box];
"UI Task" -> "1. Context" -> "2. Patterns" -> "3. Details" -> "4. Checklist" -> "Implement";
"4. Checklist" -> "2. Patterns" [label="issues" style=dashed];
}
- Context: User goal, app type, constraints (screen size, input)
- Patterns: Select containers, navigation, controls, feedback
- Details: Typography, spacing, icons, writing style
- Checklist: Verify compliance, polish, rigor before code
Container Selection
digraph containers {
rankdir=TB;
node [shape=box];
"Building what?" [shape=diamond];
"AdwApplicationWindow + HeaderBar" [style=filled fillcolor=lightgreen];
"AdwPreferencesWindow" [style=filled fillcolor=lightgreen];
"AdwDialog" [style=filled fillcolor=lightgreen];
"Building what?" -> "AdwApplicationWindow + HeaderBar" [label="main window"];
"Building what?" -> "AdwPreferencesWindow" [label="settings"];
"Building what?" -> "AdwDialog" [label="modal action"];
}
| Scenario |
Default |
Notes |
| App window |
AdwApplicationWindow + AdwHeaderBar |
Remember user size, start ~800x600 |
| Settings |
AdwPreferencesWindow |
Handles groups, search, subpages |
| List of items |
AdwPreferencesGroup with rows |
Boxed list style |
| Primary action |
Single button, header bar end |
suggested-action class if emphasized |
| Destructive action |
destructive-action class |
Requires undo or confirmation |
Navigation Selection
| Structure |
Default Pattern |
| Single view |
None needed |
| 2-4 views |
AdwViewSwitcher in header bar |
| Many/dynamic views |
AdwNavigationSplitView (sidebar) |
| Hierarchical |
AdwNavigationView (drill-down) |
Control Defaults
| Need |
Default |
Avoid |
| On/Off |
AdwSwitchRow |
Checkbox for settings |
| Choose one (few) |
AdwComboRow |
Radio buttons outside dialogs |
| Choose one (many) |
AdwComboRow + search |
Long unsearchable dropdowns |
| Text input |
AdwEntryRow |
Bare GtkEntry |
| Multiline text |
GtkTextView + card class |
Bare unstyled text view |
| Number |
AdwSpinRow |
Text entry for numbers |
| Date |
GtkCalendar in popover |
Text entry for dates |
| Action in list |
AdwActionRow + suffix button |
Multiple buttons per row |
| Search |
GtkSearchBar + toggle button |
Always-visible search box |
Search Bar Pattern
# Search bar slides down from header, toggle with button or Ctrl+F
search_bar = Gtk.SearchBar()
search_entry = Gtk.SearchEntry()
search_bar.set_child(search_entry)
search_bar.connect_entry(search_entry)
search_bar.set_key_capture_widget(window) # Type-to-search
# Toggle button in header bar
search_btn = Gtk.ToggleButton(icon_name="system-search-symbolic")
search_btn.set_tooltip_text("Search")
search_bar.bind_property("search-mode-enabled", search_btn, "active",
GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE)
header.pack_end(search_btn)
toolbar_view.add_top_bar(search_bar)
Form Validation Pattern
# Use error CSS class on invalid fields
def validate_entry(row):
text = row.get_text()
if not text or len(text) < 3:
row.add_css_class("error")
row.set_tooltip_text("Name must be at least 3 characters")
return False
row.remove_css_class("error")
row.set_tooltip_text("")
return True
name_row.connect("changed", lambda r: validate_entry(r))
Validation timing: On change for format checks, on focus-out for expensive checks, on submit for final validation.
List Widget Selection
| Content |
Widget |
Why |
| Settings/preferences |
AdwPreferencesGroup |
Boxed list style, handles rows |
| Navigation list (sidebar) |
GtkListBox |
Selection support, activatable rows |
| Large/dynamic data |
GtkListView |
Virtual scrolling, performance |
| Grid of items |
GtkGridView |
Thumbnail grids, icon views |
Selection modes: Use Gtk.SingleSelection for navigation, Gtk.MultiSelection for bulk actions. Toggle selection mode with header bar button + action bar for bulk operations. See reference for code patterns.
Iconography
Rules:
- Symbolic icons only (outline, monochrome) - never full-color in UI
- Source from GNOME Icon Library (
icon-library app)
- Header bar: icon-only buttons, always add tooltips
- Naming:
action-object-symbolic (e.g., list-add-symbolic)
- Dynamic icons: Update icon name based on state (e.g.,
user-trash-symbolic → user-trash-full-symbolic)
| Action |
Icon |
| Add/New |
list-add-symbolic |
| Delete |
user-trash-symbolic |
| Settings |
emblem-system-symbolic |
| Menu |
open-menu-symbolic |
| Search |
system-search-symbolic |
| Edit |
document-edit-symbolic |
| Back |
go-previous-symbolic |
| Drill-down |
go-next-symbolic |
| Sync |
emblem-synchronizing-symbolic |
| Offline |
network-offline-symbolic |
| Warning |
dialog-warning-symbolic |
| Error |
dialog-error-symbolic |
| Select mode |
selection-mode-symbolic |
| Check/Done |
emblem-ok-symbolic |
| Close |
window-close-symbolic |
| Refresh |
view-refresh-symbolic |
Feedback Selection
digraph feedback {
rankdir=TB;
node [shape=box];
"What happened?" [shape=diamond];
"Transient or persistent?" [shape=diamond];
"AdwToast" [style=filled fillcolor=lightgreen label="AdwToast (default)"];
"AdwBanner" [style=filled fillcolor=lightyellow];
"AdwDialog" [style=filled fillcolor=lightpink];
"Progress/Spinner" [style=filled fillcolor=lightblue];
"What happened?" -> "Transient or persistent?" [label="state/error"];
"What happened?" -> "AdwDialog" [label="needs decision"];
"What happened?" -> "Progress/Spinner" [label="ongoing operation"];
"Transient or persistent?" -> "AdwToast" [label="transient event"];
"Transient or persistent?" -> "AdwBanner" [label="persistent state"];
}
| Scenario |
Default |
Details |
| Action done |
AdwToast |
Short message, optional undo |
| Destructive action |
AdwToast + undo |
Prefer over confirmation dialog |
| Error (recoverable) |
AdwToast |
Brief, auto-retry silently |
| Error (blocking) |
AdwDialog |
Explain problem and required fix |
| Persistent state |
AdwBanner |
Offline, degraded mode, auth required |
| Needs decision |
AdwDialog |
Conflicts, irreversible actions |
| Short wait (<5s) |
AdwSpinner |
No progress bar |
| Long operation (>30s) |
Progress bar + text |
"13 of 42 processed" |
Error escalation: Toast (transient) → Banner (persists) → Dialog (requires action)
- Network blip: Toast, auto-retry
- Prolonged offline: Banner with "Retry" button
- Auth expired: Dialog + Banner until resolved
Dialog rules:
- Cancel button first (left), action button last (right)
- Specific verbs ("Delete", "Save"), never "OK" or "Yes"
- Destructive actions use
destructive-action style
Context menus: Use GtkPopoverMenu for right-click actions (remove, rename, properties). Keep menus short; move complex actions to dialogs.
Empty State Pattern
# Show placeholder when list is empty
empty_state = Adw.StatusPage(
icon_name="folder-symbolic",
title="No Projects",
description="Create a project to get started"
)
create_btn = Gtk.Button(label="Create Project")
create_btn.add_css_class("pill")
create_btn.add_css_class("suggested-action")
empty_state.set_child(create_btn)
# Use stack to switch between list and empty state
stack.add_named(list_view, "content")
stack.add_named(empty_state, "empty")
stack.set_visible_child_name("empty" if model.get_n_items() == 0 else "content")
Quality Checklist
Create TodoWrite items for each applicable check before implementing.
Layer 1: Compliance
Layer 2: Polish
Layer 3: Rigor
Accessibility Quick Check
# Test high contrast
GTK_THEME=Adwaita:hc ./myapp
# Test large text (set in GNOME Settings > Accessibility first)
# Test with screen reader
orca &
./myapp
# Keyboard-only: unplug mouse, navigate entire app with Tab/Enter/Space
Code: Set accessible labels for icon-only buttons and images:
button.update_property([Gtk.AccessibleProperty.LABEL], ["Add new item"])
image.update_property([Gtk.AccessibleProperty.LABEL], ["Project thumbnail"])
Red Flags - STOP
- Custom styling where libadwaita has a pattern
- Multiple "suggested" or "destructive" buttons per view
- Confirmation dialogs for reversible actions (use undo)
- Text over images or textured backgrounds
- Non-GNOME icons without strong justification
- Missing tooltips on icon-only header bar buttons
- Generic labels ("OK", "Yes", "No", "Submit")
- Frozen UI during operations (missing loading states)
Non-GTK Apps (Qt/PySide6)
When styling Qt apps for GNOME:
- Use Adwaita-qt or manual QSS matching Adwaita colors
- Follow same patterns conceptually (header bar → toolbar, etc.)
- Match spacing, typography scale, and icon style
- Test alongside native GNOME apps for consistency
Reference Files
| Need |
File |
| Basic UI patterns |
gnome-hig-reference.md |
| Advanced patterns |
gnome-advanced-patterns.md |
gnome-hig-reference.md - Read for most apps:
- Container, navigation, control, feedback patterns with code
- Search bar, form validation, filter models, grid views, selection modes
- File chooser dialogs, dark/light mode, responsive breakpoints
- Primary menu structure, About dialog, Shortcuts window
- Typography, writing style, CSS color variables, common mistakes
- Accessibility testing commands (high contrast, screen reader)
- Phone/tablet breakpoints, adaptive layouts
gnome-advanced-patterns.md - Read when building:
- Drag & drop (reordering, file drops, cross-widget DnD)
- Undo/Redo (command pattern, history management)
- Tabs (AdwTabView, multi-document apps)
- System notifications (GNotification vs Toast)
- Media display (image viewers, video controls, pinch-to-zoom gestures)
- Split/Paned views (resizable panels)
- Welcome/Onboarding (first-run, feature callouts)
- Popovers (tool palettes, color pickers)
- Keyboard shortcuts (mnemonics, shortcut controllers)
1---2name: designing-gnome-ui3description: Use when designing, implementing, or modifying UI for GNOME apps; before writing UI code; when reviewing existing UI for HIG compliance; when working with GTK 4/libadwaita or styling Qt/PySide6 for GNOME4---5
6# Designing GNOME UI
7
8Design GNOME UIs that are HIG-compliant, polished, and user-centered.
9
10**Core principle:** No UI code without design decisions. Pattern selection and quality verification happen before implementation.
11
12**Quality layers:** Compliance (follows HIG) → Polish (feels premium) → Rigor (handles edge cases)
13
14**Companion skill:** For app architecture (lifecycle, threading, GSettings, actions, packaging), use `developing-gtk-apps`.
15
16## What's New (libadwaita 1.6-1.8)
17
18| Need | Widget/API | Notes |
19|------|------------|-------|
20| Exclusive toggles (view mode) | `AdwToggleGroup` | Replaces multiple `GtkToggleButton` |
21| Loading indicator | `AdwSpinner` | Works with animations disabled |
22| Persistent bottom controls | `AdwBottomSheet` | Music player, persistent actions |
23| Wrapping content (tags) | `AdwWrapBox` | Auto-wraps like text |
24| Inline view switching | `AdwInlineViewSwitcher` | For cards, sidebars |
25| Keyboard shortcuts | `AdwShortcutsDialog` | Replaces deprecated `GtkShortcutsWindow` |
26| System accent color | Automatic | Apps follow desktop preference via portal |
27| System fonts | `AdwStyleManager` | Access monospace/document fonts |
28
29**Deprecations:** `.dim-label` → use `.dimmed` class
30
31```python
32# AdwToggleGroup - view mode switching
33toggle_group = Adw.ToggleGroup()
34toggle_group.add(Adw.Toggle(icon_name="view-grid-symbolic", name="grid"))
35toggle_group.add(Adw.Toggle(icon_name="view-list-symbolic", name="list"))
36toggle_group.connect("notify::active-name", lambda g, p: set_view(g.get_active_name()))
37header.pack_start(toggle_group)
38
39# AdwBottomSheet - music player controls
40bottom_sheet = Adw.BottomSheet()
41bottom_sheet.set_content(main_content)
42bottom_sheet.set_sheet(player_controls)
43bottom_sheet.set_open(True) # Show sheet
44window.set_content(bottom_sheet)
45
46# AdwWrapBox - tag display
47wrap_box = Adw.WrapBox(spacing=6)
48for tag in ["Python", "GTK", "GNOME", "libadwaita"]:
49 chip = Gtk.Label(label=tag)
50 chip.add_css_class("chip") # Custom styling
51 wrap_box.append(chip)
52
53# System fonts (1.7+) - for code editors, document views
54style_manager = Adw.StyleManager.get_default()
55mono_font = style_manager.get_monospace_font_name() # User's preferred mono font
56doc_font = style_manager.get_document_font_name() # User's preferred document font
57# Also available as CSS: --monospace-font-family, --document-font-family
58```
59
60## The Process
61
62```dot
63digraph gnome_ui_process {
64 rankdir=LR;
65 node [shape=box];
66
67 "UI Task" -> "1. Context" -> "2. Patterns" -> "3. Details" -> "4. Checklist" -> "Implement";
68 "4. Checklist" -> "2. Patterns" [label="issues" style=dashed];
69}
70```
71
721. **Context:** User goal, app type, constraints (screen size, input)
732. **Patterns:** Select containers, navigation, controls, feedback
743. **Details:** Typography, spacing, icons, writing style
754. **Checklist:** Verify compliance, polish, rigor before code
76
77## Container Selection
78
79```dot
80digraph containers {
81 rankdir=TB;
82 node [shape=box];
83
84 "Building what?" [shape=diamond];
85 "AdwApplicationWindow + HeaderBar" [style=filled fillcolor=lightgreen];
86 "AdwPreferencesWindow" [style=filled fillcolor=lightgreen];
87 "AdwDialog" [style=filled fillcolor=lightgreen];
88
89 "Building what?" -> "AdwApplicationWindow + HeaderBar" [label="main window"];
90 "Building what?" -> "AdwPreferencesWindow" [label="settings"];
91 "Building what?" -> "AdwDialog" [label="modal action"];
92}
93```
94
95| Scenario | Default | Notes |
96|----------|---------|-------|
97| App window | `AdwApplicationWindow` + `AdwHeaderBar` | Remember user size, start ~800x600 |
98| Settings | `AdwPreferencesWindow` | Handles groups, search, subpages |
99| List of items | `AdwPreferencesGroup` with rows | Boxed list style |
100| Primary action | Single button, header bar end | `suggested-action` class if emphasized |
101| Destructive action | `destructive-action` class | Requires undo or confirmation |
102
103## Navigation Selection
104
105| Structure | Default Pattern |
106|-----------|-----------------|
107| Single view | None needed |
108| 2-4 views | `AdwViewSwitcher` in header bar |
109| Many/dynamic views | `AdwNavigationSplitView` (sidebar) |
110| Hierarchical | `AdwNavigationView` (drill-down) |
111
112## Control Defaults
113
114| Need | Default | Avoid |
115|------|---------|-------|
116| On/Off | `AdwSwitchRow` | Checkbox for settings |
117| Choose one (few) | `AdwComboRow` | Radio buttons outside dialogs |
118| Choose one (many) | `AdwComboRow` + search | Long unsearchable dropdowns |
119| Text input | `AdwEntryRow` | Bare `GtkEntry` |
120| Multiline text | `GtkTextView` + `card` class | Bare unstyled text view |
121| Number | `AdwSpinRow` | Text entry for numbers |
122| Date | `GtkCalendar` in popover | Text entry for dates |
123| Action in list | `AdwActionRow` + suffix button | Multiple buttons per row |
124| Search | `GtkSearchBar` + toggle button | Always-visible search box |
125
126### Search Bar Pattern
127
128```python
129# Search bar slides down from header, toggle with button or Ctrl+F
130search_bar = Gtk.SearchBar()
131search_entry = Gtk.SearchEntry()
132search_bar.set_child(search_entry)
133search_bar.connect_entry(search_entry)
134search_bar.set_key_capture_widget(window) # Type-to-search
135
136# Toggle button in header bar
137search_btn = Gtk.ToggleButton(icon_name="system-search-symbolic")
138search_btn.set_tooltip_text("Search")
139search_bar.bind_property("search-mode-enabled", search_btn, "active",
140 GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE)
141header.pack_end(search_btn)
142toolbar_view.add_top_bar(search_bar)
143```
144
145### Form Validation Pattern
146
147```python
148# Use error CSS class on invalid fields
149def validate_entry(row):
150 text = row.get_text()
151 if not text or len(text) < 3:
152 row.add_css_class("error")
153 row.set_tooltip_text("Name must be at least 3 characters")
154 return False
155 row.remove_css_class("error")
156 row.set_tooltip_text("")
157 return True
158
159name_row.connect("changed", lambda r: validate_entry(r))
160```
161
162**Validation timing:** On change for format checks, on focus-out for expensive checks, on submit for final validation.
163
164## List Widget Selection
165
166| Content | Widget | Why |
167|---------|--------|-----|
168| Settings/preferences | `AdwPreferencesGroup` | Boxed list style, handles rows |
169| Navigation list (sidebar) | `GtkListBox` | Selection support, activatable rows |
170| Large/dynamic data | `GtkListView` | Virtual scrolling, performance |
171| Grid of items | `GtkGridView` | Thumbnail grids, icon views |
172
173**Selection modes:** Use `Gtk.SingleSelection` for navigation, `Gtk.MultiSelection` for bulk actions. Toggle selection mode with header bar button + action bar for bulk operations. See reference for code patterns.
174
175## Iconography
176
177**Rules:**
178- Symbolic icons only (outline, monochrome) - never full-color in UI
179- Source from GNOME Icon Library (`icon-library` app)
180- Header bar: icon-only buttons, always add tooltips
181- Naming: `action-object-symbolic` (e.g., `list-add-symbolic`)
182- **Dynamic icons:** Update icon name based on state (e.g., `user-trash-symbolic` → `user-trash-full-symbolic`)
183
184| Action | Icon |
185|--------|------|
186| Add/New | `list-add-symbolic` |
187| Delete | `user-trash-symbolic` |
188| Settings | `emblem-system-symbolic` |
189| Menu | `open-menu-symbolic` |
190| Search | `system-search-symbolic` |
191| Edit | `document-edit-symbolic` |
192| Back | `go-previous-symbolic` |
193| Drill-down | `go-next-symbolic` |
194| Sync | `emblem-synchronizing-symbolic` |
195| Offline | `network-offline-symbolic` |
196| Warning | `dialog-warning-symbolic` |
197| Error | `dialog-error-symbolic` |
198| Select mode | `selection-mode-symbolic` |
199| Check/Done | `emblem-ok-symbolic` |
200| Close | `window-close-symbolic` |
201| Refresh | `view-refresh-symbolic` |
202
203## Feedback Selection
204
205```dot
206digraph feedback {
207 rankdir=TB;
208 node [shape=box];
209
210 "What happened?" [shape=diamond];
211 "Transient or persistent?" [shape=diamond];
212 "AdwToast" [style=filled fillcolor=lightgreen label="AdwToast (default)"];
213 "AdwBanner" [style=filled fillcolor=lightyellow];
214 "AdwDialog" [style=filled fillcolor=lightpink];
215 "Progress/Spinner" [style=filled fillcolor=lightblue];
216
217 "What happened?" -> "Transient or persistent?" [label="state/error"];
218 "What happened?" -> "AdwDialog" [label="needs decision"];
219 "What happened?" -> "Progress/Spinner" [label="ongoing operation"];
220 "Transient or persistent?" -> "AdwToast" [label="transient event"];
221 "Transient or persistent?" -> "AdwBanner" [label="persistent state"];
222}
223```
224
225| Scenario | Default | Details |
226|----------|---------|---------|
227| Action done | `AdwToast` | Short message, optional undo |
228| Destructive action | `AdwToast` + undo | Prefer over confirmation dialog |
229| Error (recoverable) | `AdwToast` | Brief, auto-retry silently |
230| Error (blocking) | `AdwDialog` | Explain problem and required fix |
231| Persistent state | `AdwBanner` | Offline, degraded mode, auth required |
232| Needs decision | `AdwDialog` | Conflicts, irreversible actions |
233| Short wait (<5s) | `AdwSpinner` | No progress bar |
234| Long operation (>30s) | Progress bar + text | "13 of 42 processed" |
235
236**Error escalation:** Toast (transient) → Banner (persists) → Dialog (requires action)
237- Network blip: Toast, auto-retry
238- Prolonged offline: Banner with "Retry" button
239- Auth expired: Dialog + Banner until resolved
240
241**Dialog rules:**
242- Cancel button first (left), action button last (right)
243- Specific verbs ("Delete", "Save"), never "OK" or "Yes"
244- Destructive actions use `destructive-action` style
245
246**Context menus:** Use `GtkPopoverMenu` for right-click actions (remove, rename, properties). Keep menus short; move complex actions to dialogs.
247
248### Empty State Pattern
249
250```python
251# Show placeholder when list is empty
252empty_state = Adw.StatusPage(
253 icon_name="folder-symbolic",
254 title="No Projects",
255 description="Create a project to get started"
256)
257create_btn = Gtk.Button(label="Create Project")
258create_btn.add_css_class("pill")
259create_btn.add_css_class("suggested-action")
260empty_state.set_child(create_btn)
261
262# Use stack to switch between list and empty state
263stack.add_named(list_view, "content")
264stack.add_named(empty_state, "empty")
265stack.set_visible_child_name("empty" if model.get_n_items() == 0 else "content")
266```
267
268## Quality Checklist
269
270**Create TodoWrite items for each applicable check before implementing.**
271
272### Layer 1: Compliance
273
274- [ ] Correct container type and header bar structure
275- [ ] Navigation pattern matches content structure
276- [ ] Standard widgets used (not custom where native exists)
277- [ ] Symbolic icons from GNOME Icon Library
278- [ ] Typography uses style classes (`title-1`, `heading`, `body`, `caption`)
279- [ ] Libadwaita spacing defaults (no custom margins)
280- [ ] Header capitalization for labels, sentence for descriptions
281
282### Layer 2: Polish
283
284- [ ] Clear visual hierarchy - important elements prominent
285- [ ] Controls and text properly aligned
286- [ ] Consistent patterns throughout
287- [ ] Empty states have placeholder page (icon + message + action)
288- [ ] Loading states show spinner/skeleton, never frozen UI
289- [ ] Smooth resize and view transitions
290- [ ] Comfortable density - not cramped, not sparse
291
292### Layer 3: Rigor
293
294- [ ] All controls keyboard-accessible (Tab, Enter, Space)
295- [ ] All elements have accessible names for screen readers
296- [ ] Works with high contrast (`GTK_THEME=Adwaita:hc`)
297- [ ] Works with 200% text scaling
298- [ ] Error handling for every input/action
299- [ ] Edge cases handled (empty lists, long text, missing data)
300- [ ] Destructive actions have undo where possible
301- [ ] Responsive: works at 800x600, adapts to larger
302
303## Accessibility Quick Check
304
305```bash
306# Test high contrast
307GTK_THEME=Adwaita:hc ./myapp
308
309# Test large text (set in GNOME Settings > Accessibility first)
310
311# Test with screen reader
312orca &
313./myapp
314
315# Keyboard-only: unplug mouse, navigate entire app with Tab/Enter/Space
316```
317
318**Code:** Set accessible labels for icon-only buttons and images:
319```python
320button.update_property([Gtk.AccessibleProperty.LABEL], ["Add new item"])
321image.update_property([Gtk.AccessibleProperty.LABEL], ["Project thumbnail"])
322```
323
324## Red Flags - STOP
325
326- Custom styling where libadwaita has a pattern
327- Multiple "suggested" or "destructive" buttons per view
328- Confirmation dialogs for reversible actions (use undo)
329- Text over images or textured backgrounds
330- Non-GNOME icons without strong justification
331- Missing tooltips on icon-only header bar buttons
332- Generic labels ("OK", "Yes", "No", "Submit")
333- Frozen UI during operations (missing loading states)
334
335## Non-GTK Apps (Qt/PySide6)
336
337When styling Qt apps for GNOME:
338- Use Adwaita-qt or manual QSS matching Adwaita colors
339- Follow same patterns conceptually (header bar → toolbar, etc.)
340- Match spacing, typography scale, and icon style
341- Test alongside native GNOME apps for consistency
342
343## Reference Files
344
345| Need | File |
346|------|------|
347| Basic UI patterns | `gnome-hig-reference.md` |
348| Advanced patterns | `gnome-advanced-patterns.md` |
349
350**gnome-hig-reference.md** - Read for most apps:
351- Container, navigation, control, feedback patterns with code
352- Search bar, form validation, **filter models**, grid views, selection modes
353- File chooser dialogs, dark/light mode, responsive breakpoints
354- Primary menu structure, About dialog, Shortcuts window
355- Typography, writing style, **CSS color variables**, common mistakes
356- **Accessibility testing** commands (high contrast, screen reader)
357- **Phone/tablet breakpoints**, adaptive layouts
358
359**gnome-advanced-patterns.md** - Read when building:
360- Drag & drop (reordering, file drops, cross-widget DnD)
361- Undo/Redo (command pattern, history management)
362- Tabs (AdwTabView, multi-document apps)
363- System notifications (GNotification vs Toast)
364- Media display (image viewers, video controls, **pinch-to-zoom gestures**)
365- Split/Paned views (resizable panels)
366- Welcome/Onboarding (first-run, feature callouts)
367- Popovers (tool palettes, color pickers)
368- **Keyboard shortcuts** (mnemonics, shortcut controllers)