`project.yaml` at the repo root is now the single source of truth for a project's configuration, replacing `production/stage.txt`, `production/review-mode.txt` and `.claude/docs/technical-preferences.md` (all three still read as a fallback; nothing is force-deleted). **`modes.rigor` — one question instead of six.** `minimal`, `standard` or `full` fronts six knobs: `modes.workflow`, `docs.density`, `qa.level`, `modes.story_granularity`, `modes.review_mode` and `team.size`. It defaults to `minimal`, which is a change from 1.0's behaviour: the heavier tier was measured costing several times more to reach working code without producing a better result, and returning nothing when a fresh developer inherited the project. Pin `modes.rigor: standard` to keep 1.0's process level. Also in this release: - `/settings` to view or change any setting, and `project.local.yaml` for per-developer preferences that should not be committed. - `system_overrides` to hold one system to a higher standard than the rest of the project, and `testing.strict` to set per test type whether missing evidence blocks a story or warns. - A story that changes anything the player sees is not closed until the game has been launched, observed, and a screenshot retained in `production/qa/evidence/`. A parse check is not a run. This holds at every rigor level, including `minimal`, where automated tests are waived but the look is not. - The code root is engine-specific and enforced as such: Godot `src/`, Unity `Assets/`, Unreal `Source/<Module>/`. Two of the three will not build a project laid out any other way. - Migration tooling for existing projects: `.claude/scripts/migrate-v1-config.sh` converts a 1.0 layout, non-destructively by default, with a report to review before `--finalize` deletes anything. - Ask-before-write holds regardless of personal automation settings. Upgrading from 1.0 or earlier: see UPGRADING.md. Full list of changes: CHANGELOG.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016xvzG3i7pWQg4LjyDwWMXd
40 KiB
Authoring guidance for interaction-pattern-library.md. Load only the part covering the section currently being authored — not the whole file at once.
Covers: Standard Control Patterns — full reference specifications for Button (Primary/Secondary/Destructive), Toggle, Slider, Dropdown / Select, List Item, Grid Item, Modal Dialog, Confirmation Dialog, Toast / Notification, Tooltip, Progress Bar, Input Field, Tab Bar, Scroll Container.
Standard Control Patterns
Button (Primary)
Category: Input Status: Draft When to Use: The single most important action on a screen. "Start Game," "Confirm," "Accept," "Buy." There should be at most one Primary button visible at a time. It is the answer to "what does the player most likely want to do here?" When NOT to Use: Alternative or secondary actions; destructive actions that require confirmation before the consequence is irreversible; any action that is not the primary intent of the screen.
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Default | Full-opacity fill, primary color from art-bible. Label centered. | — | — | — | — |
| Hovered (mouse) | Brightness +15%, subtle scale 1.03x, cursor changes to pointer | Mouse over element | Transition from Default | 80ms ease-out | [UI hover sound — see Sound Standards] |
| Focused (keyboard/gamepad) | Focus ring visible (2px, offset 3px, high contrast color). Same brightness as Hovered. | Tab / D-pad navigation | Transition from Default | 80ms ease-out | [UI focus sound — same as hover] |
| Pressed | Scale 0.97x, brightness -10% | Click / Enter / A (Xbox) / Cross (PS) | Action fires on press-up, not press-down. Scale on press-down. | 60ms ease-in for press; 80ms ease-out on release | [UI confirm sound] |
| Disabled | 40% opacity, no pointer cursor, no hover state | — | No response | — | — |
| Loading (post-press) | Replace label with spinner. Button remains at pressed scale, disabled state. | — | Prevents double-submission | Duration of async operation | — |
Accessibility:
- Keyboard: Tab to focus, Enter or Space to activate. Must be reachable from any other interactive element on screen via Tab sequence.
- Gamepad: D-pad or left stick to navigate focus to button. A (Xbox) / Cross (PS) to activate. Focus must be placed on Primary button by default when screen opens.
- Screen reader: Button must expose accessible name matching visible label. Role: "button." State: "dimmed" when disabled. Activation announcement: "[Label] button — [result of action, if known]."
- Colorblind: Do not rely on color alone to distinguish Primary from Secondary. Primary uses higher visual weight (fill vs. outline, or larger size) in addition to color differentiation.
- Minimum touch target: 44x44pt (iOS HIG) / 48x48dp (Android). Apply even on PC if touch support is possible.
Implementation Notes:
[Godot: Extend Button control. Override _draw() for custom states rather than
modifying themes mid-state. Use focus_mode = FOCUS_ALL to ensure keyboard
focusability. Set mouse_default_cursor_shape = CURSOR_POINTING_HAND. For the
scale animation, use a Tween on the scale property of the button's parent
Control — scaling the Button itself can clip children.]
Button (Secondary)
Category: Input Status: Draft When to Use: Alternative or cancel action. "Back," "Cancel," "Skip," "Maybe Later." Lower visual weight than Primary — it should recede visually, not compete. When NOT to Use: Destructive actions (use Button (Destructive)). The most important action on the screen (use Button (Primary)).
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Default | Outlined style (border only, transparent fill), secondary color. Slightly smaller or lower weight than Primary. | — | — | — | — |
| Hovered | Background fill appears at 15% opacity. Border brightens. Scale 1.02x. | Mouse over | Transition from Default | 80ms ease-out | [UI hover sound — softer variant than Primary] |
| Focused | Focus ring, same specification as Primary. | Tab / D-pad | Transition from Default | 80ms ease-out | [UI focus sound] |
| Pressed | Scale 0.97x, fill opacity increases to 30% | Click / Enter / B (Xbox) / Circle (PS) on focused state | Action fires on press-up | 60ms ease-in | [UI cancel/back sound] |
| Disabled | 40% opacity | — | No response | — | — |
Accessibility: Same requirements as Button (Primary). Accessible name must match visible label. In a dialog with Primary and Secondary buttons, the Secondary button typically maps to the platform "cancel" input (B / Circle / Escape) as well as direct focus activation.
Implementation Notes: [Same as Button (Primary). Where a Primary and Secondary appear together, ensure Secondary is always positioned consistently — right/bottom of Primary on horizontal layouts, or below Primary on vertical layouts. Consistency across screens is more important than per-screen aesthetic preference.]
Button (Destructive)
Category: Input Status: Draft When to Use: Any action that is irreversible and causes loss of player data or significant progress: "Delete Save File," "Reset All Settings," "Leave Match," "Discard Changes." The visual treatment signals danger before the player presses. When NOT to Use: Actions that can be undone, or actions that are merely consequential but reversible.
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Default | Outlined or filled with destructive color (typically a desaturated red — confirm colorblind compatibility in accessibility-requirements). Label may include a warning icon. | — | — | — | — |
| Hovered / Focused | Same behavior as Button (Primary) hover/focus but with destructive color | — | — | 80ms | [UI hover sound] |
| Pressed (first press) | Does NOT execute the action. Instead, opens Confirmation Dialog pattern (see below). The button itself shows a brief pulse animation. | Click / Enter | Trigger Confirmation Dialog | 100ms pulse | [UI warning sound — distinct from standard confirm] |
| — | Confirmation Dialog handles the actual execution | — | — | — | — |
| Disabled | 40% opacity | — | No response | — | — |
Critical rule: A Button (Destructive) NEVER executes its action directly. It always triggers a Confirmation Dialog. There are no exceptions. A player who presses it by accident must always have one more opportunity to back out. Games that skip confirmation on destructive actions generate the most visible negative community sentiment of any UX failure type. See: every "accidentally deleted save file" complaint on any game forum.
Accessibility: Screen reader must announce the destructive nature: "[Label] button — this action cannot be undone." In addition to accessible name, use the description property if available to add the warning text.
Implementation Notes: [Destructive button triggers a separate Confirmation Dialog scene. Pass the action callback to the dialog — the button itself does not hold the execution logic. This separation prevents accidental execution if the confirmation dialog has a bug.]
Toggle
Category: Input Status: Draft When to Use: Binary on/off settings where both states are equally valid and the current state must be visible at a glance. "Subtitles: On/Off," "Aim Assist: On/Off," "Notifications: On/Off." When NOT to Use: Selections from more than two options (use Dropdown). Actions that happen once rather than representing a persistent state (use Button). Cases where the consequence of toggling is complex enough to need explanation (show a description field alongside).
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Off / Default | Track: muted fill. Thumb: leftmost position. Label: "Off" or state label. | — | — | — | — |
| Hovered | Track brightens 10%. Cursor: pointer. | Mouse over | Transition | 60ms | [UI hover sound] |
| Focused | Focus ring around entire toggle element (track + thumb). | Tab / D-pad | — | 60ms | [UI focus sound] |
| Pressed / Activated | Thumb slides to right side. Track fill changes to active color. Label changes to "On" or active state label. State persists. | Click / Enter / A / Cross | Toggle state change. Fire onChange event. Persist value. | 150ms ease-in-out for slide | [Toggle ON sound] |
| Pressed / Deactivated | Thumb slides to left. Track reverts to muted fill. | Same inputs | Toggle state change | 150ms ease-in-out | [Toggle OFF sound — subtly different from ON] |
| Disabled | 40% opacity. No interaction. Current state still visible. | — | No response | — | — |
Accessibility:
- Keyboard/Gamepad: Space or Enter to toggle. Avoid requiring directional inputs (left/right) to toggle — some users cannot predict that behavior.
- Screen reader: Role: "switch." State: "on" or "off" — the accessible name should NOT include the state (the screen reader announces state separately). Correct: accessible name "Subtitles," state "on." Incorrect: accessible name "Subtitles On."
- The toggle label (not just the visual thumb position) must change to show current state for players who cannot reliably distinguish left from right positions.
Implementation Notes: [Godot: Use a custom Control or a CheckButton. The built-in CheckButton provides accessibility role but uses a checkbox-style visual; a custom slide-toggle animation may be needed for the target art style. Ensure the slide animation is skipped when motion reduction mode is active — in that case, snap to final state instantly.]
Slider
Category: Input Status: Draft When to Use: Selecting a value from a continuous range where approximate values are acceptable and the range and relative position matter. Volume (0–100%), brightness, text size. The visual representation of position is itself useful information. When NOT to Use: Precise value entry (use Input Field). Selection from a short discrete list (use Dropdown). Binary state (use Toggle).
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Default | Track (full width). Fill (left of thumb, shows current value). Thumb (draggable handle). Current value label (right of track or above thumb). | — | — | — | — |
| Hovered | Thumb enlarges slightly (1.2x). Track brightens. | Mouse over | — | 60ms | — |
| Focused | Focus ring on thumb. Track brightens. | Tab / D-pad | — | 60ms | [UI focus sound] |
| Dragging (mouse) | Thumb follows cursor. Fill updates in real time. Value label updates in real time. | Click + drag on thumb | Continuous value update. Fire onChange continuously. | Real time | [Slider adjust sound — subtle, loops while dragging] |
| Keyboard / D-pad adjust | Thumb moves one step (5% of range per press, or 1 discrete unit). | Left/Right arrows or Left/Right D-pad while focused | Step value change. Fire onChange per step. | Instant | [Slider step sound — one click per step] |
| Keyboard fast adjust | Larger step (25% of range). | Page Up / Page Down while focused | Large step value change | Instant | [Same step sound] |
| Released | Value locks. onChange fires final value. | Mouse release | — | — | — |
| Disabled | 40% opacity. No interaction. Value visible. | — | No response | — | — |
Accessibility:
- Keyboard: Left/Right arrows to adjust by small step. Page Up/Page Down for large step. Home/End to jump to min/max.
- Screen reader: Role: "slider." Accessible name: the label (e.g., "Music Volume"). Current value announced on every change: "Music Volume, 80 percent." Min/max values announced on first focus.
- All sliders must show a numeric value alongside the visual position. Relying only on track fill position excludes players who cannot perceive relative position.
Implementation Notes: [Godot HSlider: set step to appropriate increment.
Override keyboard input to add Page Up/Down support via _input(). Bind the
value_changed signal to update the displayed numeric label. When motion reduction
mode is enabled, ensure value label updates are the sole feedback — do not suppress
them. Rumble feedback on gamepad slider adjustment is a nice enhancement for
accessibility.]
Dropdown / Select
Category: Input Status: Draft When to Use: Selection from a discrete list of 3-15 options where only the selected value needs to be visible at rest. Display resolution, language, window mode, input preset. The closed state shows only the current selection. When NOT to Use: Binary choices (use Toggle). More than ~15 options (use a full List pattern or a scrollable Select). When comparing options matters as much as selecting one (show options visibly, e.g., as a horizontal selector or list).
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Closed / Default | Label (left). Current value (right). Chevron-down icon (far right). | — | — | — | — |
| Hovered | Row background fills at 10% opacity | Mouse over | — | 60ms | — |
| Focused (closed) | Focus ring on entire row. | Tab / D-pad | — | 60ms | [UI focus sound] |
| Opening | Dropdown list appears below (or above if near screen bottom). List items visible. Previously selected item highlighted. Focus moves to selected item inside list. | Click / Enter / A / Cross | Open list | 100ms ease-out (expand) | [UI expand sound] |
| List item hovered/focused | List item highlights | Mouse / D-pad | — | 60ms | [UI hover sound] |
| List item selected | List closes. Closed state shows new value. onChange event fires. | Click / Enter / A / Cross on item | Select value, close list | 80ms ease-in (collapse) | [UI confirm sound] |
| Dismissed without selecting | List closes. Value unchanged. | Escape / B / Circle / click outside | Dismiss | 80ms | [UI cancel sound] |
| Disabled | 40% opacity. No interaction. | — | — | — | — |
Accessibility:
- Keyboard: Up/Down arrows navigate list items while open. Enter selects. Escape dismisses. First letter of an option jumps focus to first matching item.
- Screen reader: Role: "combobox." Accessible name: the field label. Expanded/collapsed state announced. Current value announced when focused. Each list item announces its value and position: "English, 1 of 12."
- The dropdown list must never obscure the current item or the control that opened it — this is a common failure on small screens.
Implementation Notes: [Godot: Custom implementation using a Button (the
closed state) and a PopupMenu or a VBoxContainer revealed by animation. Native
OptionButton provides accessibility but limited visual customization. Ensure
the popup positions itself above the control if it would be clipped by the screen
bottom. Close the popup on _input detecting click outside its rect.]
List Item
Category: Layout / Input Status: Draft When to Use: A single selectable row in a vertically scrollable list. Achievements, quest log entries, settings categories, save file slots. The list is the container; this is the row within it. When NOT to Use: Grid layouts where items exist in two dimensions (use Grid Item). Non-selectable content rows (remove hover/focus states and the pressed state).
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Default | Full-width row. Icon (optional, left). Primary label. Secondary label / metadata (right or below primary). Chevron (right, if navigates deeper). | — | — | — | — |
| Hovered | Row background at 12% opacity highlight. | Mouse over | — | 60ms | — |
| Focused | Focus ring on row OR row background at 20% opacity (consistent with platform convention). | D-pad / Tab | — | 60ms | [UI focus sound] |
| Selected (persistent) | Row background at 25% opacity. May show a selection indicator (left border, checkmark). Distinct from focused state — a row can be selected but not focused. | — | Rendered state | — | — |
| Pressed / Activated | Brief brightness flash, then navigates or performs action | Click / Enter / A / Cross | Navigation or action | 80ms flash | [UI confirm sound] |
| Disabled | 40% opacity. No interaction. | — | — | — | — |
Accessibility:
- Keyboard/Gamepad: Up/Down arrows or D-pad to move between list items. The list must handle focus cycling — reaching the bottom should stop (not wrap) unless wrapping is explicitly designed.
- Screen reader: Role: "listitem." Parent list role: "list." Accessible name: primary label content. Metadata (secondary label) is optionally included in the description. Position announced: "Quest Log, 3 of 12."
- Minimum row height: 44pt / 48dp for touch. For controller-primary platforms, 56px rows are more comfortable.
Implementation Notes: [Godot: Use a VBoxContainer inside a ScrollContainer.
Each row is a custom Control or PanelContainer with a _gui_input override.
For keyboard navigation inside the scroll container, implement custom focus
traversal — Godot's default Tab navigation does not scroll the container to keep
focused items in view. Use ensure_control_visible() on the scroll container.]
Grid Item
Category: Layout / Input Status: Draft When to Use: A selectable cell in a two-dimensional grid. Inventory slots, ability select, crafting ingredient selection, character portrait selection. The grid is the container; this is the cell. When NOT to Use: Single-column content (use List Item). Non-selectable display cells (remove interactive states).
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Empty | Empty slot visual (subtle border or dashed outline). Different from disabled. | — | — | — | — |
| Populated | Item icon fills cell. Stack count (bottom right, if applicable). Quality indicator (border color or icon overlay). | — | — | — | — |
| Hovered | Brightness +15%. Tooltip appears after 400ms delay. | Mouse over | — | 60ms | — |
| Focused | Focus ring (2px, offset 2px). Same brightness as hovered. Tooltip appears after 400ms delay or immediately on gamepad. | D-pad navigation | — | 60ms | [UI focus sound] |
| Selected (persistent) | Distinct border (thicker, contrasting color). May show selection checkmark. | Click / Enter / A / Cross | Select item. Can coexist with focused state on a different cell. | Instant | [UI select sound] |
| Pressed | Brief scale 0.95x, then executes action | Double-click / Enter / A / Cross | Action (equip, use, inspect — defined by context) | 80ms | [UI confirm sound] |
| Locked | Padlock overlay icon on populated content. No hover/focus states. | — | No interaction | — | — |
| Drag source | Cell dims (50% opacity), drag preview appears at cursor. | Click + drag (mouse only) | Begin drag operation | Instant | [UI grab sound] |
| Drop target (valid) | Cell brightens, accepting color indicator | Item dragged over | — | 60ms | — |
| Drop target (invalid) | Red tint or shake animation | Item dragged over invalid slot | — | 60ms | [UI error sound] |
Accessibility:
- Keyboard/Gamepad: D-pad or arrow keys navigate cells. The grid must communicate its dimensions to screen readers. Row/column position announced.
- Screen reader: Role: "gridcell." Parent role: "grid." Accessible name: item name (or "empty slot" for empty cells). State: "selected" when selected, "dimmed" when locked. Position: "row 2, column 3."
- Tooltips must be reachable by keyboard — they must appear when the cell is focused, not only when hovered.
Implementation Notes: [Godot: GridContainer with fixed column count. Each
cell is a custom Control. Implement custom D-pad navigation by overriding
_gui_input and calculating the cell to the left/right/above/below based on
index and column count. GridContainer does not provide this natively.]
Modal Dialog
Category: Feedback / Layout Status: Draft When to Use: A decision or acknowledgment that must be resolved before the player can continue. The dialog is blocking — background content is dimmed and non-interactive. "Are you sure?", "Your progress will be saved.", error states. When NOT to Use: Non-blocking notifications (use Toast / Notification). Information that can wait until the player is ready (add it to a persistent help system instead). Dialogs that should allow the player to continue playing behind them.
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Opening | Background overlay animates from 0 to 60% opacity. Dialog panel scales from 0.9 to 1.0. Dialog enters from center (not from an edge). | Triggered by code | Focus moves to first interactive element in dialog (or the Primary button) | 200ms ease-out | [UI modal open sound] |
| Active | Background non-interactive. Dialog has all input focus. Player cannot interact with background. | Keyboard / gamepad navigates within dialog only | — | — | — |
| Dismissing (confirmed) | Dialog panel scales to 1.1 then fades. Overlay fades to 0%. | Primary button pressed | Execute action, return focus to trigger element | 180ms | [UI confirm sound] |
| Dismissing (cancelled) | Dialog panel scales to 0.9 then fades. Overlay fades to 0%. | Secondary button / Escape / B / Circle | No action, return focus to trigger element | 150ms | [UI cancel sound] |
| Cannot dismiss | If the dialog represents a blocking error, do not provide a cancel path. Provide only resolution options. | — | — | — | — |
Focus trap rule: While a modal dialog is open, Tab and D-pad navigation must cycle within the dialog's interactive elements only. It must not be possible to navigate focus outside the dialog to the background content. This is both an accessibility requirement (WCAG 2.1 SC 2.1.2) and a UX integrity requirement. When the dialog closes, focus must return to the element that triggered it, not to the top of the page.
Accessibility:
- Screen reader: Dialog container role: "dialog." Accessible name: dialog title (required — every dialog must have a title, even if visually hidden). On open, screen reader announces dialog title and first focusable element. Focus trap active.
- Keyboard: Escape key always maps to the cancel/dismiss action (same as Secondary button or close button). Enter always maps to the primary/confirm action.
- Motion reduction: Scale animation replaced with instant appear/disappear. Overlay fade retained at 100ms (faster).
Implementation Notes: [Godot: Implement as a CanvasLayer with a high layer
value (100+) to ensure it renders above all game content. The background overlay
is a full-screen ColorRect at 60% black opacity. Use grab_focus() on the
dialog's primary button after the open animation completes. Override _input() to
implement the focus trap — intercept Tab navigation and reroute to the dialog's
focusable elements.]
Confirmation Dialog
Category: Feedback / Layout Status: Draft When to Use: The specific case of confirming a destructive action. Always triggered by Button (Destructive). Always has exactly two options: confirm (labeled with the specific action, not "OK") and cancel. When NOT to Use: Non-destructive confirmations. Errors or notifications that do not require a decision. Any dialog with more than two actions.
Label rule: The confirm button must be labeled with the specific action, not a generic "OK" or "Yes." "Delete Save File" not "OK." "Leave Match" not "Yes." This reduces mistakes for players who have difficulty reading the dialog content quickly. The pattern comes from Apple HIG and is validated by decades of usability research.
Structure:
- Title: Brief, action-describing. "Delete save file?" not "Are you sure?"
- Body: One sentence stating the consequence. "This cannot be undone."
- Confirm button: Button (Primary) — labeled with the specific action. "Delete Save File."
- Cancel button: Button (Secondary) — "Cancel."
- Default focus: Cancel (safer default — reduces accidental destructive actions).
Accessibility: Inherits all Modal Dialog accessibility. Additionally: screen reader announces "Alert dialog, [title]" to signal destructive context. Default focus on Cancel is a requirement, not a preference.
Implementation Notes: [Confirmation Dialog is a specific instance of Modal
Dialog — implement it as a subclass or as a parameterized scene. The default
focus on Cancel is critical: set grab_focus() on the Cancel button, not the
Confirm button, after open animation completes.]
Toast / Notification
Category: Feedback Status: Draft When to Use: Brief, non-blocking information that does not require a player decision. "Game saved." "Achievement unlocked." "Your inventory is full." The player can continue playing; the notification disappears on its own. When NOT to Use: Information that requires a decision (use Modal Dialog). Errors that require the player to take action. Critical information that the player must not miss.
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Entering | Slides in from screen edge (typically bottom-right, away from primary action areas). Fades from 0 to 100% opacity. | Triggered by code | — | 200ms ease-out | [Sound matching notification type — see Sound Standards] |
| Displayed | Full opacity. Optional: icon (left), title, body text (optional), dismiss button (X, optional). | Pointer hover pauses auto-dismiss timer | Pause auto-dismiss | — | — |
| Auto-dismiss | Fades from 100 to 0% opacity, slides out | Timer expires (5 seconds default for one-line; 8 seconds for two-line) | Remove from queue | 200ms ease-in | — |
| Manual dismiss | Fades and slides out immediately | Click/tap X button or swipe on touch | Remove | 150ms | [UI cancel sound, quiet] |
| Queue overflow | New notification pushes oldest out early | New notification triggered while previous is displayed | FIFO queue, max 3 simultaneous | — | — |
Accessibility:
- Screen reader: Toasts must be read aloud without requiring focus. In HTML, this uses
role="status"orrole="alert". In game UI, this requires the engine's accessibility notification system. Verify engine support in engine-reference docs. - Motion reduction: Slide animation replaced with fade only.
- Toasts must never be the sole communication channel for information the player needs to act on. If the information requires action, use a persistent UI element in addition to the toast.
- Auto-dismiss timer: 5 seconds is the minimum. Players with cognitive processing differences may need more time. Consider a setting to extend to 10 or 15 seconds.
Implementation Notes: [Godot: Manage a queue of PanelContainer scenes in a
VBoxContainer anchored to a screen corner. Each toast is instantiated, added to
the container, then auto-removed after a timer. The container should be on a high
CanvasLayer (50+) but below modal dialogs (100+). Animate using a Tween on
modulate.a and position.x. When motion reduction is active, skip the position
animation.]
Tooltip
Category: Feedback Status: Draft When to Use: Contextual information that supplements a visible label. Item descriptions in inventory. Stat explanations on a character sheet. Setting descriptions in accessibility options. The player must be able to access this information or proceed without it. When NOT to Use: Information the player MUST read to complete an action — put that in the label or body text, not a tooltip. Tooltips are not discoverable on mobile touch without a hover state. On touch-only platforms, use an info button that opens a description modal instead.
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Hidden | — | — | — | — | — |
| Hover trigger | — | Mouse enters element | Begin 400ms delay timer | — | — |
| Gamepad/keyboard trigger | — | Element receives focus | Begin 300ms delay timer (shorter because navigation is intentional) | — | — |
| Appearing | Tooltip panel fades in and scales from 0.95 to 1.0. Positioned near element (prefer above, adjust if near screen edge). | Timer expires | Show tooltip | 120ms ease-out | — |
| Displayed | Tooltip visible. Title (optional). Body text. Max width: 300px. Multiple lines allowed. | — | — | — | — |
| Hiding | Tooltip fades out | Mouse leaves element / focus moves away | Hide tooltip | 80ms ease-in | — |
Accessibility:
- Screen reader: Tooltip content must be accessible without hover. The accessible name of the parent element should include the most critical tooltip information. The full tooltip text is optionally in the
descriptionproperty. Screen reader reads tooltip content when element is focused. - The delay (300-400ms) prevents accidental tooltip display and is required — instant tooltips are disruptive in gamepad navigation.
- Tooltip text must meet the same contrast requirements as body text (4.5:1 minimum).
Implementation Notes: [Godot: Attach a custom TooltipControl scene as a
child of the trigger element. Show/hide with a Timer node. Position the tooltip
using a CanvasLayer to ensure it appears above all other UI. For screen edges,
detect if the tooltip rect extends beyond get_viewport_rect() and flip the
position to the opposite side.]
Progress Bar
Category: Feedback / Layout Status: Draft When to Use: Linear progress toward a defined endpoint. Loading screens (time to completion), XP fill toward next level, quest objectives with countable progress ("3 of 10 enemies defeated"), download progress. When NOT to Use: Circular or radial progress (use a separate Radial Progress pattern if needed). Values that fluctuate up and down rapidly (use Health/Resource Bar pattern). Values with no defined endpoint.
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Default | Track (full width, background color). Fill (left to right, value color). Value label (percentage or N/M, outside or inside fill). | — | — | — | — |
| Value increasing | Fill width animates to new value | Value changes | Smooth fill animation | 300ms ease-out | [Context-dependent — XP gain has a sound; loading has none] |
| Value at maximum | Fill reaches full width. Optional: completion animation (pulse, glow). | Value reaches 100% | Completion event fires | 200ms | [Completion sound if appropriate] |
| Value at zero | Fill hidden (zero width). Track still visible. | — | — | — | — |
| Indeterminate (unknown duration) | Animated loop (fill segment moves left-to-right, repeat). Used for loading of unknown duration. | — | — | Infinite loop | — |
Accessibility:
- Screen reader: Role: "progressbar." Accessible name: what is progressing (e.g., "Experience Points," "Loading"). Value: current numeric value AND percentage AND maximum. "Experience Points, 450 of 1000, 45 percent." Update on significant changes (not every pixel).
- Do not rely only on fill color to communicate value. Include a numeric label.
- Indeterminate progress bars: announce "Loading, in progress" — do not announce changes since the value is unknown.
- Motion reduction: Indeterminate animation is replaced with a static "loading" indicator. Smooth fill animation is replaced with instant jump to new value.
Implementation Notes: [Godot: ProgressBar built-in with custom theming.
For indeterminate mode, ProgressBar does not have a native indeterminate state
in Godot 4.x — implement using a looping Tween on a fill element's position.
Ensure the Tween is paused when motion reduction mode is active and a static
indicator is shown instead.]
Input Field
Category: Input Status: Draft When to Use: Text entry. Player name on a new save, search within a list, remapping a key binding (special case — shows the key press, not typed text), entering a numeric value precisely. When NOT to Use: Selecting from known options (use Dropdown or List). On console-primary platforms, minimize text entry — it requires a virtual keyboard, which is high friction.
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Default | Field border, placeholder text (label-style, muted color), empty input area. | — | — | — | — |
| Hovered | Border brightens slightly | Mouse over | — | 60ms | — |
| Focused | Border brightens fully. Cursor (blinking, 530ms on/530ms off). Placeholder text hidden. | Tab / click | Open virtual keyboard on console/mobile | Instant | [UI focus sound] |
| Typing | Characters appear. Cursor advances. | Keyboard input | Update field value | Immediate | [Subtle keystroke sound, optional] |
| Value present | Field shows typed value. Placeholder hidden. Clear button appears (X, right of field) if value is non-empty. | — | — | — | — |
| Character limit reached | No further input accepted. Optional: brief shake animation and limit indicator changes color. | Input at limit | Reject further characters | 200ms shake | [UI error sound, subtle] |
| Clear | Field empties. Cursor returns. Clear button disappears. | Click X / gamepad clear input | Clear value | Instant | [UI cancel sound, subtle] |
| Validation error | Border turns error color (red — ensure colorblind safe). Error message appears below field. | On submit or on blur | Show error | Instant | [UI error sound] |
| Validated / correct | Border turns success color (green — ensure colorblind safe). Success icon optional. | On validation pass | — | Instant | — |
| Disabled | 40% opacity, no interaction. Value still visible. | — | — | — | — |
Accessibility:
- Keyboard: All standard text editing shortcuts (Home, End, Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+Z).
- Screen reader: Role: "textbox." Accessible name: field label (not placeholder text). Current value announced. Character limit announced when reached. Validation errors announced immediately on occurrence.
- Placeholder text must not be used as the only label — a visible label above or beside the field is required. Placeholder text disappears when the player types, causing confusion for players with cognitive or memory impairments.
Implementation Notes: [Godot LineEdit: set placeholder_text for the hint
but always include a visible Label node as the field's accessible name. Bind
text_changed signal for real-time validation. Bind text_submitted for form
submission on Enter. On console, LineEdit.call("_popup_keyboard") or use the OS
virtual keyboard API — verify against engine-reference/godot/ for Godot 4.6
console keyboard API specifics.]
Tab Bar
Category: Navigation Status: Draft When to Use: Dividing a single screen's content into discrete sections where only one section is visible at a time. Character sheet tabs (Stats / Equipment / Skills), settings tabs (Gameplay / Graphics / Audio / Accessibility). Maximum 5-6 tabs before the pattern breaks down and a sidebar navigation should be considered instead. When NOT to Use: More than 6 tabs. Content that benefits from simultaneous visibility (use a layout pattern instead). Navigation between different screens (use Screen Push).
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Default (inactive tab) | Tab label. No active indicator. | — | — | — | — |
| Active tab | Tab label. Active indicator (underline, fill, or contrasting background). Content area shows this tab's content. | — | — | — | — |
| Hovered (inactive) | Tab background fills slightly | Mouse over | — | 60ms | — |
| Focused (keyboard/gamepad) | Focus ring on tab label. | Tab key (within tab bar) or D-pad left/right on tab row | — | 60ms | [UI focus sound] |
| Activated | Active indicator transitions to this tab. Content area transitions (fade or slide). | Click / Enter / A / Cross | Switch active tab. Content update. | 150ms ease | [UI tab switch sound] |
| Gamepad shoulder button | — | L1/R1 (PS) or LB/RB (Xbox) | Switch to previous/next tab (standard platform convention) | 150ms | [UI tab switch sound] |
Accessibility:
- Keyboard: Arrow keys navigate between tabs within the tab bar (left/right). Tab key moves focus into the content area below. This follows the ARIA tab panel pattern.
- Screen reader: Role: "tab" for individual tabs. Role: "tablist" for the container. Role: "tabpanel" for the content area. Active tab state: "selected." Accessible name: tab label. Tabpanel is labeled by its corresponding tab.
- The active tab must be visually distinguishable by more than color alone (underline, fill pattern, or weight change in addition to color).
Implementation Notes: [Godot: TabContainer built-in. For custom visual
styling, implement manually with a HBoxContainer of tab buttons and a
MarginContainer for content. The shoulder button shortcut (LB/RB) must be
implemented in the screen's _input() override — it is not built into Godot's
tab system. Check platform conventions: Xbox uses LB/RB; PlayStation uses L1/R1;
both are the same physical button, so a single binding works.]
Scroll Container
Category: Layout Status: Draft When to Use: Content that exceeds the visible area of its container. Inventory lists, lore entry text, credits, long settings lists. The scroll indicator shows the player that more content exists. When NOT to Use: Content that can be paginated instead (pagination may be clearer for dense list navigation). Infinite scroll (always provide a loading state and an end state).
Interaction Specification:
| State | Visual | Input | Response | Duration | Audio |
|---|---|---|---|---|---|
| Content fits | No scrollbar visible (or always-visible scrollbar at full height, depending on art direction). | — | — | — | — |
| Scrollable | Scrollbar appears (right edge). Scrollbar thumb size represents viewport vs. content ratio. | — | — | — | — |
| Scrolling (mouse) | Content moves. Scrollbar thumb moves proportionally. | Mouse wheel | Scroll by 3 lines per wheel tick (configurable in OS) | Smooth | — |
| Scrollbar drag | Content moves. Thumb follows pointer. | Click + drag scrollbar thumb | Scroll proportionally | Real time | — |
| Keyboard scroll | Content moves one item height per keypress. | Up/Down arrows when container is focused and no child is focused | Scroll by one unit | Immediate | — |
| Gamepad scroll | Content moves to keep focused item in view. | D-pad navigation to items beyond visible area | Auto-scroll to keep focused item visible | Smooth 150ms | — |
| Scroll top / bottom | Content stops. Scrollbar thumb at end. | Content boundary reached | Stop scrolling | — | — |
| Focus follows scroll | When a child element receives focus, scroll container ensures it is fully visible. | Any child receives focus | Scroll to reveal focused element | 200ms ease | — |
Accessibility:
- Keyboard/Gamepad: The scroll container itself should not require explicit scrollbar interaction — navigating list items inside it should auto-scroll to keep focused items in view.
- Screen reader: The scroll container announces "scrollable" and the scroll position ("showing items 5 through 15 of 30"). This requires engine accessibility support — verify in engine-reference/godot/.
- Fade edges (content fading at scroll boundaries to indicate more content exists) are a helpful visual affordance but must not be the only indicator that content exists beyond the visible area. Include a scrollbar.
Implementation Notes: [Godot ScrollContainer: call ensure_control_visible()
on the focused child whenever gui_focus_changed fires inside the container.
Bind this via a recursive connect on the container's gui_focus_changed signal.
For smooth scroll animation, use a Tween on scroll_vertical rather than
setting it directly.]