docs: add UX design specification and design direction mockups

Complete 14-step UX design workflow covering project understanding,
core experience, emotional response, visual foundation, user journeys,
component strategy, UX patterns, and responsive/accessibility strategy.
This commit is contained in:
2026-04-12 23:37:55 +07:00
parent 638b001297
commit 93bb4e89d4
2 changed files with 1157 additions and 0 deletions
@@ -0,0 +1,369 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2048 Clone - Design Direction</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700;800&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif;
background: #faf8ef;
color: #776e65;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
min-height: 100vh;
}
/* === HEADER === */
.header {
width: 500px;
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.title { font-size: 80px; font-weight: 800; line-height: 1; }
.scores {
display: flex;
gap: 8px;
}
.score-box {
background: #bbada0;
border-radius: 3px;
padding: 10px 25px;
text-align: center;
min-width: 80px;
}
.score-label {
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
color: #eee4da;
letter-spacing: 0.5px;
}
.score-value {
font-size: 25px;
font-weight: 700;
color: #f9f6f2;
}
/* === SUBTITLE === */
.subtitle-row {
width: 500px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.subtitle { font-size: 18px; }
.subtitle strong { font-weight: 700; }
.new-game-btn {
background: #8f7a66;
color: #f9f6f2;
border: none;
border-radius: 3px;
padding: 10px 20px;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: background 0.15s;
}
.new-game-btn:hover { background: #9f8b77; }
/* === GRID === */
.grid-container {
width: 500px;
height: 500px;
background: #bbada0;
border-radius: 6px;
padding: 15px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(4, 1fr);
gap: 15px;
position: relative;
}
.cell {
background: #cdc1b4;
border-radius: 3px;
}
/* === TILES === */
.tile {
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
user-select: none;
}
.tile-2 { background: #eee4da; color: #776e65; font-size: 55px; }
.tile-4 { background: #ede0c8; color: #776e65; font-size: 55px; }
.tile-8 { background: #f2b179; color: #f9f6f2; font-size: 55px; }
.tile-16 { background: #f59563; color: #f9f6f2; font-size: 45px; }
.tile-32 { background: #f67c5f; color: #f9f6f2; font-size: 45px; }
.tile-64 { background: #f65e3b; color: #f9f6f2; font-size: 45px; }
.tile-128 { background: #edcf72; color: #f9f6f2; font-size: 35px; box-shadow: 0 0 30px 10px rgba(243,215,116,0.25); }
.tile-256 { background: #edcc61; color: #f9f6f2; font-size: 35px; box-shadow: 0 0 30px 10px rgba(243,215,116,0.32); }
.tile-512 { background: #edc850; color: #f9f6f2; font-size: 35px; box-shadow: 0 0 30px 10px rgba(243,215,116,0.4); }
.tile-1024 { background: #edc53f; color: #f9f6f2; font-size: 25px; box-shadow: 0 0 30px 10px rgba(243,215,116,0.48); }
.tile-2048 { background: #edc22e; color: #f9f6f2; font-size: 25px; box-shadow: 0 0 30px 10px rgba(243,215,116,0.56); }
.tile-super{ background: #3c3a32; color: #f9f6f2; font-size: 25px; }
/* === GAME OVER OVERLAY === */
.overlay {
position: absolute;
inset: 0;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
opacity: 0;
pointer-events: none;
transition: opacity 0.8s;
}
.overlay.game-over { background: rgba(238, 228, 218, 0.73); }
.overlay.win { background: rgba(237, 194, 46, 0.5); }
.overlay-message {
font-size: 60px;
font-weight: 700;
color: #776e65;
}
.win .overlay-message { color: #f9f6f2; }
/* === DESIGN DIRECTION TABS === */
.direction-tabs {
width: 500px;
margin-top: 40px;
border-top: 2px solid #bbada0;
padding-top: 20px;
}
.direction-tabs h2 {
font-size: 24px;
margin-bottom: 15px;
}
.tab-row {
display: flex;
gap: 8px;
margin-bottom: 20px;
}
.tab-btn {
background: #bbada0;
color: #f9f6f2;
border: none;
border-radius: 3px;
padding: 8px 16px;
font-size: 14px;
font-weight: 700;
cursor: pointer;
}
.tab-btn.active { background: #8f7a66; }
.state-desc {
font-size: 16px;
line-height: 1.6;
margin-bottom: 20px;
padding: 15px;
background: #eee4da;
border-radius: 6px;
}
.state-desc strong { color: #776e65; }
/* === MOBILE === */
@media (max-width: 520px) {
.header, .subtitle-row, .grid-container, .direction-tabs { width: 280px; }
.grid-container { height: 280px; gap: 10px; padding: 10px; }
.title { font-size: 45px; }
.score-box { padding: 6px 12px; min-width: 55px; }
.score-label { font-size: 10px; }
.score-value { font-size: 16px; }
.subtitle { font-size: 13px; }
.new-game-btn { padding: 6px 12px; font-size: 13px; }
.tile-2, .tile-4, .tile-8 { font-size: 30px; }
.tile-16, .tile-32, .tile-64 { font-size: 25px; }
.tile-128, .tile-256, .tile-512 { font-size: 20px; }
.tile-1024, .tile-2048, .tile-super { font-size: 14px; }
.overlay-message { font-size: 35px; }
}
</style>
</head>
<body>
<!-- Header -->
<div class="header">
<div class="title">2048</div>
<div class="scores">
<div class="score-box">
<div class="score-label">Score</div>
<div class="score-value" id="score">7832</div>
</div>
<div class="score-box">
<div class="score-label">Best</div>
<div class="score-value">21504</div>
</div>
</div>
</div>
<!-- Subtitle -->
<div class="subtitle-row">
<div class="subtitle">Join the numbers and get to the <strong>2048 tile!</strong></div>
<button class="new-game-btn" onclick="showState('midgame')">New Game</button>
</div>
<!-- Grid -->
<div class="grid-container" id="grid">
<!-- Cells and tiles injected by JS -->
</div>
<!-- Design Direction Controls -->
<div class="direction-tabs">
<h2>Game States Preview</h2>
<div class="tab-row">
<button class="tab-btn active" onclick="showState('midgame')">Mid-Game</button>
<button class="tab-btn" onclick="showState('early')">Early Game</button>
<button class="tab-btn" onclick="showState('neardeath')">Near Death</button>
<button class="tab-btn" onclick="showState('gameover')">Game Over</button>
<button class="tab-btn" onclick="showState('win')">Win!</button>
</div>
<div class="state-desc" id="state-desc">
<strong>Mid-Game:</strong> Player has built momentum. Multiple tile tiers visible, showing the color progression from beige (2-4) through orange (8-32) to gold (128+). Grid has breathing room — tension is building but not critical.
</div>
</div>
<script>
const states = {
early: {
grid: [
[0,0,2,0],
[0,4,0,0],
[0,0,2,0],
[0,0,0,0]
],
score: 8,
desc: '<strong>Early Game:</strong> Two starting tiles plus one move. Clean grid, low numbers, maximum possibility. The warm cream background and subtle tile colors create a calm, inviting start.'
},
midgame: {
grid: [
[256,64,8,2],
[128,32,4,0],
[16,8,2,0],
[4,2,0,0]
],
score: 7832,
desc: '<strong>Mid-Game:</strong> Player has built momentum. Multiple tile tiers visible, showing the color progression from beige (2-4) through orange (8-32) to gold (128+). Grid has breathing room — tension is building but not critical.'
},
neardeath: {
grid: [
[512,128,64,16],
[32,16,8,4],
[8,4,2,64],
[2,32,4,0]
],
score: 14280,
desc: '<strong>Near Death:</strong> 15 of 16 cells filled. One empty space left. Every move is critical. The visual density of the board — dark tiles everywhere — signals danger without any explicit warning text.'
},
gameover: {
grid: [
[512,128,64,16],
[32,16,8,4],
[8,4,2,64],
[2,32,4,8]
],
score: 14312,
desc: '<strong>Game Over:</strong> Board full, no merges possible. The translucent overlay dims the board (rgba white at 73% opacity). "Game over!" message is large and unmissable. Try Again button is prominent.',
overlay: 'gameover'
},
win: {
grid: [
[2048,256,64,8],
[128,32,16,4],
[8,4,2,0],
[2,0,0,0]
],
score: 28456,
desc: '<strong>Win!</strong> The 2048 tile achieved. Gold overlay celebrates the moment. "You win!" in bold white. "Keep going" button invites continued play. The 2048 tile glows with maximum intensity.',
overlay: 'win'
}
};
function renderGrid(state) {
const grid = document.getElementById('grid');
grid.innerHTML = '';
// Remove existing overlay
const existingOverlay = grid.querySelector('.overlay');
if (existingOverlay) existingOverlay.remove();
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
const val = state.grid[r][c];
const cell = document.createElement('div');
if (val === 0) {
cell.className = 'cell';
} else {
const tierClass = val >= 4096 ? 'tile-super' : `tile-${val}`;
cell.className = `cell tile ${tierClass}`;
cell.textContent = val;
}
grid.appendChild(cell);
}
}
// Add overlay if needed
if (state.overlay) {
const overlay = document.createElement('div');
overlay.className = `overlay ${state.overlay === 'win' ? 'win' : 'game-over'}`;
overlay.innerHTML = `
<div class="overlay-message">${state.overlay === 'win' ? 'You win!' : 'Game over!'}</div>
<button class="new-game-btn">${state.overlay === 'win' ? 'Keep going' : 'Try again'}</button>
`;
grid.appendChild(overlay);
// Trigger animation
requestAnimationFrame(() => {
overlay.style.opacity = '1';
overlay.style.pointerEvents = 'auto';
});
}
document.getElementById('score').textContent = state.score;
document.getElementById('state-desc').innerHTML = state.desc;
}
function showState(name) {
renderGrid(states[name]);
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
}
// Initial render
showState('midgame');
</script>
</body>
</html>
@@ -0,0 +1,788 @@
---
stepsCompleted: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
lastStep: 14
inputDocuments:
- '_bmad-output/planning-artifacts/prd.md'
- '_bmad-output/brainstorming/brainstorming-session-2026-04-12-001.md'
---
# UX Design Specification - try-bmad
**Author:** MiTi
**Date:** 2026-04-12
---
<!-- UX design content will be appended sequentially through collaborative workflow steps -->
## Executive Summary
### Project Vision
A faithful web-based recreation of the original 2048 puzzle game, built with Svelte + Tailwind CSS + Vite. The focus is on craft over novelty — deeply understanding and reproducing every detail of the original's mechanics and feel. The result serves as both a playable game and a learning artifact for modern web tooling.
### Target Users
- **Casual discoverers** — first-time players who expect intuitive, zero-instruction gameplay
- **Score chasers** — returning players who want persistent best scores and smooth "one more try" loops
- **Mobile players** — on-the-go users who need responsive touch controls and quick session support
- All users are general audience with no assumed technical knowledge; the game must teach itself through play
### Key Design Challenges
1. **Animation choreography** — slide, merge-bounce, pop, and score-float animations must layer cleanly at original timings (100ms/200ms) without visual conflict
2. **Input parity across devices** — swipe gestures must feel as precise and responsive as keyboard arrows; accidental swipes need mitigation (10px minimum threshold)
3. **State communication** — players must always see current score, best score, and game status (win/lose/in-progress); localStorage persistence must be invisible and reliable
### Design Opportunities
1. **Micro-interaction dopamine loop** — merge bounce + score float + tile pop create the satisfying feedback that drives "one more try" behavior
2. **Progressive visual feedback** — 12-tier tile color system and glow effects on 128+ tiles communicate progress without text, rewarding advancement naturally
3. **Zero-friction onboarding** — no tutorial needed; the UX stays out of the way and lets the game mechanics teach themselves
## Core User Experience
### Defining Experience
The core experience is the **directional slide** — a single input (arrow key or swipe) that triggers a cascade: tiles slide, identical values merge, score updates, a new tile spawns. This atomic interaction is the entire game loop. Every UX decision must serve this moment — making it feel instant, satisfying, and predictable.
### Platform Strategy
- **Platform:** Web SPA — browser-based, no installation, fully client-side
- **Desktop input:** Arrow keys (primary), WASD and Vim keys (secondary)
- **Mobile input:** Single-finger swipe with 10px minimum threshold to prevent accidental moves
- **Offline:** Fully functional once loaded — no server dependency at any point
- **Technology:** Pure web standards — CSS transitions for animation, localStorage for persistence, pointer/touch events for input
### Effortless Interactions
- **Slide-merge-score** feels like one continuous motion, not three sequential events
- **Game state persistence** is silent — no save button, no confirmation; refresh and your game is there
- **New Game** is a single tap — no "are you sure?" dialogs
- **Win-to-continue** transition is seamless — victory overlay appears, dismiss to keep playing
- **Input switching** between keyboard and touch works without mode selection
### Critical Success Moments
1. **First merge** — new player slides, two tiles combine, score ticks up. "I get it" moment.
2. **Near-death board** — 15/16 cells filled, every move consequential. Tension must feel exciting, not frustrating.
3. **Reaching 2048** — victory overlay feels earned; "Keep going" option is immediately visible.
4. **Page refresh recovery** — returning to an intact game builds implicit trust in the product.
### Experience Principles
1. **Responsive immediacy** — every input gets instant visual feedback; no action feels ignored or delayed
2. **Invisible infrastructure** — persistence, state management, and platform adaptation are imperceptible to the player
3. **Escalating tension** — UX amplifies the natural difficulty curve through visual density and tile color progression
4. **Respectful simplicity** — no tutorials, tooltips, or onboarding; the grid and starting tiles communicate everything
## Desired Emotional Response
### Primary Emotional Goals
- **Compulsive flow** — the player enters a rhythmic slide-merge-slide state where time blurs and "one more try" is the automatic response to every game over
- **Earned mastery** — each game teaches strategy implicitly; players feel themselves improving without instruction
- **Satisfying feedback** — every action produces visible, audible-feeling results; nothing feels ignored
### Emotional Journey Mapping
| Stage | Emotion | Design Driver |
|-------|---------|---------------|
| First open | Curiosity + clarity | Clean grid, two tiles, no clutter |
| First merge | Understanding + delight | Tile combine + score bump + pop animation |
| Mid-game | Focused absorption | Rhythmic input, rising numbers, pattern recognition |
| Near-death board | Thrilling tension | Board density signals urgency through visual weight |
| Game over | Brief disappointment → motivation | Score comparison triggers "I can beat that" |
| Reaching 2048 | Earned triumph | Victory overlay with seamless keep-going option |
| Return visit | Quiet trust | Game state intact on refresh |
### Micro-Emotions
- **Confidence over confusion** — the game never asks the player to figure out UI, only strategy
- **Accomplishment over frustration** — every game shows a score; every session has measurable progress
- **Excitement over anxiety** — tight boards feel thrilling, not punishing; color progression signals state naturally
### Design Implications
- **Flow state protection** → Animations must not block input; moves can queue during transitions to maintain rhythm
- **Merge delight** → Bounce animation (200ms) + score float create a micro-reward at the exact moment of success
- **Invisible trust** → localStorage persistence never surfaces; no save indicators, no failure states visible to player
- **Natural tension** → Tile color darkening and density communicate board state without explicit warnings or counters
### Emotional Design Principles
1. **Never break rhythm** — the player's input cadence is sacred; animations serve the flow, never interrupt it
2. **Reward every action** — even moves that don't merge should feel acknowledged through smooth tile sliding
3. **Signal through aesthetics** — color, size, and glow communicate game state; no text-based status needed
4. **Fail gracefully** — game over is a pause, not a punishment; the path to "try again" is instant
## UX Pattern Analysis & Inspiration
### Inspiring Products Analysis
**Original 2048 (play2048.co)** — direct reference
- Zero-chrome interface: grid is the entire product, header holds only score + best + new game
- No onboarding: two tiles on a grid teach the game in one interaction
- Animations layer without blocking input; score updates feel causally connected to merges
- Tile color encodes value — experienced players read color before numbers
**Wordle** — emotional arc reference
- Session structure creates anticipation; the attempt → result → retry loop drives retention
- Letter-by-letter reveal animation builds tension through pacing — analogous to merge-bounce micro-celebrations
- Minimal UI with maximum information density; every pixel serves gameplay
**Threes!** — mobile interaction reference
- Buttery smooth swipe with slight overshoot easing — gold standard for touch-based puzzle input
- Warm color palette and tile personality create emotional attachment to game pieces
- "Next tile" preview at screen edge reduces frustration (not used in 2048 for fidelity, but noted as design choice)
### Transferable UX Patterns
**Interaction Patterns:**
- **Zero-chrome layout** (from 2048) — the game grid dominates; UI elements are minimal and peripheral
- **Non-blocking animations** (from 2048) — input is never locked during transitions; moves can queue
- **Weighted swipe** (from Threes!) — touch input has momentum and intentionality; prevents accidental triggers
**Visual Patterns:**
- **Color-as-information** (from 2048) — tile color tiers encode value; players read color before number at higher levels
- **Micro-celebration timing** (from Wordle) — brief animation at moment of success creates dopamine without interrupting flow
- **Score causality** (from 2048) — "+N" float appears at merge location, visually linking action to reward
**Retention Patterns:**
- **"One more try" loop** (from all three) — game over → score comparison → instant restart with zero friction
- **Implicit learning** (from 2048/Threes!) — players develop strategy through play, not instruction
### Anti-Patterns to Avoid
- **Interstitial ads or pauses** — any interruption between game over and restart kills the retry loop
- **Tutorial overlays** — explaining a self-teaching game insults the player and delays the first merge moment
- **Confirmation dialogs on New Game** — friction on restart breaks the "one more try" impulse
- **Over-animated transitions** — animations longer than 200ms feel sluggish and break flow state
- **Score without context** — showing score without best-score comparison removes the motivational anchor
### Design Inspiration Strategy
**Adopt directly:**
- Original 2048's zero-chrome layout, tile color system, and animation timings — this is a faithful clone
- Non-blocking input during animations — critical for flow state preservation
- Instant restart with no confirmation — supports the retry loop
**Adapt for our stack:**
- CSS transitions in Svelte for slide/merge/pop — matching original timings (100ms/200ms) with Svelte's transition system
- Touch handling adapted from Threes! philosophy — weighted, intentional swipe with 10px threshold
**Explicitly avoid:**
- Any UI element not present in the original (no settings, no themes, no undo)
- Any animation that blocks the next input
- Any text-based instruction or tutorial
## Design System Foundation
### Design System Choice
**Tailwind CSS utility-first custom system** — no external component library.
All UI elements in this project are game-specific (tile grid, score display, overlays) with no equivalent in standard component libraries. Tailwind provides the systematic foundation (spacing, colors, transitions, responsive utilities) while allowing fully custom game components.
### Rationale for Selection
1. **Already in tech stack** — Tailwind CSS chosen in PRD; no additional dependency
2. **No applicable component library** — game UI is entirely bespoke (tiles, grid, overlays); Material/Ant/Chakra components don't apply
3. **Fidelity requirement** — faithful clone means matching the original's exact visual language, not adapting a design system's defaults
4. **Utility-first speed** — responsive breakpoints, transition timings, and color values are expressible as Tailwind config without writing raw CSS
5. **Purge-friendly** — unused utilities are stripped at build time, keeping bundle under 50KB target
### Implementation Approach
- **Tailwind config** defines the game's design tokens: tile colors (12-tier), font sizes (55/45/35px), grid dimensions (500px/280px), transition durations (100ms/200ms)
- **Svelte components** own their structure; Tailwind classes handle all styling
- **No CSS-in-JS** — pure utility classes + minimal custom CSS for animations (keyframes for pop/bounce)
- **Responsive** via Tailwind's breakpoint system with single 520px custom breakpoint
### Customization Strategy
- **Custom color palette** in `tailwind.config.js` matching original 2048 tile colors exactly
- **Custom spacing/sizing** for grid cells and container dimensions
- **Custom transition utilities** for game-specific animation durations
- **Minimal `@apply`** — prefer inline utilities; extract only for highly repeated tile-state styles
## Detailed Core User Experience
### Defining Experience
**"Slide tiles to merge and reach 2048."** The defining interaction is the directional slide that causes a merge — the moment two tiles combine into one higher number. Every UX decision serves this atomic moment: making it feel instant, predictable, and satisfying.
### User Mental Model
Players bring a spatial reasoning model: the grid is a physical space with gravity. "Push" all tiles in a direction. Same numbers touching after push = bigger number. The mental model matches the visual representation 1:1 — no abstraction layer, no learning curve. Players who have used any sliding puzzle or played the original 2048 have immediate transfer.
### Success Criteria
- **Input latency** < 16ms — slide must feel instant, never laggy
- **Merge position** matches expectation — result tile appears at leading edge of movement direction
- **Score causality** — "+N" float animation originates from merge location, visually linking action to reward
- **Sequence integrity** — new tile spawns after slide completes, never during; maintains predictable state
- **Input queuing** — next move can be registered during current animation; flow state never interrupted
- **State accuracy** — once-per-move merge rule enforced; no double-merges in single slide
### Novel UX Patterns
No novel patterns required. The directional grid slide is a fully established interaction pattern, perfected by the original 2048. Our approach is **faithful reproduction**, not innovation. The UX challenge is execution quality — matching the original's timings, easing curves, and feedback choreography — not inventing new interactions.
### Experience Mechanics
**1. Initiation**
- Desktop: Arrow key / WASD / Vim key press detected
- Mobile: Single-finger swipe exceeding 10px threshold in dominant direction
- Input ignored if no tiles can move in that direction (no state change = no animation)
**2. Slide Phase (100ms)**
- All movable tiles translate simultaneously toward input direction
- Tiles stop at grid boundary or when blocked by a different-value tile
- CSS transition with ease-in-out timing
**3. Merge Phase (200ms)**
- Identical adjacent tiles in movement path combine into sum value
- Merged tile plays bounce animation (scale 1.0 → 1.2 → 1.0)
- Only one merge per tile per move (once-per-move rule)
- Merge order: leading edge first (tiles closest to movement direction merge first)
**4. Score Update (600ms)**
- "+N" float animation rises from merge position and fades out
- Current score counter increments
- Best score updates if current exceeds previous best
**5. Spawn Phase (200ms)**
- Random empty cell selected for new tile
- 90% chance of value 2, 10% chance of value 4
- New tile plays pop animation (scale 0 → 1.0 with slight overshoot)
**6. State Evaluation (instant)**
- Check for 2048 tile → trigger win overlay (first time only)
- Check for available moves → if none, trigger game over overlay
- Save game state to localStorage
## Visual Design Foundation
### Color System
**Page & Grid:**
- Page background: `#faf8ef` (warm cream)
- Grid background: `#bbada0` (warm grey-brown)
- Empty cell: `#cdc1b4` (light taupe)
**Tile Color Tiers (12 levels):**
| Value | Background | Text Color | Notes |
|-------|-----------|------------|-------|
| 2 | `#eee4da` | `#776e65` | Light beige, dark text |
| 4 | `#ede0c8` | `#776e65` | Slightly darker beige |
| 8 | `#f2b179` | `#f9f6f2` | Orange, white text |
| 16 | `#f59563` | `#f9f6f2` | Deeper orange |
| 32 | `#f67c5f` | `#f9f6f2` | Red-orange |
| 64 | `#f65e3b` | `#f9f6f2` | Bright red |
| 128 | `#edcf72` | `#f9f6f2` | Gold + glow shadow |
| 256 | `#edcc61` | `#f9f6f2` | Deeper gold + glow |
| 512 | `#edc850` | `#f9f6f2` | Rich gold + glow |
| 1024 | `#edc53f` | `#f9f6f2` | Intense gold + glow |
| 2048 | `#edc22e` | `#f9f6f2` | Brightest gold + glow |
| 4096+ | `#3c3a32` | `#f9f6f2` | Dark (super tiles) |
**Glow effect** on 128+ tiles: `box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.4)`
**UI Colors:**
- Header text: `#776e65` (warm dark brown)
- Score box background: `#bbada0`
- Score box text: `#f9f6f2` (off-white)
- Button background: `#8f7a66` (brown)
- Button text: `#f9f6f2`
- Game over overlay: `rgba(238, 228, 218, 0.73)`
- Win overlay: `rgba(237, 194, 46, 0.5)`
### Typography System
**Font Family:** Clear Sans, Helvetica Neue, Arial, sans-serif
**Tile Number Sizes (dynamic by digit count):**
- 1 digit (2-8): `55px` bold
- 2 digits (16-64): `45px` bold
- 3 digits (128-512): `35px` bold
- 4 digits (1024-8192): `25px` bold
- 5+ digits: `15px` bold
**UI Typography:**
- Game title: `80px` bold, `#776e65`
- Score label: `13px` uppercase, `#eee4da`
- Score value: `25px` bold, `#f9f6f2`
- Subtitle/instructions: `18px`, `#776e65`
- Button text: `18px` bold, `#f9f6f2`
- Overlay message: `60px` bold
**Mobile scaling:** All tile font sizes reduce proportionally with the 280px/500px container ratio (0.56x)
### Spacing & Layout Foundation
**Desktop Layout (>520px):**
- Game container width: `500px`, centered
- Grid: `4x4`, cells `~106px` each
- Grid gap: `15px`
- Grid padding: `15px`
- Grid border-radius: `6px`
- Header padding: `0 0 40px 0`
**Mobile Layout (≤520px):**
- Game container: `280px` width
- Grid cells: `~57px` each
- Grid gap: `10px`
- Grid padding: `10px`
- Proportional scaling of all spacing
**Spacing Scale:**
- Base unit: `15px` (grid gap)
- Half: `8px` (inner tile padding)
- Double: `30px` (section gaps)
- Page margin: auto-centered with `max-width: 500px`
### Accessibility Considerations
- **Color contrast:** All tile text meets WCAG AA ratio against its background (dark text on light tiles, white text on dark tiles — transition happens at value 8)
- **Font sizing:** Minimum 15px even on smallest tiles; mobile sizes never drop below readable threshold
- **No color-only information:** Tile values are always displayed as numbers; color is supplementary, not the sole indicator
- **Keyboard accessible:** Full game playable with arrow keys; focus states for New Game button
- **Reduced motion:** Respect `prefers-reduced-motion` media query — disable animations, keep instant state updates
## Design Direction Decision
### Design Directions Explored
Since this is a faithful 2048 clone, design direction exploration focused on verifying that our visual foundation (colors, typography, spacing) accurately reproduces the original's look and feel, rather than exploring divergent visual approaches. A single HTML mockup was generated showing all game states.
**Mockup:** `_bmad-output/planning-artifacts/ux-design-directions.html`
**States verified:**
- Early game (sparse board, calm start)
- Mid-game (multi-tier color progression, building momentum)
- Near-death (15/16 filled, visual density signals tension)
- Game over (translucent white overlay, prominent message)
- Win (gold overlay, "Keep going" option)
### Chosen Direction
**Faithful reproduction of the original 2048 visual language** — warm cream background, earthy tile color progression, zero-chrome layout with grid as dominant element. No alternative directions considered because fidelity to the original is a core project requirement.
### Design Rationale
1. **Fidelity is the goal** — this project exists to faithfully recreate 2048, not to reinterpret it
2. **Proven visual language** — the original's color system, typography, and layout have been validated by millions of players
3. **Learning focus** — implementation quality matters more than design originality; the challenge is reproducing these visuals in Svelte + Tailwind
4. **Emotional alignment** — the warm, earthy palette supports the calm-to-tense emotional journey we defined
### Implementation Approach
- **Svelte components:** `App`, `Grid`, `Tile`, `ScoreBoard`, `GameMessage` — each owns its visual rendering
- **Tailwind config:** Custom color palette, font sizes, spacing scale, and transition durations matching original values
- **CSS animations:** Keyframe definitions for pop (new tile), bounce (merge), and fade (overlays) in global stylesheet
- **Responsive:** Single `@media (max-width: 520px)` breakpoint scales all dimensions proportionally
## User Journey Flows
### Journey 1: Core Gameplay Loop
The universal interaction cycle shared by all user personas.
```mermaid
flowchart TD
A[Player Input] --> B{Valid direction?}
B -->|No tiles can move| A
B -->|Tiles can move| C[Slide all tiles in direction]
C --> D{Any merges?}
D -->|Yes| E[Merge matching tiles]
E --> F[Update score + float animation]
F --> G[Spawn new tile]
D -->|No merges| G
G --> H{Board full?}
H -->|No| A
H -->|Yes| I{Any valid moves remain?}
I -->|Yes| A
I -->|No| J[Game Over]
E --> K{Created 2048 tile?}
K -->|Yes, first time| L[Win Overlay]
K -->|No| G
L --> M{Player choice}
M -->|Keep going| A
M -->|New game| N[Reset board]
N --> A
```
**Key UX decisions in this flow:**
- Invalid moves (no tiles can move in direction) produce no animation, no state change — silent rejection
- Merge check happens during slide, not after — tiles merge as they collide
- Win detection only triggers once per game; subsequent 2048+ merges don't re-trigger
- Score float animation runs concurrently with spawn, not sequentially
### Journey 2: Game Lifecycle
Complete flow from app open to session end.
```mermaid
flowchart TD
A[Open App] --> B{Saved game in localStorage?}
B -->|Yes| C[Restore game state]
B -->|No| D[Initialize new game]
D --> E[Spawn 2 random tiles]
C --> F[Display restored board + scores]
E --> F2[Display fresh board]
F --> G[Gameplay Loop]
F2 --> G
G --> H{Game ends}
H -->|Win| I[Win Overlay - 800ms fade]
H -->|Game Over| J[Game Over Overlay - 800ms fade]
I --> K[Keep going / New Game]
J --> L[Try Again]
K -->|Keep going| G
K -->|New Game| M[Clear localStorage game state]
L --> M
M --> D
G -->|User clicks New Game mid-play| M
G -->|User closes tab| N[Auto-save to localStorage]
```
**Key UX decisions in this flow:**
- No "welcome screen" or splash — straight to game
- Saved game restores silently; player sees their board, not a loading screen
- New Game button works mid-game with no confirmation dialog
- Auto-save happens after every move, not on tab close (beforeunload is unreliable)
### Journey 3: Input Handling (Cross-Platform)
Detailed input flow covering keyboard and touch parity.
```mermaid
flowchart TD
A[Input Event] --> B{Input type?}
B -->|Keyboard| C{Key mapping}
C -->|Arrow keys| D[Map to direction]
C -->|WASD| D
C -->|Vim hjkl| D
C -->|Other key| E[Ignore]
B -->|Touch| F[Record touch start position]
F --> G[Touch move / Touch end]
G --> H{Distance > 10px?}
H -->|No| E
H -->|Yes| I{Dominant axis?}
I -->|Horizontal| J{deltaX direction}
I -->|Vertical| K{deltaY direction}
J -->|Positive| L[Right]
J -->|Negative| M[Left]
K -->|Positive| N[Down]
K -->|Negative| O[Up]
L & M & N & O --> D
D --> P{Animation in progress?}
P -->|No| Q[Execute move immediately]
P -->|Yes| R[Queue move, execute after current animation]
```
**Key UX decisions in this flow:**
- Touch uses dominant axis (larger delta) to determine direction — prevents diagonal ambiguity
- 10px minimum threshold prevents accidental swipes
- Input queuing during animations preserves flow state — player never feels "dropped"
- All keyboard mappings produce identical behavior — no preferential treatment
### Journey Patterns
**Feedback Pattern: Immediate Visual Response**
Every valid input produces visible change within one frame (16ms). Invalid inputs produce no change — silence is the feedback for "that didn't work."
**State Pattern: Invisible Persistence**
Game state saves after every move. Restore is silent. The player never interacts with save/load — it's infrastructure, not a feature.
**Recovery Pattern: Zero-Friction Restart**
From any state (mid-game, game over, win), one tap/click reaches a fresh board. No confirmation, no "are you sure," no transition screen.
### Flow Optimization Principles
1. **Minimum path to value:** App open → playing in 0 steps (restored game) or 0 steps (new game auto-starts)
2. **No dead ends:** Every terminal state (win, game over) has an immediate forward path (keep going, try again, new game)
3. **Silent error handling:** Invalid moves don't produce error messages; absence of response IS the feedback
4. **Concurrent animations:** Score float, tile spawn, and merge bounce can overlap — sequential would feel sluggish
## Component Strategy
### Design System Components
No external component library used. All components are custom Svelte components styled with Tailwind CSS utilities. The "design system" is the Tailwind config (custom colors, spacing, transitions) plus 5 game-specific components.
### Custom Components
#### `App.svelte` — Root Container
**Purpose:** Top-level layout orchestrator; holds game state, routes keyboard input, manages overlays
**Anatomy:** Header (title + scores) → subtitle row (instructions + New Game button) → Grid → GameMessage overlay
**States:** Playing, Won (overlay visible), Game Over (overlay visible)
**Interaction:** Listens for keyboard events on `window`; passes direction to game logic
**Accessibility:** `tabindex` on container for keyboard focus; `role="application"` for game context
#### `Grid.svelte` — Game Board
**Purpose:** Renders the 4x4 grid background with empty cell placeholders; positions Tile components
**Anatomy:** 4x4 CSS grid with `15px` gap; empty cells as background; tiles positioned absolutely over cells
**States:** Single state — always renders 16 cell backgrounds; tile count varies
**Props:** `tiles` array (each tile has `value`, `row`, `col`, `id`, `isNew`, `isMerged`)
**Interaction:** None — purely presentational; touch events handled at App level
**Accessibility:** `role="grid"`, `aria-label="Game board"`
#### `Tile.svelte` — Individual Tile
**Purpose:** Renders a single numbered tile with value-based color, font size, and animations
**Anatomy:** Rounded rectangle with centered number; color from 12-tier system
**States:**
- **Default:** Positioned at grid cell, value-based color and font size
- **Sliding:** CSS transition `transform` over 100ms to new position
- **New (spawning):** Pop animation — scale 0 → 1.0 over 200ms
- **Merged:** Bounce animation — scale 1.0 → 1.2 → 1.0 over 200ms
- **Glowing:** 128+ values get `box-shadow` glow effect
**Props:** `value`, `row`, `col`, `isNew`, `isMerged`
**Dynamic styling:** Font size tier (55/45/35/25/15px) based on digit count; background/text color from value lookup
**Accessibility:** `role="gridcell"`, `aria-label` with tile value
#### `ScoreBoard.svelte` — Score Display
**Purpose:** Shows current score and best score in styled boxes; animates score additions
**Anatomy:** Two side-by-side boxes — each with label ("SCORE"/"BEST") and numeric value
**States:**
- **Default:** Static display of score values
- **Score added:** "+N" float animation rises from score box and fades (600ms)
**Props:** `score`, `bestScore`, `scoreAddition` (triggers float animation)
**Accessibility:** `aria-live="polite"` on score value for screen reader updates
#### `GameMessage.svelte` — Overlay
**Purpose:** Displays win or game-over message with action buttons
**Anatomy:** Full-grid overlay with centered message text + action button(s)
**States:**
- **Hidden:** `opacity: 0`, `pointer-events: none`
- **Win:** Gold background overlay, "You win!" text, "Keep going" + "New Game" buttons
- **Game Over:** White semi-transparent overlay, "Game over!" text, "Try again" button
- **Transition:** 800ms opacity fade-in
**Props:** `type` ("win" | "gameover" | null), `onKeepGoing`, `onNewGame`
**Accessibility:** `role="alertdialog"`, `aria-modal="true"`, auto-focus on primary action button
### Component Implementation Strategy
**Build order follows dependency chain:**
1. `Tile` first — atomic unit, no dependencies, most complex styling
2. `Grid` — depends on Tile, handles positioning logic
3. `ScoreBoard` — independent, simple state
4. `GameMessage` — independent, overlay logic
5. `App` — orchestrates all components, owns game state
**Styling approach:**
- Tailwind utility classes for layout, spacing, colors
- Svelte `class:` directives for conditional tile states
- CSS `@keyframes` in `<style>` blocks for pop/bounce/fade animations
- Dynamic `style` attribute for tile positioning (`transform: translate`)
### Implementation Roadmap
**Phase 1 — MVP (core gameplay):**
- `App`, `Grid`, `Tile`, `ScoreBoard`, `GameMessage`
- All 5 components needed from day one — no phasing possible for a game this small
- Keyboard input only
**Phase 2 — Growth (persistence + mobile):**
- Add touch event handling to `App`
- Add localStorage save/restore logic to `App`
- Add score float animation to `ScoreBoard`
- Add slide/pop/bounce animations to `Tile`
**Phase 3 — Polish:**
- Add glow effect to `Tile` for 128+ values
- Add responsive sizing (all components scale at 520px breakpoint)
- Add `prefers-reduced-motion` support
## UX Consistency Patterns
### Button Hierarchy
**Primary Action**`#8f7a66` background, `#f9f6f2` text, `18px` bold
- Used for: "New Game", "Try again", "Keep going"
- Hover: lighten background 10%
- Active: darken background 5%
- Always full-width within its container context
- No icons — text-only buttons throughout
**Button placement rules:**
- "New Game" always in subtitle row, right-aligned
- Overlay buttons always centered below message text
- Win overlay shows two buttons: "Keep going" (primary) + "New Game" (secondary, same style)
- Game over overlay shows single button: "Try again"
### Feedback Patterns
**Score Update Feedback:**
- "+N" text floats upward from score box, fades over 600ms
- Score counter increments immediately (no counting animation)
- Best score updates silently — no special animation, just value change
**Move Feedback:**
- Valid move: tiles slide (100ms) + optional merge bounce (200ms) + new tile pop (200ms)
- Invalid move (no tiles can move): no visual response at all — silence IS the feedback
- No sound effects, no haptic feedback — purely visual
**State Transition Feedback:**
- Win/game over: overlay fades in over 800ms
- New game: instant reset — no transition animation, board appears immediately with 2 tiles
- Page refresh restore: instant — board appears as-was, no loading indicator
### Overlay Patterns
**When to use:** Only for game-ending states (win, game over). Never for confirmations, settings, or informational messages.
**Visual design:**
- Full-grid coverage (overlays the game board only, not header/scores)
- Semi-transparent background (game board visible underneath)
- Centered message + action button(s)
- 800ms fade-in transition
**Behavior:**
- Overlays don't block keyboard input for restart (Enter key = primary action)
- No close button or click-outside-to-dismiss — only explicit action buttons
- Win overlay is dismissable (keep going); game over overlay requires action (try again/new game)
**Accessibility:**
- `role="alertdialog"` + `aria-modal="true"`
- Auto-focus primary action button on overlay appear
- Trap focus within overlay while visible
### Animation Patterns
**Timing Standards:**
| Animation | Duration | Easing | Trigger |
|-----------|----------|--------|---------|
| Tile slide | 100ms | ease-in-out | Every valid move |
| Tile pop (spawn) | 200ms | ease (scale 0→1) | New tile appears |
| Tile bounce (merge) | 200ms | ease (scale 1→1.2→1) | Tiles merge |
| Score float | 600ms | ease-out (translate + fade) | Score increases |
| Overlay fade | 800ms | ease | Win or game over |
**Animation Rules:**
1. Animations never block input — moves queue during transitions
2. Slide completes before spawn begins (sequential dependency)
3. Merge bounce and score float can run concurrently
4. All animations respect `prefers-reduced-motion` — replaced with instant state changes
5. No animation exceeds 800ms — keeps interactions feeling snappy
### State Communication Patterns
**How the game communicates state without text:**
- **Board density** = urgency (more tiles = more tension)
- **Tile color** = progress (beige → orange → red → gold)
- **Tile glow** = milestone (128+ values glow)
- **Overlay presence** = terminal state (win or game over)
- **Score comparison** = motivation (current vs. best)
**No explicit state indicators:** No move counter, no "tiles remaining" display, no difficulty label. The board IS the state display.
## Responsive Design & Accessibility
### Responsive Strategy
**Desktop (>520px):** Full 500px game container, centered on page. Keyboard-primary input. Generous spacing and full-size tile numbers. This is the primary experience.
**Mobile (≤520px):** Compact 280px game container, viewport-width adapted. Touch/swipe-primary input. Proportionally scaled typography and spacing. All functionality identical — no features removed on mobile.
**Tablet:** No special treatment — tablets >520px get desktop layout; smaller tablets get mobile layout. Touch input works at both sizes.
**No landscape/portrait distinction:** The game is a square grid — orientation doesn't affect layout.
### Breakpoint Strategy
**Single breakpoint: 520px** (matching original 2048)
| Property | Desktop (>520px) | Mobile (≤520px) |
|----------|-----------------|-----------------|
| Container width | 500px | 280px |
| Grid cell size | ~106px | ~57px |
| Grid gap | 15px | 10px |
| Grid padding | 15px | 10px |
| Title font | 80px | 45px |
| Tile font (1 digit) | 55px | 30px |
| Tile font (2 digits) | 45px | 25px |
| Tile font (3 digits) | 35px | 20px |
| Tile font (4 digits) | 25px | 14px |
| Score value font | 25px | 16px |
| Overlay message font | 60px | 35px |
**Approach:** Mobile-first CSS with `min-width: 520px` media query for desktop overrides. Tailwind's custom breakpoint in config.
### Accessibility Strategy
**WCAG Level: AA** — industry standard, appropriate for a public web game.
**Color & Contrast:**
- All tile text meets AA contrast ratio (4.5:1 for normal text)
- Dark text (#776e65) on light tiles (2, 4): ratio > 5:1
- White text (#f9f6f2) on colored tiles (8+): ratio > 4.5:1
- Tile values always shown as numbers — color is supplementary, never sole indicator
**Keyboard Accessibility:**
- Full game playable with keyboard only (arrow keys)
- `Tab` navigates to New Game button
- `Enter` activates focused button (including overlay actions)
- Focus visible indicator on New Game button (outline style)
- No keyboard traps — overlay auto-focuses action button, `Tab` cycles within overlay
**Screen Reader Support:**
- `role="application"` on game container (game-specific keyboard behavior)
- `role="grid"` on game board, `role="gridcell"` on tiles
- `aria-label` on tiles with value (e.g., "Tile: 128")
- `aria-live="polite"` on score display for value change announcements
- `role="alertdialog"` on win/game-over overlays
**Motion Sensitivity:**
- Respect `prefers-reduced-motion` media query
- When active: disable all CSS transitions and animations
- Game logic unchanged — only visual transitions removed
- Tiles appear instantly at new positions; overlays appear without fade
**Touch Targets:**
- New Game button: minimum 44x44px touch area
- Overlay action buttons: minimum 44x44px touch area
- Grid swipe: full grid area (500px/280px) is the touch target — no precision needed
### Testing Strategy
**Responsive Testing:**
- Chrome DevTools device emulation (iPhone SE, iPhone 14, iPad, desktop)
- Real device testing on at least one iOS and one Android device
- Verify touch swipe works reliably on real mobile hardware
- Test at 520px boundary — ensure breakpoint transition is clean
**Accessibility Testing:**
- Lighthouse accessibility audit (target: 100 score)
- Keyboard-only gameplay test (complete a full game without mouse/touch)
- `prefers-reduced-motion` toggle test (verify animations disabled)
- Color contrast checker on all tile color tiers
- Screen reader test with VoiceOver (macOS/iOS) for basic game state announcements
**Browser Support:**
- Chrome (latest), Firefox (latest), Safari (latest), Edge (latest)
- No IE11 support required
- Test CSS grid support (universal in modern evergreen browsers)
### Implementation Guidelines
**Responsive Development:**
- Use Tailwind's responsive utilities with custom `game` breakpoint at `520px`
- Container uses `max-width: 500px` + `margin: auto` for centering
- Grid dimensions use CSS custom properties for easy breakpoint switching
- Font sizes defined as Tailwind utilities with responsive variants
**Accessibility Development:**
- Semantic HTML: `<main>`, `<header>`, `<button>` — no divs-as-buttons
- ARIA attributes added directly in Svelte component templates
- Focus management: `element.focus()` on overlay appear, restore focus on dismiss
- `prefers-reduced-motion` check via CSS `@media` query — wrap all `transition` and `animation` properties
- Test with keyboard after every component implementation — don't backfill accessibility later