xiaohongshu
xiaohongshu
board-game-ui
UI/UX design for digital board games. Use when building game interfaces, implementing drag-and-drop, rendering game boards, showing player information, handling animations, or designing responsive layouts. Covers Canvas, SVG, and DOM-based approaches.
Full skill instructions
This skill provides expertise for building user interfaces for digital board games. It covers rendering approaches (DOM, Canvas, SVG), interaction patterns (drag-and-drop, click-to-select), responsive design for different screen sizes, and UX principles specific to turn-based games.
| Approach | Best For | Avoid When |
|---|---|---|
| DOM/CSS | Card games, simple boards, UI overlays | Many moving pieces, complex animations |
| SVG | Maps, vector graphics, zoomable boards | Thousands of elements, pixel effects |
| Canvas | Complex animations, particles, real-time | Accessibility needed, text-heavy |
| Hybrid | Most board games | Over-engineering simple games |
Use DOM for UI chrome (menus, player info, cards) and Canvas/SVG for the game board:
<div class="game-container">
<!-- DOM: Player info, always visible -->
<aside class="player-panel">
<div class="player-info" data-player="1">...</div>
</aside>
<!-- SVG: The game board/map -->
<main class="board-area">
<svg id="game-board" viewBox="0 0 1000 800">
<!-- Routes, cities, tokens -->
</svg>
</main>
<!-- DOM: Action buttons, cards in hand -->
<footer class="action-bar">
<div class="hand-cards">...</div>
<div class="action-buttons">...</div>
</footer>
</div>
.game-container {
display: grid;
height: 100vh;
gap: 1rem;
padding: 1rem;
/* Desktop: sidebar layout */
grid-template-columns: 250px 1fr;
grid-template-rows: 1fr auto;
grid-template-areas:
"sidebar board"
"sidebar actions";
}
/* Tablet: stack sidebar above */
@media (max-width: 1024px) {
.game-container {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"sidebar"
"board"
"actions";
}
.player-panel {
display: flex;
overflow-x: auto;
}
}
/* Mobile: minimal chrome */
@media (max-width: 600px) {
.game-container {
padding: 0.5rem;
gap: 0.5rem;
}
.player-panel {
font-size: 0.875rem;
}
}
.board-area {
grid-area: board;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
#game-board {
max-width: 100%;
max-height: 100%;
aspect-ratio: 5 / 4; /* Match your board dimensions */
}
<svg id="game-board" viewBox="0 0 1000 800">
<!-- Background layer -->
<g id="background">
<image href="/assets/map-age-1.jpg" width="1000" height="800" />
</g>
<!-- Routes layer -->
<g id="routes">
<path class="route" data-route-id="london-paris"
d="M 200,150 Q 250,200 350,180"
stroke="#666" stroke-width="8" fill="none" />
</g>
<!-- Cities layer -->
<g id="cities">
<g class="city" data-city-id="london" transform="translate(200, 150)">
<circle r="20" fill="#333" />
<text y="35" text-anchor="middle">London</text>
</g>
</g>
<!-- Tokens layer (on top) -->
<g id="tokens">
<g class="ship-token" data-player="1" data-ship-id="ship-1"
transform="translate(200, 150)">
<use href="#airship-icon" fill="var(--player-1-color)" />
</g>
</g>
<!-- Definitions -->
<defs>
<symbol id="airship-icon" viewBox="0 0 40 20">
<ellipse cx="20" cy="10" rx="18" ry="8" />
<rect x="8" y="14" width="24" height="4" rx="2" />
</symbol>
</defs>
</svg>
// Click handling on SVG elements
document.getElementById('game-board').addEventListener('click', (e) => {
const route = e.target.closest('.route');
const city = e.target.closest('.city');
const token = e.target.closest('.ship-token');
if (route) {
handleRouteClick(route.dataset.routeId);
} else if (city) {
handleCityClick(city.dataset.cityId);
} else if (token) {
handleTokenClick(token.dataset.shipId);
}
});
// Hover effects
function setupHoverEffects() {
document.querySelectorAll('.route').forEach(route => {
route.addEventListener('mouseenter', () => {
route.classList.add('highlighted');
showRouteTooltip(route.dataset.routeId);
});
route.addEventListener('mouseleave', () => {
route.classList.remove('highlighted');
hideTooltip();
});
});
}
/* SVG hover/selection states */
.route {
cursor: pointer;
transition: stroke 0.2s, stroke-width 0.2s;
}
.route:hover,
.route.highlighted {
stroke: #4a9eff;
stroke-width: 12;
}
.route.claimed {
stroke: var(--claiming-player-color);
}
.route.unavailable {
opacity: 0.3;
pointer-events: none;
}
.city:hover circle {
fill: #555;
transform: scale(1.1);
}
// Make elements draggable
function setupDraggable(element, type, id) {
element.draggable = true;
element.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('application/json', JSON.stringify({ type, id }));
e.dataTransfer.effectAllowed = 'move';
element.classList.add('dragging');
});
element.addEventListener('dragend', () => {
element.classList.remove('dragging');
clearDropTargets();
});
}
// Make slots accept drops
function setupDropTarget(element, acceptTypes, onDrop) {
element.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
element.classList.add('drop-target');
});
element.addEventListener('dragleave', () => {
element.classList.remove('drop-target');
});
element.addEventListener('drop', (e) => {
e.preventDefault();
element.classList.remove('drop-target');
const data = JSON.parse(e.dataTransfer.getData('application/json'));
if (acceptTypes.includes(data.type)) {
onDrop(data);
}
});
}
For touch devices and accessibility:
let selectedItem = null;
function handleItemClick(item) {
if (selectedItem === item) {
// Deselect
deselectItem();
} else if (selectedItem) {
// Try to place selected item
if (canPlaceAt(selectedItem, item)) {
placeItem(selectedItem, item);
}
deselectItem();
} else {
// Select this item
selectItem(item);
}
}
function selectItem(item) {
selectedItem = item;
item.classList.add('selected');
highlightValidTargets(item);
}
function deselectItem() {
if (selectedItem) {
selectedItem.classList.remove('selected');
clearHighlights();
selectedItem = null;
}
}
<div class="blueprint">
<div class="blueprint-grid">
<!-- Frame slots -->
<div class="slot frame-slot" data-slot-type="frame" data-slot-id="frame-1">
<div class="slot-label">Frame</div>
<div class="slot-content">
<!-- Upgrade tile goes here when installed -->
</div>
<div class="gas-socket">
<!-- Gas cube indicator -->
</div>
</div>
<!-- Drive slots -->
<div class="slot drive-slot" data-slot-type="drive" data-slot-id="drive-1">
<div class="slot-label">Drive</div>
<div class="slot-content empty"></div>
</div>
<!-- More slots... -->
</div>
<div class="blueprint-stats">
<div class="stat">
<span class="stat-icon">⚖️</span>
<span class="stat-value" data-stat="weight">12</span>
</div>
<div class="stat">
<span class="stat-icon">🎈</span>
<span class="stat-value" data-stat="lift">15</span>
</div>
<!-- More stats -->
</div>
</div>
.blueprint-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
padding: 1rem;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: 8px;
}
.slot {
aspect-ratio: 1;
border: 2px dashed #444;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
.slot.empty:hover {
border-color: #4a9eff;
background: rgba(74, 158, 255, 0.1);
}
.slot.filled {
border-style: solid;
border-color: #666;
}
.slot.drop-target {
border-color: #4aff4a;
background: rgba(74, 255, 74, 0.1);
}
<div class="hand-container">
<div class="hand" id="player-hand">
<!-- Cards fan out -->
</div>
</div>
.hand-container {
perspective: 1000px;
padding: 1rem;
}
.hand {
display: flex;
justify-content: center;
}
.card {
width: 120px;
height: 180px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
margin-left: -40px; /* Overlap */
transition: transform 0.2s, margin 0.2s;
cursor: pointer;
}
.card:first-child {
margin-left: 0;
}
.card:hover {
transform: translateY(-20px) scale(1.05);
margin-left: 0;
margin-right: 40px;
z-index: 10;
}
.card.selected {
transform: translateY(-30px);
box-shadow: 0 0 20px rgba(74, 158, 255, 0.5);
}
function animateTokenMove(tokenElement, fromPos, toPos, duration = 500) {
return new Promise(resolve => {
// Calculate path
const dx = toPos.x - fromPos.x;
const dy = toPos.y - fromPos.y;
// Use CSS animation
tokenElement.style.transition = `transform ${duration}ms ease-out`;
tokenElement.style.transform = `translate(${dx}px, ${dy}px)`;
setTimeout(() => {
// After animation, update actual position
tokenElement.style.transition = '';
tokenElement.style.transform = '';
tokenElement.setAttribute('transform', `translate(${toPos.x}, ${toPos.y})`);
resolve();
}, duration);
});
}
/* Highlight changes */
@keyframes pulse-highlight {
0%, 100% { box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.5); }
50% { box-shadow: 0 0 0 10px rgba(74, 158, 255, 0); }
}
.stat-value.changed {
animation: pulse-highlight 0.5s ease-out;
}
/* Income animation */
@keyframes float-up {
0% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-30px); }
}
.income-popup {
animation: float-up 1s ease-out forwards;
color: #4aff4a;
font-weight: bold;
}
<div class="turn-indicator">
<div class="current-player">
<div class="player-avatar" style="--player-color: var(--player-1-color)"></div>
<span class="player-name">Germany</span>
</div>
<div class="turn-timer" id="turn-timer">
<svg class="timer-ring" viewBox="0 0 36 36">
<circle cx="18" cy="18" r="16" fill="none" stroke="#333" stroke-width="3" />
<circle class="timer-progress" cx="18" cy="18" r="16"
fill="none" stroke="#4a9eff" stroke-width="3"
stroke-dasharray="100" stroke-dashoffset="0" />
</svg>
<span class="timer-text">60</span>
</div>
</div>
function startTurnTimer(duration) {
const timerText = document.querySelector('.timer-text');
const timerProgress = document.querySelector('.timer-progress');
let remaining = duration;
const interval = setInterval(() => {
remaining--;
timerText.textContent = remaining;
const percent = (remaining / duration) * 100;
timerProgress.style.strokeDashoffset = 100 - percent;
if (remaining <= 0) {
clearInterval(interval);
}
}, 1000);
return () => clearInterval(interval);
}
// Tooltip system
const tooltip = document.getElementById('tooltip');
function showTooltip(content, x, y) {
tooltip.innerHTML = content;
tooltip.style.left = `${x + 10}px`;
tooltip.style.top = `${y + 10}px`;
tooltip.classList.add('visible');
}
function hideTooltip() {
tooltip.classList.remove('visible');
}
// Rich tooltips for game elements
function getTechnologyTooltip(techId) {
const tech = technologies[techId];
return `
<div class="tooltip-tech">
<h4>${tech.name}</h4>
<p class="tooltip-cost">Cost: ${tech.cost} Research</p>
<p class="tooltip-desc">${tech.description}</p>
<p class="tooltip-unlocks">Unlocks: ${tech.unlocks}</p>
</div>
`;
}
// Make game elements keyboard accessible
document.querySelectorAll('.slot, .card, .route').forEach(el => {
el.setAttribute('tabindex', '0');
el.setAttribute('role', 'button');
el.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
el.click();
}
});
});
<!-- Announce game state changes -->
<div id="game-announcer" aria-live="polite" class="sr-only"></div>
<script>
function announce(message) {
document.getElementById('game-announcer').textContent = message;
}
// Usage
announce("Germany's turn. They placed a worker at the Construction Hall.");
</script>
Use this skill when:
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience