ux-user-flow

Navigation architecture and user journey patterns for single-page game applications. This skill covers routing, state-driven UI, and progressive disclosure.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "ux-user-flow" with this command: npx skills add matthewharwood/fantasy-phonics/matthewharwood-fantasy-phonics-ux-user-flow

UX User Flow Skill

Navigation architecture and user journey patterns for single-page game applications. This skill covers routing, state-driven UI, and progressive disclosure.

State-Based Routing

Game State Machine

const GAME_STATES = { loading: 'game-loading', playing: 'game-playing', paused: 'game-paused', complete: 'game-complete', menu: 'game-menu' };

class GameRouter { #state = GAME_STATES.loading;

get state() { return this.#state; }

set state(newState) { const oldState = this.#state; this.#state = newState;

document.body.dataset.state = newState;

this.dispatchEvent(new CustomEvent('state-change', {
  detail: { from: oldState, to: newState }
}));

} }

CSS State Display

/* Hide all views by default */ [data-view] { display: none; }

/* Show view matching state */ [data-state="game-loading"] [data-view="loading"], [data-state="game-playing"] [data-view="playing"], [data-state="game-menu"] [data-view="menu"] { display: block; }

URL Hash Routing

class HashRouter { constructor() { window.addEventListener('hashchange', () => this.#handleRoute()); this.#handleRoute(); }

#handleRoute() { const hash = window.location.hash.slice(1) || 'home'; this.dispatchEvent(new CustomEvent('route', { detail: { route: hash } })); }

navigate(route) { window.location.hash = route; } }

Game Phase Navigation

Phase Progression

const PHASES = ['word', 'collision', 'mutation', 'story'];

class PhaseManager { #currentPhase = 'word'; #completedPhases = new Set();

get currentPhase() { return this.#currentPhase; }

canAccess(phase) { const index = PHASES.indexOf(phase); if (index === 0) return true; const prevPhase = PHASES[index - 1]; return this.#completedPhases.has(prevPhase); }

completePhase(phase) { this.#completedPhases.add(phase); const nextIndex = PHASES.indexOf(phase) + 1; if (nextIndex < PHASES.length) { this.#currentPhase = PHASES[nextIndex]; } } }

Phase Indicator UI

<phase-indicator phases="word,collision,mutation,story" current="word"> </phase-indicator>

Multi-Step Flows

Wizard Pattern

Steps are stored as direct references during construction - NO querySelector:

class StepWizard extends HTMLElement { #steps = []; // Direct element references #currentStep = 0;

constructor() { super(); this.attachShadow({ mode: 'open' });

// Build steps during construction, store direct references
const stepData = ['Step 1', 'Step 2', 'Step 3'];
stepData.forEach(title => {
  const step = document.createElement('div');
  step.className = 'step';
  step.setAttribute('part', 'step');
  step.textContent = title;
  this.#steps.push(step);  // Store direct reference
  this.shadowRoot.appendChild(step);
});

this.#updateView();

}

connectedCallback() { this.addEventListener('click', this); this.addEventListener('keydown', this); }

disconnectedCallback() { this.removeEventListener('click', this); this.removeEventListener('keydown', this); }

handleEvent(e) { // Handle navigation via events }

next() { if (this.#currentStep < this.#steps.length - 1) { this.#currentStep++; this.#updateView(); } }

previous() { if (this.#currentStep > 0) { this.#currentStep--; this.#updateView(); } }

#updateView() { // Use direct references - never querySelector this.#steps.forEach((step, i) => { step.hidden = i !== this.#currentStep; step.setAttribute('aria-current', i === this.#currentStep ? 'step' : 'false'); }); } }

customElements.define('step-wizard', StepWizard);

Progress Tracking

<ol class="step-progress" aria-label="Progress"> <li data-status="complete">Create Account</li> <li data-status="current" aria-current="step">Choose Avatar</li> <li data-status="pending">Start Game</li> </ol>

Progressive Disclosure

Reveal on Interaction

class ExpandableSection extends HTMLElement { handleEvent(e) { if (e.type === 'click') { const expanded = this.getAttribute('aria-expanded') === 'true'; this.setAttribute('aria-expanded', !expanded); this.#content.hidden = expanded; } } }

Conditional Rendering

render() { // Only show advanced options when enabled if (this.#showAdvanced) { this.#advancedSection.hidden = false; }

// Unlock features based on progress this.#lockedFeatures.forEach(feature => { const isUnlocked = this.#progress >= feature.requiredProgress; feature.element.toggleAttribute('locked', !isUnlocked); feature.element.setAttribute('aria-disabled', !isUnlocked); }); }

Just-in-Time Help

#showHintIfNeeded() { const attempts = this.#failedAttempts; if (attempts >= 3) { this.#showHint('mild'); } if (attempts >= 5) { this.#showHint('strong'); } }

Menu Navigation

Keyboard Navigation

handleEvent(e) { if (e.type === 'keydown') { switch (e.key) { case 'ArrowDown': e.preventDefault(); this.#focusNext(); break; case 'ArrowUp': e.preventDefault(); this.#focusPrevious(); break; case 'Home': e.preventDefault(); this.#focusFirst(); break; case 'End': e.preventDefault(); this.#focusLast(); break; case 'Escape': this.close(); break; } } }

Menu Structure

<nav aria-label="Game menu"> <ul role="menu"> <li role="none"> <button role="menuitem" aria-current="page">Word Phase</button> </li> <li role="none"> <button role="menuitem">Collision Phase</button> </li> <li role="none"> <button role="menuitem" aria-disabled="true">Story Phase</button> </li> </ul> </nav>

View Transitions

Crossfade Views

[data-view] { opacity: 0; transition: opacity 0.2s ease; pointer-events: none; }

[data-view].active { opacity: 1; pointer-events: auto; }

Slide Transitions

[data-view] { transform: translateX(100%); transition: transform 0.3s ease; }

[data-view].active { transform: translateX(0); }

[data-view].exiting { transform: translateX(-100%); }

Focus Management

On View Change

Store focus target references during construction - NO querySelector:

class ViewRouter extends HTMLElement { #views = new Map(); // view name -> { element, focusTarget } #currentView = null;

constructor() { super(); this.attachShadow({ mode: 'open' });

// Build views during construction
const viewConfigs = [
  { name: 'home', title: 'Home' },
  { name: 'game', title: 'Game' },
  { name: 'settings', title: 'Settings' }
];

viewConfigs.forEach(config => {
  const view = document.createElement('div');
  view.className = 'view';
  view.setAttribute('data-view', config.name);

  // Create and store focus target reference
  const heading = document.createElement('h1');
  heading.textContent = config.title;
  heading.setAttribute('tabindex', '-1');  // Focusable but not in tab order
  view.appendChild(heading);

  // Store both view and focus target as direct references
  this.#views.set(config.name, {
    element: view,
    focusTarget: heading  // Direct reference for focus
  });

  this.shadowRoot.appendChild(view);
});

}

#navigateTo(viewName) { // Hide current view if (this.#currentView) { this.#currentView.element.hidden = true; }

// Show new view
const view = this.#views.get(viewName);
if (view) {
  view.element.hidden = false;
  this.#currentView = view;

  // Focus using direct reference - NO querySelector
  view.focusTarget?.focus();
}

} }

Skip Links

<a href="#main-game" class="skip-link">Skip to game</a> <a href="#word-list" class="skip-link">Skip to word list</a>

History Management

Browser Back/Forward

class HistoryManager { pushState(state, title, url) { history.pushState(state, title, url); }

connectedCallback() { window.addEventListener('popstate', (e) => { if (e.state) { this.#restoreState(e.state); } }); }

#restoreState(state) { this.dispatchEvent(new CustomEvent('history-navigate', { detail: state })); } }

Game Progression Patterns

Save Points

async saveProgress() { const progress = { phase: this.#currentPhase, completedWords: Array.from(this.#completedWords), score: this.#score, timestamp: Date.now() };

await this.#storage.set('progress', progress); }

async loadProgress() { const progress = await this.#storage.get('progress'); if (progress) { this.#restoreProgress(progress); } }

Session Continuity

connectedCallback() { // Check for saved session this.#checkSavedSession(); }

#checkSavedSession() { const saved = this.#storage.get('session'); if (saved) { this.#showResumePrompt(saved); } }

#showResumePrompt(session) { this.dispatchEvent(new CustomEvent('resume-available', { detail: { phase: session.phase, progress: session.progress } })); }

Error Recovery

Graceful Degradation

async #loadPhase(phase) { try { const data = await this.#fetchPhaseData(phase); this.#renderPhase(data); } catch (error) { this.#showErrorRecovery({ message: 'Failed to load phase', retry: () => this.#loadPhase(phase), fallback: () => this.#loadOfflineData(phase) }); } }

Offline Support

if (!navigator.onLine) { this.#showOfflineMode(); this.#loadCachedContent(); }

window.addEventListener('online', () => { this.#syncProgress(); this.#showOnlineMode(); });

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

animejs-v4

No summary provided by upstream source.

Repository SourceNeeds Review
General

webgpu-canvas

No summary provided by upstream source.

Repository SourceNeeds Review
General

web-components-architecture

No summary provided by upstream source.

Repository SourceNeeds Review
General

audio-design

No summary provided by upstream source.

Repository SourceNeeds Review