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 and data-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.

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.