mhagrelius

designing-gnome-ui

0
0
# Install this skill:
npx skills add mhagrelius/dotfiles --skill "designing-gnome-ui"

Install specific skill from multi-skill repository

# Description

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 GNOME

# SKILL.md


name: designing-gnome-ui
description: 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 GNOME


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];
}
  1. Context: User goal, app type, constraints (screen size, input)
  2. Patterns: Select containers, navigation, controls, feedback
  3. Details: Typography, spacing, icons, writing style
  4. 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
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-symbolicuser-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

  • [ ] Correct container type and header bar structure
  • [ ] Navigation pattern matches content structure
  • [ ] Standard widgets used (not custom where native exists)
  • [ ] Symbolic icons from GNOME Icon Library
  • [ ] Typography uses style classes (title-1, heading, body, caption)
  • [ ] Libadwaita spacing defaults (no custom margins)
  • [ ] Header capitalization for labels, sentence for descriptions

Layer 2: Polish

  • [ ] Clear visual hierarchy - important elements prominent
  • [ ] Controls and text properly aligned
  • [ ] Consistent patterns throughout
  • [ ] Empty states have placeholder page (icon + message + action)
  • [ ] Loading states show spinner/skeleton, never frozen UI
  • [ ] Smooth resize and view transitions
  • [ ] Comfortable density - not cramped, not sparse

Layer 3: Rigor

  • [ ] All controls keyboard-accessible (Tab, Enter, Space)
  • [ ] All elements have accessible names for screen readers
  • [ ] Works with high contrast (GTK_THEME=Adwaita:hc)
  • [ ] Works with 200% text scaling
  • [ ] Error handling for every input/action
  • [ ] Edge cases handled (empty lists, long text, missing data)
  • [ ] Destructive actions have undo where possible
  • [ ] Responsive: works at 800x600, adapts to larger

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)

# Supported AI Coding Agents

This skill is compatible with the SKILL.md standard and works with all major AI coding agents:

Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.