UI Components Hard
Retro Snake Game
A classic browser-based Snake game built using the HTML5 Canvas API. Features score tracking, speed increments, and a sleek dark mode aesthetic.
Open in Lab
MCP
vanilla-js canvas css react tailwind vue svelte
Targets: TS JS HTML React Vue Svelte
Code
:root {
--game-bg: #0f172a;
--game-accent: #10b981;
--game-text: #f8fafc;
--game-border: #1e293b;
}
.game-container {
background: var(--game-bg);
padding: 1.5rem;
border-radius: 24px;
color: var(--game-text);
font-family: "Inter", sans-serif;
max-width: 440px;
margin: 0 auto;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.game-header {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
}
.score-box {
background: var(--game-border);
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 700;
color: var(--game-accent);
}
.canvas-wrapper {
position: relative;
background: #020617;
border: 4px solid var(--game-border);
border-radius: 12px;
overflow: hidden;
line-height: 0;
}
#game-canvas {
width: 100%;
height: auto;
image-rendering: pixelated;
}
.game-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.overlay-content {
text-align: center;
}
#overlay-title {
font-size: 2rem;
color: var(--game-accent);
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.1em;
}
#overlay-msg {
font-size: 0.875rem;
opacity: 0.8;
margin-bottom: 1.5rem;
}
.start-game-btn {
background: var(--game-accent);
color: var(--game-bg);
border: none;
padding: 0.75rem 2rem;
border-radius: 8px;
font-weight: 700;
text-transform: uppercase;
cursor: pointer;
transition: transform 0.2s;
}
.start-game-btn:hover {
transform: scale(1.05);
}
.game-controls-hint {
text-align: center;
margin-top: 1rem;
font-size: 0.75rem;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
}const canvas = document.getElementById("game-canvas");
const ctx = canvas.getContext("2d");
const scoreEl = document.getElementById("current-score");
const highScoreEl = document.getElementById("high-score");
const overlay = document.getElementById("game-overlay");
const overlayTitle = document.getElementById("overlay-title");
const overlayMsg = document.getElementById("overlay-msg");
const startBtn = document.getElementById("start-game-btn");
const gridSize = 20;
const tileCount = canvas.width / gridSize;
let score = 0;
let highScore = localStorage.getItem("snake-high-score") || 0;
let dx = 0;
let dy = 0;
let snake = [{ x: 10, y: 10 }];
let food = { x: 5, y: 5 };
let gameLoop;
let isGameRunning = false;
highScoreEl.textContent = highScore;
function startGame() {
isGameRunning = true;
score = 0;
scoreEl.textContent = score;
dx = 1;
dy = 0;
snake = [{ x: 10, y: 10 }];
overlay.style.display = "none";
createFood();
if (gameLoop) clearInterval(gameLoop);
gameLoop = setInterval(draw, 100);
}
function createFood() {
food = {
x: Math.floor(Math.random() * tileCount),
y: Math.floor(Math.random() * tileCount),
};
// Don't spawn food on snake
if (snake.some((segment) => segment.x === food.x && segment.y === food.y)) {
createFood();
}
}
function draw() {
moveSnake();
if (checkGameOver()) return;
// Clear Canvas
ctx.fillStyle = "#020617";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw Food
ctx.fillStyle = "#ef4444";
ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize - 2, gridSize - 2);
// Draw Snake
ctx.fillStyle = "#10b981";
snake.forEach((segment, index) => {
// Head is slightly different
if (index === 0) ctx.fillStyle = "#34d399";
else ctx.fillStyle = "#10b981";
ctx.fillRect(segment.x * gridSize, segment.y * gridSize, gridSize - 2, gridSize - 2);
});
}
function moveSnake() {
const head = { x: snake[0].x + dx, y: snake[0].y + dy };
snake.unshift(head);
if (head.x === food.x && head.y === food.y) {
score += 10;
scoreEl.textContent = score;
createFood();
} else {
snake.pop();
}
}
function checkGameOver() {
const head = snake[0];
// Wall collision
const hitWall = head.x < 0 || head.x >= tileCount || head.y < 0 || head.y >= tileCount;
// Self collision
const hitSelf = snake.slice(1).some((segment) => segment.x === head.x && segment.y === head.y);
if (hitWall || hitSelf) {
gameOver();
return true;
}
return false;
}
function gameOver() {
isGameRunning = false;
clearInterval(gameLoop);
overlay.style.display = "flex";
overlayTitle.textContent = "Game Over";
overlayMsg.textContent = `Score: ${score}`;
startBtn.textContent = "Try Again";
if (score > highScore) {
highScore = score;
localStorage.setItem("snake-high-score", highScore);
highScoreEl.textContent = highScore;
}
}
window.addEventListener("keydown", (e) => {
if (!isGameRunning && e.code === "Space") {
startGame();
return;
}
switch (e.key) {
case "ArrowUp":
case "w":
if (dy === 0) {
dx = 0;
dy = -1;
}
break;
case "ArrowDown":
case "s":
if (dy === 0) {
dx = 0;
dy = 1;
}
break;
case "ArrowLeft":
case "a":
if (dx === 0) {
dx = -1;
dy = 0;
}
break;
case "ArrowRight":
case "d":
if (dx === 0) {
dx = 1;
dy = 0;
}
break;
}
});
startBtn.addEventListener("click", startGame);<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple Game</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="game-container">
<div class="game-header">
<div class="score-box">Score: <span id="current-score">0</span></div>
<div class="score-box">High Score: <span id="high-score">0</span></div>
</div>
<div class="canvas-wrapper">
<canvas id="game-canvas" width="400" height="400"></canvas>
<div id="game-overlay" class="game-overlay">
<div class="overlay-content">
<h2 id="overlay-title">Snake Retro</h2>
<p id="overlay-msg">Press Space to Start</p>
<button id="start-game-btn" class="start-game-btn">Start Game</button>
</div>
</div>
</div>
<div class="game-controls-hint">
Use Arrow Keys or WASD to move
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useEffect, useRef, useState, useCallback } from "react";
const GRID = 20;
const TILE = 20;
const SIZE = GRID * TILE;
type Point = { x: number; y: number };
export default function SimpleGameRC() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const stateRef = useRef({
snake: [{ x: 10, y: 10 }] as Point[],
food: { x: 5, y: 5 } as Point,
dx: 0,
dy: 0,
running: false,
score: 0,
gameOver: false,
});
const loopRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [score, setScore] = useState(0);
const [highScore, setHighScore] = useState(() => Number(localStorage.getItem("snake-hs") || 0));
const [phase, setPhase] = useState<"idle" | "running" | "over">("idle");
function placeFood(snake: Point[]): Point {
let f: Point;
do {
f = { x: Math.floor(Math.random() * GRID), y: Math.floor(Math.random() * GRID) };
} while (snake.some((s) => s.x === f.x && s.y === f.y));
return f;
}
const draw = useCallback(() => {
const ctx = canvasRef.current?.getContext("2d");
if (!ctx) return;
const s = stateRef.current;
// Move
const head = { x: s.snake[0].x + s.dx, y: s.snake[0].y + s.dy };
s.snake.unshift(head);
if (head.x === s.food.x && head.y === s.food.y) {
s.score += 10;
setScore(s.score);
s.food = placeFood(s.snake);
} else {
s.snake.pop();
}
// Collision
const hitWall = head.x < 0 || head.x >= GRID || head.y < 0 || head.y >= GRID;
const hitSelf = s.snake.slice(1).some((seg) => seg.x === head.x && seg.y === head.y);
if (hitWall || hitSelf) {
clearInterval(loopRef.current!);
s.running = false;
setPhase("over");
if (s.score > highScore) {
const hs = s.score;
setHighScore(hs);
localStorage.setItem("snake-hs", String(hs));
}
return;
}
// Render
ctx.fillStyle = "#020617";
ctx.fillRect(0, 0, SIZE, SIZE);
ctx.fillStyle = "#ef4444";
ctx.fillRect(s.food.x * TILE, s.food.y * TILE, TILE - 2, TILE - 2);
s.snake.forEach((seg, i) => {
ctx.fillStyle = i === 0 ? "#34d399" : "#10b981";
ctx.fillRect(seg.x * TILE, seg.y * TILE, TILE - 2, TILE - 2);
});
}, [highScore]);
function startGame() {
const s = stateRef.current;
s.snake = [{ x: 10, y: 10 }];
s.dx = 1;
s.dy = 0;
s.score = 0;
s.running = true;
s.gameOver = false;
s.food = placeFood(s.snake);
setScore(0);
setPhase("running");
if (loopRef.current) clearInterval(loopRef.current);
loopRef.current = setInterval(draw, 100);
}
useEffect(() => {
function onKey(e: KeyboardEvent) {
const s = stateRef.current;
if (!s.running && e.code === "Space") {
startGame();
return;
}
switch (e.key) {
case "ArrowUp":
case "w":
if (s.dy === 0) {
s.dx = 0;
s.dy = -1;
}
break;
case "ArrowDown":
case "s":
if (s.dy === 0) {
s.dx = 0;
s.dy = 1;
}
break;
case "ArrowLeft":
case "a":
if (s.dx === 0) {
s.dx = -1;
s.dy = 0;
}
break;
case "ArrowRight":
case "d":
if (s.dx === 0) {
s.dx = 1;
s.dy = 0;
}
break;
}
}
window.addEventListener("keydown", onKey);
return () => {
window.removeEventListener("keydown", onKey);
if (loopRef.current) clearInterval(loopRef.current);
};
}, []);
return (
<div className="min-h-screen bg-[#0d1117] flex flex-col items-center justify-center gap-4 p-6">
<div className="flex gap-8 text-sm">
<span className="text-[#8b949e]">
Score: <strong className="text-[#34d399] tabular-nums">{score}</strong>
</span>
<span className="text-[#8b949e]">
Best: <strong className="text-[#f1e05a] tabular-nums">{highScore}</strong>
</span>
</div>
<div className="relative rounded-xl overflow-hidden border border-[#30363d]">
<canvas ref={canvasRef} width={SIZE} height={SIZE} className="block" />
{phase !== "running" && (
<div className="absolute inset-0 bg-black/70 flex flex-col items-center justify-center gap-4">
<p className="text-[#e6edf3] text-xl font-bold">
{phase === "over" ? "Game Over" : "Snake"}
</p>
{phase === "over" && <p className="text-[#8b949e] text-sm">Score: {score}</p>}
<button
onClick={startGame}
className="px-6 py-2.5 bg-[#238636] border border-[#2ea043] text-white rounded-xl font-semibold text-sm hover:bg-[#2ea043] transition-colors"
>
{phase === "over" ? "Try Again" : "Start Game"}
</button>
</div>
)}
</div>
{/* Mobile D-pad */}
<div className="grid grid-cols-3 gap-1 mt-2">
{[
{
label: "↑",
row: 1,
col: 2,
dir: { dx: 0, dy: -1 },
cond: (s: typeof stateRef.current) => s.dy === 0,
},
{
label: "←",
row: 2,
col: 1,
dir: { dx: -1, dy: 0 },
cond: (s: typeof stateRef.current) => s.dx === 0,
},
{
label: "↓",
row: 2,
col: 2,
dir: { dx: 0, dy: 1 },
cond: (s: typeof stateRef.current) => s.dy === 0,
},
{
label: "→",
row: 2,
col: 3,
dir: { dx: 1, dy: 0 },
cond: (s: typeof stateRef.current) => s.dx === 0,
},
].map(({ label, row, col, dir, cond }) => (
<button
key={label}
onClick={() => {
const s = stateRef.current;
if (s.running && cond(s)) {
s.dx = dir.dx;
s.dy = dir.dy;
}
}}
style={{ gridRow: row, gridColumn: col }}
className="w-10 h-10 bg-[#21262d] border border-[#30363d] rounded-lg text-[#8b949e] hover:text-[#e6edf3] hover:border-[#8b949e]/40 transition-colors flex items-center justify-center"
>
{label}
</button>
))}
</div>
<p className="text-[11px] text-[#484f58]">Arrow keys or WASD to control</p>
</div>
);
}<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const GRID = 20;
const TILE = 20;
const SIZE = GRID * TILE;
const canvas = ref(null);
const score = ref(0);
const highScore = ref(0);
const phase = ref("idle"); // 'idle' | 'running' | 'over'
const state = {
snake: [{ x: 10, y: 10 }],
food: { x: 5, y: 5 },
dx: 0,
dy: 0,
running: false,
score: 0,
gameOver: false,
};
let loopId = null;
function placeFood(snake) {
let f;
do {
f = { x: Math.floor(Math.random() * GRID), y: Math.floor(Math.random() * GRID) };
} while (snake.some((s) => s.x === f.x && s.y === f.y));
return f;
}
function draw() {
const ctx = canvas.value?.getContext("2d");
if (!ctx) return;
const s = state;
const head = { x: s.snake[0].x + s.dx, y: s.snake[0].y + s.dy };
s.snake.unshift(head);
if (head.x === s.food.x && head.y === s.food.y) {
s.score += 10;
score.value = s.score;
s.food = placeFood(s.snake);
} else {
s.snake.pop();
}
const hitWall = head.x < 0 || head.x >= GRID || head.y < 0 || head.y >= GRID;
const hitSelf = s.snake.slice(1).some((seg) => seg.x === head.x && seg.y === head.y);
if (hitWall || hitSelf) {
clearInterval(loopId);
s.running = false;
phase.value = "over";
if (s.score > highScore.value) {
highScore.value = s.score;
localStorage.setItem("snake-hs", String(highScore.value));
}
return;
}
ctx.fillStyle = "#020617";
ctx.fillRect(0, 0, SIZE, SIZE);
ctx.fillStyle = "#ef4444";
ctx.fillRect(s.food.x * TILE, s.food.y * TILE, TILE - 2, TILE - 2);
s.snake.forEach((seg, i) => {
ctx.fillStyle = i === 0 ? "#34d399" : "#10b981";
ctx.fillRect(seg.x * TILE, seg.y * TILE, TILE - 2, TILE - 2);
});
}
function startGame() {
state.snake = [{ x: 10, y: 10 }];
state.dx = 1;
state.dy = 0;
state.score = 0;
state.running = true;
state.gameOver = false;
state.food = placeFood(state.snake);
score.value = 0;
phase.value = "running";
if (loopId) clearInterval(loopId);
loopId = setInterval(draw, 100);
}
function handleDpad(dx, dy, condFn) {
if (state.running && condFn(state)) {
state.dx = dx;
state.dy = dy;
}
}
const dpadButtons = [
{ label: "\u2191", row: 1, col: 2, dx: 0, dy: -1, cond: (s) => s.dy === 0 },
{ label: "\u2190", row: 2, col: 1, dx: -1, dy: 0, cond: (s) => s.dx === 0 },
{ label: "\u2193", row: 2, col: 2, dx: 0, dy: 1, cond: (s) => s.dy === 0 },
{ label: "\u2192", row: 2, col: 3, dx: 1, dy: 0, cond: (s) => s.dx === 0 },
];
function onKey(e) {
if (!state.running && e.code === "Space") {
startGame();
return;
}
switch (e.key) {
case "ArrowUp":
case "w":
if (state.dy === 0) {
state.dx = 0;
state.dy = -1;
}
break;
case "ArrowDown":
case "s":
if (state.dy === 0) {
state.dx = 0;
state.dy = 1;
}
break;
case "ArrowLeft":
case "a":
if (state.dx === 0) {
state.dx = -1;
state.dy = 0;
}
break;
case "ArrowRight":
case "d":
if (state.dx === 0) {
state.dx = 1;
state.dy = 0;
}
break;
}
}
onMounted(() => {
highScore.value = Number(localStorage.getItem("snake-hs") || 0);
window.addEventListener("keydown", onKey);
});
onUnmounted(() => {
window.removeEventListener("keydown", onKey);
if (loopId) clearInterval(loopId);
});
</script>
<template>
<div class="wrapper">
<div class="score-row">
<span class="score-label">Score: <strong class="score-val">{{ score }}</strong></span>
<span class="score-label">Best: <strong class="best-val">{{ highScore }}</strong></span>
</div>
<div class="canvas-wrap">
<canvas ref="canvas" :width="SIZE" :height="SIZE" style="display: block;"></canvas>
<div v-if="phase !== 'running'" class="overlay">
<p class="overlay-title">{{ phase === 'over' ? 'Game Over' : 'Snake' }}</p>
<p v-if="phase === 'over'" class="overlay-score">Score: {{ score }}</p>
<button class="start-btn" @click="startGame">
{{ phase === 'over' ? 'Try Again' : 'Start Game' }}
</button>
</div>
</div>
<div class="dpad">
<button
v-for="btn in dpadButtons"
:key="btn.label"
class="dpad-btn"
:style="{ gridRow: btn.row, gridColumn: btn.col }"
@click="handleDpad(btn.dx, btn.dy, btn.cond)"
>
{{ btn.label }}
</button>
</div>
<p class="hint">Arrow keys or WASD to control</p>
</div>
</template>
<style scoped>
.wrapper {
min-height: 100vh;
background: #0d1117;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1.5rem;
font-family: system-ui, -apple-system, sans-serif;
}
.score-row { display: flex; gap: 2rem; font-size: 0.875rem; }
.score-label { color: #8b949e; }
.score-val { color: #34d399; font-variant-numeric: tabular-nums; }
.best-val { color: #f1e05a; font-variant-numeric: tabular-nums; }
.canvas-wrap {
position: relative;
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid #30363d;
}
.overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
}
.overlay-title { color: #e6edf3; font-size: 1.25rem; font-weight: 700; }
.overlay-score { color: #8b949e; font-size: 0.875rem; }
.start-btn {
padding: 0.625rem 1.5rem;
background: #238636;
border: 1px solid #2ea043;
color: white;
border-radius: 0.75rem;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
}
.start-btn:hover { background: #2ea043; }
.dpad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.25rem; margin-top: 0.5rem; }
.dpad-btn {
width: 2.5rem;
height: 2.5rem;
background: #21262d;
border: 1px solid #30363d;
border-radius: 0.5rem;
color: #8b949e;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.dpad-btn:hover { color: #e6edf3; border-color: rgba(139,147,158,0.4); }
.hint { font-size: 11px; color: #484f58; }
</style><script>
import { onMount, onDestroy } from "svelte";
const GRID = 20;
const TILE = 20;
const SIZE = GRID * TILE;
let canvas;
let score = 0;
let highScore = 0;
let phase = "idle"; // 'idle' | 'running' | 'over'
let state = {
snake: [{ x: 10, y: 10 }],
food: { x: 5, y: 5 },
dx: 0,
dy: 0,
running: false,
score: 0,
gameOver: false,
};
let loopId = null;
function placeFood(snake) {
let f;
do {
f = { x: Math.floor(Math.random() * GRID), y: Math.floor(Math.random() * GRID) };
} while (snake.some((s) => s.x === f.x && s.y === f.y));
return f;
}
function draw() {
const ctx = canvas?.getContext("2d");
if (!ctx) return;
const s = state;
const head = { x: s.snake[0].x + s.dx, y: s.snake[0].y + s.dy };
s.snake.unshift(head);
if (head.x === s.food.x && head.y === s.food.y) {
s.score += 10;
score = s.score;
s.food = placeFood(s.snake);
} else {
s.snake.pop();
}
const hitWall = head.x < 0 || head.x >= GRID || head.y < 0 || head.y >= GRID;
const hitSelf = s.snake.slice(1).some((seg) => seg.x === head.x && seg.y === head.y);
if (hitWall || hitSelf) {
clearInterval(loopId);
s.running = false;
phase = "over";
if (s.score > highScore) {
highScore = s.score;
localStorage.setItem("snake-hs", String(highScore));
}
return;
}
ctx.fillStyle = "#020617";
ctx.fillRect(0, 0, SIZE, SIZE);
ctx.fillStyle = "#ef4444";
ctx.fillRect(s.food.x * TILE, s.food.y * TILE, TILE - 2, TILE - 2);
s.snake.forEach((seg, i) => {
ctx.fillStyle = i === 0 ? "#34d399" : "#10b981";
ctx.fillRect(seg.x * TILE, seg.y * TILE, TILE - 2, TILE - 2);
});
}
function startGame() {
state.snake = [{ x: 10, y: 10 }];
state.dx = 1;
state.dy = 0;
state.score = 0;
state.running = true;
state.gameOver = false;
state.food = placeFood(state.snake);
score = 0;
phase = "running";
if (loopId) clearInterval(loopId);
loopId = setInterval(draw, 100);
}
function handleDpad(dx, dy, condFn) {
if (state.running && condFn(state)) {
state.dx = dx;
state.dy = dy;
}
}
const dpadButtons = [
{ label: "↑", row: 1, col: 2, dx: 0, dy: -1, cond: (s) => s.dy === 0 },
{ label: "←", row: 2, col: 1, dx: -1, dy: 0, cond: (s) => s.dx === 0 },
{ label: "↓", row: 2, col: 2, dx: 0, dy: 1, cond: (s) => s.dy === 0 },
{ label: "→", row: 2, col: 3, dx: 1, dy: 0, cond: (s) => s.dx === 0 },
];
function onKey(e) {
if (!state.running && e.code === "Space") {
startGame();
return;
}
switch (e.key) {
case "ArrowUp":
case "w":
if (state.dy === 0) {
state.dx = 0;
state.dy = -1;
}
break;
case "ArrowDown":
case "s":
if (state.dy === 0) {
state.dx = 0;
state.dy = 1;
}
break;
case "ArrowLeft":
case "a":
if (state.dx === 0) {
state.dx = -1;
state.dy = 0;
}
break;
case "ArrowRight":
case "d":
if (state.dx === 0) {
state.dx = 1;
state.dy = 0;
}
break;
}
}
onMount(() => {
highScore = Number(localStorage.getItem("snake-hs") || 0);
window.addEventListener("keydown", onKey);
});
onDestroy(() => {
window.removeEventListener("keydown", onKey);
if (loopId) clearInterval(loopId);
});
</script>
<div class="wrapper">
<div class="score-row">
<span class="score-label">Score: <strong class="score-val">{score}</strong></span>
<span class="score-label">Best: <strong class="best-val">{highScore}</strong></span>
</div>
<div class="canvas-wrap">
<canvas bind:this={canvas} width={SIZE} height={SIZE} style="display: block;"></canvas>
{#if phase !== 'running'}
<div class="overlay">
<p class="overlay-title">{phase === 'over' ? 'Game Over' : 'Snake'}</p>
{#if phase === 'over'}
<p class="overlay-score">Score: {score}</p>
{/if}
<button class="start-btn" on:click={startGame}>
{phase === 'over' ? 'Try Again' : 'Start Game'}
</button>
</div>
{/if}
</div>
<div class="dpad">
{#each dpadButtons as btn}
<button
class="dpad-btn"
style="grid-row: {btn.row}; grid-column: {btn.col};"
on:click={() => handleDpad(btn.dx, btn.dy, btn.cond)}
>
{btn.label}
</button>
{/each}
</div>
<p class="hint">Arrow keys or WASD to control</p>
</div>
<style>
.wrapper {
min-height: 100vh;
background: #0d1117;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1.5rem;
font-family: system-ui, -apple-system, sans-serif;
}
.score-row { display: flex; gap: 2rem; font-size: 0.875rem; }
.score-label { color: #8b949e; }
.score-val { color: #34d399; font-variant-numeric: tabular-nums; }
.best-val { color: #f1e05a; font-variant-numeric: tabular-nums; }
.canvas-wrap {
position: relative;
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid #30363d;
}
.overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
}
.overlay-title { color: #e6edf3; font-size: 1.25rem; font-weight: 700; }
.overlay-score { color: #8b949e; font-size: 0.875rem; }
.start-btn {
padding: 0.625rem 1.5rem;
background: #238636;
border: 1px solid #2ea043;
color: white;
border-radius: 0.75rem;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
}
.start-btn:hover { background: #2ea043; }
.dpad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.25rem; margin-top: 0.5rem; }
.dpad-btn {
width: 2.5rem;
height: 2.5rem;
background: #21262d;
border: 1px solid #30363d;
border-radius: 0.5rem;
color: #8b949e;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.dpad-btn:hover { color: #e6edf3; border-color: rgba(139,147,158,0.4); }
.hint { font-size: 11px; color: #484f58; }
</style>Retro Snake Game
Relive the classic arcade experience. This component demonstrates the power of the Canvas API for game development. It handles real-time rendering, collision detection, and dynamic game states with efficient loops.
Features
- Responsive Canvas rendering
- Keyboard controls (Arrow keys / WASD)
- Performance-based speed increases
- High score persistence (Local Storage)
- Modern “Neon” visual theme
- Pause/Resume support