Frontend Interaction Skills Through Solitaire
Learn essential frontend development concepts by examining how the Klondike Solitaire game creates intuitive user interactions - from drag-and-drop mechanics to visual feedback systems.
Frontend Interaction Skills Through Solitaire
Modern web applications succeed based on how well they interact with users. This lesson examines a complete Solitaire game to understand essential frontend development skills: creating intuitive interactions, providing visual feedback, and building interfaces that teach users through experience.
Why Solitaire for Frontend Skills?
Solitaire is an excellent example for learning frontend development because it demonstrates core interaction patterns:
- Drag and Drop - Cards move naturally between piles
- Click Interactions - Multiple ways to interact with the same elements
- Visual Feedback - Users understand rules through visual cues
- State Management - Interface reflects game state changes instantly
- Responsive Design - Works across different screen sizes
Core Frontend Concepts Demonstrated
1. HTML Structure and Semantic Layout
Well-structured HTML forms the foundation of any frontend application. The solitaire game uses semantic structure to create meaning.
Game Layout Structure
<!-- Semantic game container -->
<div id="game_screen" class="game-container wrap" tabindex="1">
<!-- Clear visual hierarchy -->
<div class="game-controls">
<div class="score-display">Score: <span id="score_value">0</span></div>
<div class="timer-display">Time: <span id="timer_value">00:00</span></div>
<div class="game-buttons">
<button id="hint_btn">Hint</button>
<button id="undo_btn">Undo</button>
<button id="restart_btn">Restart</button>
</div>
</div>
<!-- Foundation row - clearly separated -->
<div class="foundation-row">
<div id="stock" class="card-pile stock-pile"></div>
<div id="waste" class="card-pile waste-pile empty"></div>
<div class="card-pile empty"></div> <!-- Visual spacer -->
<!-- Foundation piles with semantic data attributes -->
<div id="foundation_0" class="card-pile foundation" data-pile="foundation" data-index="0"></div>
<!-- ... more foundation piles -->
</div>
<!-- Tableau - main play area -->
<div class="game-board">
<div id="tableau_0" class="card-pile tableau-pile" data-pile="tableau" data-index="0"></div>
<!-- ... more tableau piles -->
</div>
</div>
Key Frontend Principles:
- Semantic structure: Each area has a clear purpose and role
- Data attributes:
data-pile
anddata-index
enable JavaScript interactions - Visual hierarchy: Controls, foundation, and tableau are clearly separated
- Accessibility:
tabindex
enables keyboard navigation - Meaningful IDs: Each element can be targeted specifically
2. CSS for Visual Design and User Experience
CSS transforms the HTML structure into an intuitive, visually appealing interface.
Card Visual Design
.card {
width: 76px;
height: 106px;
border: 1px solid #000;
border-radius: 6px;
background: #fff;
position: absolute;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 4px;
font-size: 12px;
font-weight: bold;
user-select: none; /* Prevents text selection during drag */
}
/* Visual feedback for different states */
.card.red { color: #d00; }
.card.black { color: #000; }
.card.face-down {
background: #004d9f;
background-image: repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
rgba(255,255,255,.1) 10px,
rgba(255,255,255,.1) 20px
);
}
/* Interactive feedback */
.card.dragging {
z-index: 1000;
transform: rotate(5deg); /* Visual cue that card is being moved */
}
.card.highlighted {
box-shadow: 0 0 10px #ffff00; /* Clear visual selection */
}
Key Frontend Principles:
- Visual hierarchy: Different elements have distinct visual treatments
- State representation: Visual appearance reflects current state
- Interactive feedback: Users get immediate visual response to actions
- Consistent styling: All similar elements follow the same visual patterns
Responsive Game Board Layout
.game-board {
display: grid;
grid-template-columns: repeat(7, 1fr); /* Equal columns for tableau */
gap: 10px;
margin-top: 20px;
}
.foundation-row {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
margin-bottom: 20px;
}
.tableau-pile {
min-height: 300px; /* Ensures space for card stacking */
}
Key Frontend Principles:
- CSS Grid: Modern layout system for consistent spacing
- Responsive design: Columns adapt to container width
- Flexible spacing: Grid gap creates consistent visual rhythm
3. JavaScript Event Handling and Interactions
JavaScript brings the interface to life by handling user interactions and updating the display.
Drag and Drop Implementation
_createCardElement(card) {
const el = document.createElement('div');
el.className = `card ${card.color} ${card.faceUp ? '' : 'face-down'}`;
el.id = `card_${card.id}`;
el.setAttribute('data-card-id', card.id);
el.draggable = card.faceUp; // Only face-up cards can be dragged
// Drag start - user begins dragging
el.addEventListener('dragstart', (e) => {
this.draggedCardId = card.id;
e.dataTransfer.effectAllowed = 'move';
el.classList.add('dragging'); // Visual feedback
});
// Drag end - user stops dragging
el.addEventListener('dragend', () => {
el.classList.remove('dragging');
this.draggedCardId = null;
});
// Click interaction - alternative to drag/drop
el.addEventListener('click', () => {
controller.handleCardClick(card.id);
});
return el;
}
Key Frontend Principles:
- Event-driven programming: Interface responds to user actions
- Multiple interaction methods: Drag/drop AND click for accessibility
- Visual state management: CSS classes reflect current interaction state
- Data flow: Events trigger controller methods that update game state
Drop Zone Implementation
_attachDropHandlers(host, kind, index, game) {
// Allow dropping
host.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
// Handle drop
host.addEventListener('drop', (e) => {
e.preventDefault();
if (!this.draggedCardId) return;
controller.handleDrop(this.draggedCardId, kind, index);
});
}
Key Frontend Principles:
- Default behavior prevention:
preventDefault()
enables custom drop behavior - Visual feedback:
dropEffect
shows users what will happen - Data validation: Checks ensure valid drag/drop state
- Separation of concerns: UI handles events, controller handles logic
4. Dynamic DOM Manipulation
The interface updates in real-time as the game state changes, demonstrating essential DOM manipulation skills.
Real-time Pile Rendering
renderPiles(game) {
// Clear existing display
document.querySelectorAll('.card-pile').forEach(p => p.innerHTML = '');
// STOCK - shows only top card or back pattern
this._renderPileTop(this.dom.stock, game.stock.top());
this._attachStockHandlers(this.dom.stock, game);
// WASTE - shows top card, updates empty state
if (game.waste.isEmpty)
this.dom.waste.classList.add('empty');
else
this.dom.waste.classList.remove('empty');
this._renderPileTop(this.dom.waste, game.waste.top());
// TABLEAU - complex stacking display
game.tableau.forEach((t, i) => {
const host = this.dom.tableau[i];
t.cards.forEach((card, idx) => {
const el = this._createCardElement(card);
el.style.top = `${idx * 20}px`; // Visual stacking
el.style.zIndex = idx; // Proper layering
host.appendChild(el);
});
this._attachDropHandlers(host, 'tableau', i, game);
});
}
Key Frontend Principles:
- State synchronization: Display always reflects current game state
- Efficient updates: Clear and rebuild for consistent state
- Visual stacking: CSS positioning creates realistic card stacking
- Dynamic styling: CSS properties set via JavaScript for positioning
Real-time Score and Timer Updates
// Score updates immediately when points are earned
updateScore(s) {
this.eScore.textContent = s;
}
// Timer updates every second
startTimer() {
this.timer.start = Date.now();
this.timer.intervalId = setInterval(() => {
const elapsed = Math.floor((Date.now() - this.timer.start) / 1000);
const mm = String(Math.floor(elapsed / 60)).padStart(2, '0');
const ss = String(elapsed % 60).padStart(2, '0');
this.ui.updateTime(`${mm}:${ss}`);
}, 1000);
}
Key Frontend Principles:
- Real-time updates: Interface reflects changes immediately
- Formatted display: Data is formatted for user-friendly presentation
- Resource management: Timers are properly started and stopped
5. Visual Feedback and User Communication
Effective frontend applications communicate with users through visual cues and feedback.
Modal and Overlay Systems
showWin(score, timeStr) {
this.winScore.textContent = `Score: ${score}`;
this.winTime.textContent = `Time: ${timeStr}`;
this.winBox.style.display = 'block'; // Show win modal
}
hideWin() {
this.winBox.style.display = 'none'; // Hide win modal
}
.win-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* Perfect centering */
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 30px;
border-radius: 10px;
text-align: center;
font-size: 24px;
z-index: 2000; /* Appears above all game elements */
display: none;
}
Key Frontend Principles:
- Modal patterns: Overlays focus user attention on important information
- Perfect centering: CSS transform technique for responsive centering
- Z-index management: Proper layering ensures modals appear above content
- Contextual information: Displays relevant game statistics
Hover and Interactive States
.game-buttons button:hover {
background: #45a049; /* Visual feedback on hover */
}
.card-pile.empty {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
}
.card-pile.foundation {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
}
Key Frontend Principles:
- Hover feedback: Users understand interactive elements
- Empty state design: Clear visual indication of empty areas
- Purposeful differentiation: Different pile types have distinct appearances
6. Screen State Management
Modern frontend applications manage multiple interface states smoothly.
Multi-Screen Navigation
// Clean screen transitions
showMenu() {
this.menu.style.display = 'block';
this.game.style.display = 'none';
this.over.style.display = 'none';
}
showGame() {
this.menu.style.display = 'none';
this.game.style.display = 'block';
this.over.style.display = 'none';
this.game.focus(); // Enable keyboard interactions
}
showOver(finalScore, timeStr) {
document.getElementById('final_score').textContent = `Final Score: ${finalScore}`;
document.getElementById('final_time').textContent = `Time: ${timeStr}`;
this.menu.style.display = 'none';
this.game.style.display = 'block';
this.over.style.display = 'block'; // Overlay on game screen
}
Key Frontend Principles:
- Single responsibility: Each method handles one screen state
- Complete state management: All elements properly shown/hidden
- Focus management: Keyboard accessibility maintained
- Data binding: Screen content updates with current game data
Advanced Frontend Patterns
Event Delegation and Bubbling
// Efficient event handling for dynamically created cards
document.addEventListener('click', (e) => {
if (e.target.classList.contains('card')) {
const cardId = e.target.getAttribute('data-card-id');
controller.handleCardClick(cardId);
}
});
Keyboard Accessibility
// Keyboard shortcuts enhance usability
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' && ui.menu.style.display !== 'none') {
controller.startNewGame();
}
// Could add: Arrow keys for card selection, Enter for moves, etc.
});
CSS Custom Properties for Theming
:root {
--card-width: 76px;
--card-height: 106px;
--pile-gap: 10px;
--background-green: #0f7b0f;
}
.card {
width: var(--card-width);
height: var(--card-height);
}
Learning Exercises
Exercise 1: Enhanced Visual Feedback
Add visual feedback for valid drop targets:
_attachDropHandlers(host, kind, index, game) {
host.addEventListener('dragover', (e) => {
e.preventDefault();
// Add visual feedback for valid drops
if (this._canAcceptCard(host, this.draggedCardId)) {
host.classList.add('valid-drop-target');
}
});
host.addEventListener('dragleave', () => {
host.classList.remove('valid-drop-target');
});
}
.valid-drop-target {
background: rgba(0, 255, 0, 0.2);
border-color: #00ff00;
}
Exercise 2: Card Animation
Add smooth animations for card movements:
.card {
transition: all 0.3s ease-in-out;
}
.card.moving {
transform: scale(1.1);
}
_moveCardWithAnimation(cardElement, fromPile, toPile) {
cardElement.classList.add('moving');
setTimeout(() => {
// Move card to new position
toPile.appendChild(cardElement);
cardElement.classList.remove('moving');
}, 300);
}
Exercise 3: Responsive Design
Make the game work on mobile devices:
@media (max-width: 768px) {
.game-board {
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(2, 1fr);
}
.card {
width: 60px;
height: 84px;
font-size: 10px;
}
}
Exercise 4: Progressive Enhancement
Add touch support for mobile devices:
// Touch events for mobile compatibility
el.addEventListener('touchstart', (e) => {
this.touchStartPos = {
x: e.touches[0].clientX,
y: e.touches[0].clientY
};
});
el.addEventListener('touchmove', (e) => {
e.preventDefault(); // Prevent scrolling
// Update card position to follow finger
});
el.addEventListener('touchend', (e) => {
// Determine drop target based on final position
const dropTarget = this._getDropTargetFromPosition(e.changedTouches[0]);
if (dropTarget) {
controller.handleDrop(card.id, dropTarget.kind, dropTarget.index);
}
});
Frontend Best Practices Demonstrated
1. Separation of Concerns
// UI handles display logic
class UI {
renderPiles(game) { /* display logic */ }
}
// Controller handles user interactions
class Controller {
handleCardClick(cardId) { /* interaction logic */ }
}
// Game handles business logic
class Game {
tryMoveCardById(cardId, target) { /* game logic */ }
}
2. Progressive Enhancement
- Base functionality works with basic HTML/CSS
- JavaScript adds enhanced interactions
- Touch events add mobile support
- Keyboard shortcuts add power-user features
3. Accessibility Considerations
- Semantic HTML structure
- Keyboard navigation support
- Visual focus indicators
- Screen reader friendly content
4. Performance Optimization
- Efficient DOM updates (clear and rebuild)
- CSS animations over JavaScript animations
- Event delegation for dynamic content
- Minimal DOM queries
Common Frontend Mistakes to Avoid
1. Direct DOM Manipulation
// BAD: Directly manipulating styles everywhere
document.getElementById('card1').style.left = '100px';
document.getElementById('card1').style.top = '50px';
// GOOD: Use CSS classes for state changes
cardElement.classList.add('positioned');
2. Lack of User Feedback
// BAD: Silent failures
if (!this.game.tryMove(card, target)) {
// Nothing happens - user is confused
}
// GOOD: Clear feedback
if (!this.game.tryMove(card, target)) {
this.showMessage("Invalid move - cards must alternate colors");
}
3. Poor Event Handling
// BAD: Memory leaks from unremoved listeners
cards.forEach(card => {
card.addEventListener('click', handler); // Never removed
});
// GOOD: Clean event management
this.eventListeners.push({element: card, event: 'click', handler});
// Later: remove all listeners when cleaning up
Conclusion
The Solitaire game demonstrates that effective frontend development goes far beyond making things “look pretty.” It’s about creating interfaces that:
- Communicate clearly with users through visual design
- Respond intuitively to user interactions
- Provide immediate feedback for all actions
- Work consistently across different devices and contexts
- Remain accessible to all users
Key frontend skills demonstrated:
- HTML structure creates semantic meaning and accessibility
- CSS design provides visual hierarchy and user experience
- JavaScript interactions bring interfaces to life
- Event handling enables complex user interactions
- State management keeps interface synchronized with data
- Responsive design works across all device types
Whether building games, business applications, or creative websites, these frontend principles will help you create interfaces that users find intuitive, engaging, and effective. The best frontend development makes complex interactions feel simple and natural - just like a well-designed card game.