Claude-skill-registry designing-gnome-ui

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

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/designing-gnome-ui" ~/.claude/skills/majiayu000-claude-skill-registry-designing-gnome-ui && rm -rf "$T"
manifest: skills/data/designing-gnome-ui/SKILL.md
source content

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)

NeedWidget/APINotes
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 colorAutomaticApps 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"];
}
ScenarioDefaultNotes
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 actionSingle button, header bar end
suggested-action
class if emphasized
Destructive action
destructive-action
class
Requires undo or confirmation

Navigation Selection

StructureDefault Pattern
Single viewNone needed
2-4 views
AdwViewSwitcher
in header bar
Many/dynamic views
AdwNavigationSplitView
(sidebar)
Hierarchical
AdwNavigationView
(drill-down)

Control Defaults

NeedDefaultAvoid
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

ContentWidgetWhy
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
    )
ActionIcon
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"];
}
ScenarioDefaultDetails
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

NeedFile
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)