Skip to main content

Course Progress

Loading...

Adding Interactivity with JavaScript

Duration: 60 minutes
Module 1: Mini-Project

Learning Objectives

  • Master the concepts in this lesson
  • Apply knowledge through practice
  • Build practical skills
  • Prepare for next topics

Understanding JavaScript's Role in Web Interactivity

Welcome to Session 10 of our Web Development Fundamentals module! Today, we'll explore how JavaScript transforms static webpages into dynamic, interactive experiences. By the end of this session, you'll understand how to breathe life into your websites by implementing JavaScript-powered features that users can interact with directly.

"HTML provides the structure, CSS adds style, but JavaScript brings your website to life."

The Three Pillars of Web Development

The Three Pillars of Web Development Web Page HTML: Structure CSS: Presentation JavaScript: Behavior Defines Content and Organization Controls Appearance and Layout Enables Interactivity and Dynamic Content

What Makes a Website Interactive?

An interactive website responds to user actions, updates content dynamically, and creates a two-way communication between the user and the interface. Let's examine the key concepts behind interactivity:

Event-Driven Programming

JavaScript uses an event-driven programming model where code executes in response to user actions (events) like clicks, keyboard input, or form submissions. Think of events as triggers that set off specific reactions on your webpage.

Analogy: The Doorbell Effect

Imagine your website as a house. Events are like doorbells or phones ringing - they signal that something requires attention. The JavaScript event listeners are like people in the house waiting to respond to these signals. When the doorbell rings (an event occurs), the designated person (the event handler) goes to answer it and takes appropriate action.

Example: Click Event Listener

// Select the button element
const myButton = document.getElementById('myButton');

// Add an event listener to respond to clicks
myButton.addEventListener('click', function() {
    alert('Button was clicked!');
});

DOM Manipulation

The Document Object Model (DOM) is a programming interface that represents the structure of HTML and XML documents. JavaScript can modify this structure in real-time, allowing developers to change content, attributes, and styles without requiring a page reload.

Analogy: The Living Blueprint

Think of the DOM as a living blueprint of a building. While the HTML file is like the original architect's plan, the DOM is a 3D model that can be modified on the fly. JavaScript is like a contractor who can add rooms, change colors, or move furniture without needing to redraw the entire blueprint.

DOM Manipulation Process HTML Document (Static File) DOM Tree (In Memory) Updated DOM (Modified) Visual Changes (User Sees) Parsed by Browser JavaScript Manipulation Rendered by Browser

Example: Changing Content Dynamically

// Select the element to modify
const messageElement = document.getElementById('message');

// Change its text content
messageElement.textContent = 'This text was updated with JavaScript!';

// Change its appearance
messageElement.style.color = 'blue';
messageElement.style.fontWeight = 'bold';

User Input Processing

JavaScript can capture, validate, and process user input, enabling functionalities like form submission, real-time validation, and data filtering.

Analogy: The Helpful Assistant

Imagine JavaScript as a helpful assistant that sits between the user and your server. This assistant can check if forms are filled out correctly, suggest corrections, and organize information before it's sent - much like how a receptionist might review paperwork before passing it to the appropriate department.

Example: Form Validation

// Select the form and input elements
const form = document.getElementById('myForm');
const emailInput = document.getElementById('email');
const errorMessage = document.getElementById('error');

// Add validation on form submission
form.addEventListener('submit', function(event) {
    // Check if email follows a valid pattern
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    
    if (!emailPattern.test(emailInput.value)) {
        // Prevent form submission
        event.preventDefault();
        
        // Show error message
        errorMessage.textContent = 'Please enter a valid email address';
        errorMessage.style.display = 'block';
        
        // Highlight the input field
        emailInput.style.borderColor = 'red';
    }
});

Common Interactive Elements on Websites

Accordions and Collapsible Content

Accordions allow users to expand and collapse sections of content, saving screen space and helping users focus on specific information.

Implementation Example

// Select all accordion headers
const accordionHeaders = document.querySelectorAll('.accordion-header');

// Add click event to each header
accordionHeaders.forEach(header => {
    header.addEventListener('click', function() {
        // Toggle the 'active' class on the header
        this.classList.toggle('active');
        
        // Get the associated content panel
        const panel = this.nextElementSibling;
        
        // Toggle the panel's visibility
        if (panel.style.maxHeight) {
            panel.style.maxHeight = null;
        } else {
            panel.style.maxHeight = panel.scrollHeight + "px";
        }
    });
});

Real-World Application

FAQ pages commonly use accordions to organize many questions and answers in a compact, user-friendly format. WordPress websites often incorporate accordion components for content organization in sidebars or product descriptions.

Image Sliders and Carousels

Sliders and carousels allow users to browse through multiple images or content sections within a limited space, often used for showcasing featured content or products.

Slider Interaction Sequence User JavaScript DOM Clicks "Next" button Hide current slide Update current slide index Show new slide Update active indicator Visual update complete

Simple Slider Implementation

let currentSlide = 0;
const slides = document.querySelectorAll('.slide');
const totalSlides = slides.length;

// Function to show a specific slide
function showSlide(index) {
    // Hide all slides
    slides.forEach(slide => {
        slide.style.display = 'none';
    });
    
    // Show the selected slide
    slides[index].style.display = 'block';
}

// Function to navigate to the next slide
function nextSlide() {
    currentSlide = (currentSlide + 1) % totalSlides;
    showSlide(currentSlide);
}

// Function to navigate to the previous slide
function prevSlide() {
    currentSlide = (currentSlide - 1 + totalSlides) % totalSlides;
    showSlide(currentSlide);
}

// Set up event listeners for navigation buttons
document.getElementById('next').addEventListener('click', nextSlide);
document.getElementById('prev').addEventListener('click', prevSlide);

// Initialize the slider
showSlide(currentSlide);

Real-World Application

E-commerce websites frequently use image sliders to showcase multiple product views. News websites employ carousels to feature multiple headlines in a prominent location. In WordPress, sliders are often implemented on home pages to feature key content or testimonials.

Modal Windows and Lightboxes

Modals display content in a focused overlay that sits on top of the main page, requiring user interaction before returning to the main content.

Modal Window Title × Modal content goes here OK

Modal Window Implementation

// Select elements
const modal = document.getElementById('myModal');
const openModalBtn = document.getElementById('openModal');
const closeModalBtn = document.getElementById('closeModal');
const modalOverlay = document.getElementById('modalOverlay');

// Function to open the modal
function openModal() {
    modal.style.display = 'block';
    modalOverlay.style.display = 'block';
    document.body.style.overflow = 'hidden'; // Prevent scrolling
}

// Function to close the modal
function closeModal() {
    modal.style.display = 'none';
    modalOverlay.style.display = 'none';
    document.body.style.overflow = 'auto'; // Re-enable scrolling
}

// Event listeners
openModalBtn.addEventListener('click', openModal);
closeModalBtn.addEventListener('click', closeModal);

// Close modal when clicking the overlay
modalOverlay.addEventListener('click', closeModal);

// Prevent closing when clicking inside the modal itself
modal.addEventListener('click', function(event) {
    event.stopPropagation();
});

Real-World Application

Login/signup forms often appear in modal windows to keep users on the same page. Image galleries use lightboxes to show enlarged images without navigating away. WordPress sites frequently use modals for newsletter signups, contact forms, or displaying additional product information.

Practical Implementation Examples

Creating a Tabbed Content Interface

Tabbed interfaces allow users to navigate between different content sections without leaving the page, improving user experience by organizing related content in a compact space.

Tab Container Structure Tab Container Tab Navigation Tab Content Panels Tab 1 Tab 2 Tab 3 Content Panel 1 Content Panel 2 Content Panel 3 click → Sets active click → Sets active click → Sets active

HTML Structure

<div class="tab-container">
    <!-- Tab Navigation -->
    <div class="tab-nav">
        <button class="tab-button active" data-tab="tab1">Tab 1</button>
        <button class="tab-button" data-tab="tab2">Tab 2</button>
        <button class="tab-button" data-tab="tab3">Tab 3</button>
    </div>
    
    <!-- Tab Content -->
    <div class="tab-content">
        <div id="tab1" class="tab-panel active">
            <h3>Tab 1 Content</h3>
            <p>This is the content for the first tab.</p>
        </div>
        
        <div id="tab2" class="tab-panel">
            <h3>Tab 2 Content</h3>
            <p>This is the content for the second tab.</p>
        </div>
        
        <div id="tab3" class="tab-panel">
            <h3>Tab 3 Content</h3>
            <p>This is the content for the third tab.</p>
        </div>
    </div>
</div>

CSS Styling

/* Tab Container */
.tab-container {
    max-width: 600px;
    margin: 0 auto;
}

/* Tab Navigation */
.tab-nav {
    display: flex;
    border-bottom: 1px solid #ccc;
}

.tab-button {
    padding: 10px 15px;
    background: #f1f1f1;
    border: none;
    cursor: pointer;
    border-radius: 5px 5px 0 0;
    margin-right: 5px;
}

.tab-button.active {
    background: #fff;
    border: 1px solid #ccc;
    border-bottom: 1px solid #fff;
    margin-bottom: -1px;
}

/* Tab Content */
.tab-panel {
    display: none;
    padding: 20px;
    border: 1px solid #ccc;
    border-top: none;
}

.tab-panel.active {
    display: block;
}

JavaScript Functionality

// Select all tab buttons and panels
const tabButtons = document.querySelectorAll('.tab-button');
const tabPanels = document.querySelectorAll('.tab-panel');

// Add click event to each tab button
tabButtons.forEach(button => {
    button.addEventListener('click', function() {
        // Remove active class from all buttons and panels
        tabButtons.forEach(btn => btn.classList.remove('active'));
        tabPanels.forEach(panel => panel.classList.remove('active'));
        
        // Add active class to the clicked button
        this.classList.add('active');
        
        // Get the tab id from the data attribute
        const tabId = this.getAttribute('data-tab');
        
        // Activate the corresponding panel
        document.getElementById(tabId).classList.add('active');
    });
});

WordPress Application

In WordPress, tabbed interfaces are commonly used for:

  • Product information tabs in WooCommerce shops (Description, Additional Information, Reviews)
  • Dashboard widgets displaying different categories of information
  • Settings pages where options are organized into logical groups
  • Custom post type displays where content needs to be categorized

Building a Simple Form with Validation

Let's implement a contact form with real-time validation to enhance user experience and ensure data quality.

HTML Structure

<form id="contactForm" class="contact-form">
    <div class="form-group">
        <label for="name">Name:</label>
        <input type="text" id="name" name="name" required>
        <span class="error-message" id="nameError"></span>
    </div>
    
    <div class="form-group">
        <label for="email">Email:</label>
        <input type="email" id="email" name="email" required>
        <span class="error-message" id="emailError"></span>
    </div>
    
    <div class="form-group">
        <label for="message">Message:</label>
        <textarea id="message" name="message" rows="5" required></textarea>
        <span class="error-message" id="messageError"></span>
    </div>
    
    <div class="form-group">
        <button type="submit" id="submitButton">Submit</button>
    </div>
    
    <div id="formSuccess" class="success-message" style="display: none;">
        Thank you for your message! We'll respond shortly.
    </div>
</form>

JavaScript Validation

// Select form and inputs
const contactForm = document.getElementById('contactForm');
const nameInput = document.getElementById('name');
const emailInput = document.getElementById('email');
const messageInput = document.getElementById('message');

// Select error message elements
const nameError = document.getElementById('nameError');
const emailError = document.getElementById('emailError');
const messageError = document.getElementById('messageError');

// Success message element
const formSuccess = document.getElementById('formSuccess');

// Validation function for name
function validateName() {
    if (nameInput.value.trim() === '') {
        nameError.textContent = 'Name is required';
        nameInput.classList.add('invalid');
        return false;
    } else if (nameInput.value.trim().length < 2) {
        nameError.textContent = 'Name must be at least 2 characters';
        nameInput.classList.add('invalid');
        return false;
    } else {
        nameError.textContent = '';
        nameInput.classList.remove('invalid');
        return true;
    }
}

// Validation function for email
function validateEmail() {
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    
    if (emailInput.value.trim() === '') {
        emailError.textContent = 'Email is required';
        emailInput.classList.add('invalid');
        return false;
    } else if (!emailPattern.test(emailInput.value)) {
        emailError.textContent = 'Please enter a valid email address';
        emailInput.classList.add('invalid');
        return false;
    } else {
        emailError.textContent = '';
        emailInput.classList.remove('invalid');
        return true;
    }
}

// Validation function for message
function validateMessage() {
    if (messageInput.value.trim() === '') {
        messageError.textContent = 'Message is required';
        messageInput.classList.add('invalid');
        return false;
    } else if (messageInput.value.trim().length < 10) {
        messageError.textContent = 'Message must be at least 10 characters';
        messageInput.classList.add('invalid');
        return false;
    } else {
        messageError.textContent = '';
        messageInput.classList.remove('invalid');
        return true;
    }
}

// Add input event listeners for real-time validation
nameInput.addEventListener('input', validateName);
emailInput.addEventListener('input', validateEmail);
messageInput.addEventListener('input', validateMessage);

// Form submission handler
contactForm.addEventListener('submit', function(event) {
    // Prevent default form submission
    event.preventDefault();
    
    // Validate all fields
    const isNameValid = validateName();
    const isEmailValid = validateEmail();
    const isMessageValid = validateMessage();
    
    // If all validations pass
    if (isNameValid && isEmailValid && isMessageValid) {
        // In a real application, you would send the data to a server here
        
        // For demonstration, we'll just show the success message
        formSuccess.style.display = 'block';
        contactForm.reset();
        
        // Hide success message after 5 seconds
        setTimeout(function() {
            formSuccess.style.display = 'none';
        }, 5000);
    }
});

WordPress Integration

In WordPress development, this JavaScript validation can be integrated with:

  • Contact Form 7 or Gravity Forms for enhanced user feedback
  • WooCommerce checkout process to validate customer information
  • Custom user registration forms with real-time username availability checking
  • Comment forms with live character counting and validation

Best Practices for JavaScript Interactivity

Progressive Enhancement

Build your websites so they work without JavaScript first, then enhance them with JavaScript functionality. This ensures baseline usability for all users, regardless of their browser capabilities or if JavaScript is disabled.

Analogy: The Cake and Frosting

Think of your website as a cake. HTML is the cake itself - it should be complete and functional on its own. CSS is like the basic frosting that makes it look good. JavaScript is like the elaborate decorations, sprinkles, and candles - they enhance the experience but aren't necessary for the cake to be edible and enjoyable.

Event Delegation

Instead of attaching event listeners to multiple elements, attach a single listener to a parent element and use event bubbling to handle events for all child elements. This improves performance and handles dynamically added elements automatically.

Example: Event Delegation

// Without event delegation (inefficient for many buttons)
// document.querySelectorAll('.button').forEach(button => {
//     button.addEventListener('click', handleClick);
// });

// With event delegation (more efficient)
document.getElementById('buttonContainer').addEventListener('click', function(event) {
    // Check if the clicked element is a button
    if (event.target.classList.contains('button')) {
        // Handle the button click
        handleClick(event);
    }
});

Accessibility Considerations

Ensure your interactive elements are accessible to all users, including those using assistive technologies like screen readers or keyboard navigation.

Key Accessibility Tips:

  • Use semantic HTML elements for interactive components when possible
  • Add proper ARIA attributes for custom interactive elements
  • Ensure keyboard navigability (focus states, tabindex)
  • Provide visual feedback for interactions
  • Test with screen readers and keyboard-only navigation

Example: Accessible Toggle Button

<!-- HTML -->
<button id="toggleButton" 
        aria-pressed="false" 
        aria-label="Toggle dark mode">
    Dark Mode
</button>

// JavaScript
const toggleButton = document.getElementById('toggleButton');

toggleButton.addEventListener('click', function() {
    // Get the current state
    const isPressed = this.getAttribute('aria-pressed') === 'true';
    
    // Toggle the state
    this.setAttribute('aria-pressed', !isPressed);
    
    // Update the text
    this.textContent = isPressed ? 'Dark Mode' : 'Light Mode';
    
    // Toggle the dark mode class on the body
    document.body.classList.toggle('dark-mode');
});

Performance Optimization

Optimize your JavaScript code to ensure smooth interactions and prevent unnecessary slowdowns.

Performance Tips:

  • Minimize DOM manipulations by batching changes
  • Use requestAnimationFrame for animations
  • Debounce or throttle event handlers for scroll or resize events
  • Avoid excessive jQuery chaining or DOM traversals
  • Optimize selectors (getElementById is faster than querySelector)

Example: Debouncing a Search Input

// Function to search and update results
function searchProducts(query) {
    console.log('Searching for:', query);
    // Actual search implementation here
}

// Debounce function to prevent excessive API calls
function debounce(func, delay) {
    let timeout;
    
    return function() {
        const context = this;
        const args = arguments;
        
        clearTimeout(timeout);
        
        timeout = setTimeout(function() {
            func.apply(context, args);
        }, delay);
    };
}

// Get the search input
const searchInput = document.getElementById('searchInput');

// Create debounced version of search function (only runs after typing stops)
const debouncedSearch = debounce(function(event) {
    searchProducts(event.target.value);
}, 300);

// Add event listener with debounced function
searchInput.addEventListener('input', debouncedSearch);

Case Study: Building a Dynamic Content Filter

Let's integrate everything we've learned into a practical example: a content filter that allows users to sort and filter a collection of items in real-time without page reloads.

Use Case: Product Filtering

Imagine a WordPress site with a products page where users need to filter items by category, price range, and sort by different criteria.

HTML Structure

<div class="product-filter-container">
    <div class="filter-controls">
        <!-- Category Filter -->
        <div class="filter-group">
            <label for="categoryFilter">Category:</label>
            <select id="categoryFilter">
                <option value="all">All Categories</option>
                <option value="electronics">Electronics</option>
                <option value="clothing">Clothing</option>
                <option value="home">Home & Kitchen</option>
            </select>
        </div>
        
        <!-- Price Range Filter -->
        <div class="filter-group">
            <label for="priceFilter">Max Price: $<span id="priceValue">100</span></label>
            <input type="range" id="priceFilter" min="0" max="200" value="100" step="10">
        </div>
        
        <!-- Sort Options -->
        <div class="filter-group">
            <label for="sortOption">Sort By:</label>
            <select id="sortOption">
                <option value="name-asc">Name (A-Z)</option>
                <option value="name-desc">Name (Z-A)</option>
                <option value="price-asc">Price (Low to High)</option>
                <option value="price-desc">Price (High to Low)</option>
            </select>
        </div>
    </div>
    
    <!-- Results Count -->
    <div class="results-info">
        Showing <span id="productCount">0</span> products
    </div>
    
    <!-- Product Grid -->
    <div id="productGrid" class="product-grid">
        <!-- Products will be inserted here dynamically -->
    </div>
</div>

JavaScript Implementation

// Sample product data (in a real app, this might come from an API or WordPress)
const products = [
    { id: 1, name: 'Laptop', category: 'electronics', price: 120, image: 'laptop.jpg' },
    { id: 2, name: 'T-shirt', category: 'clothing', price: 25, image: 'tshirt.jpg' },
    { id: 3, name: 'Coffee Maker', category: 'home', price: 80, image: 'coffee-maker.jpg' },
    { id: 4, name: 'Smartphone', category: 'electronics', price: 150, image: 'smartphone.jpg' },
    { id: 5, name: 'Jeans', category: 'clothing', price: 45, image: 'jeans.jpg' },
    { id: 6, name: 'Blender', category: 'home', price: 70, image: 'blender.jpg' },
    { id: 7, name: 'Headphones', category: 'electronics', price: 35, image: 'headphones.jpg' },
    { id: 8, name: 'Dress', category: 'clothing', price: 60, image: 'dress.jpg' },
    { id: 9, name: 'Toaster', category: 'home', price: 30, image: 'toaster.jpg' }
];

// Select filter elements
const categoryFilter = document.getElementById('categoryFilter');
const priceFilter = document.getElementById('priceFilter');
const priceValue = document.getElementById('priceValue');
const sortOption = document.getElementById('sortOption');
const productCount = document.getElementById('productCount');
const productGrid = document.getElementById('productGrid');

// Update price value display when slider changes
priceFilter.addEventListener('input', function() {
    priceValue.textContent = this.value;
    filterAndSortProducts();
});

// Add event listeners to filters
categoryFilter.addEventListener('change', filterAndSortProducts);
sortOption.addEventListener('change', filterAndSortProducts);

// Filter and sort products based on current selections
function filterAndSortProducts() {
    const selectedCategory = categoryFilter.value;
    const maxPrice = parseInt(priceFilter.value);
    const sortBy = sortOption.value;
    
    // Filter products
    let filteredProducts = products.filter(product => {
        // Check if product meets the category filter
        const categoryMatch = selectedCategory === 'all' || product.category === selectedCategory;
        
        // Check if product meets the price filter
        const priceMatch = product.price <= maxPrice;
        
        // Return true if both filters match
        return categoryMatch && priceMatch;
    });
    
    // Sort filtered products
    filteredProducts.sort((a, b) => {
        if (sortBy === 'name-asc') {
            return a.name.localeCompare(b.name);
        } else if (sortBy === 'name-desc') {
            return b.name.localeCompare(a.name);
        } else if (sortBy === 'price-asc') {
            return a.price - b.price;
        } else if (sortBy === 'price-desc') {
            return b.price - a.price;
        }
    });
    
    // Update the product count
    productCount.textContent = filteredProducts.length;
    
    // Display filtered and sorted products
    displayProducts(filteredProducts);
}

// Function to display products in the grid
function displayProducts(products) {
    // Clear the product grid
    productGrid.innerHTML = '';
    
    // Add each product to the grid
    products.forEach(product => {
        const productCard = document.createElement('div');
        productCard.className = 'product-card';
        productCard.setAttribute('data-category', product.category);
        
        productCard.innerHTML = `
            <div class="product-image">
                <img src="images/${product.image}" alt="${product.name}"/>
            </div>
            <div class="product-details">
                <h3>${product.name}</h3>
                <p class="product-category">${product.category}</p>
                <p class="product-price">${product.price}</p>
                <button class="view-button">View Details</button>
            </div>
        `;
        
        productGrid.appendChild(productCard);
    });
}

// Initialize the product display
filterAndSortProducts();

Integrating with WordPress

// WordPress Integration (PHP + JavaScript)

// In your WordPress theme's functions.php:
function enqueue_product_filter_scripts() {
    wp_enqueue_script(
        'product-filter', 
        get_template_directory_uri() . '/js/product-filter.js', 
        array('jquery'), 
        '1.0', 
        true
    );
    
    // Pass WordPress data to JavaScript
    $products_query = new WP_Query(array(
        'post_type' => 'product',
        'posts_per_page' => -1
    ));
    
    $products = array();
    
    if ($products_query->have_posts()) {
        while ($products_query->have_posts()) {
            $products_query->the_post();
            
            // Get product data
            $product_id = get_the_ID();
            $product_terms = get_the_terms($product_id, 'product_category');
            $category = !empty($product_terms) ? $product_terms[0]->slug : 'uncategorized';
            
            $products[] = array(
                'id' => $product_id,
                'name' => get_the_title(),
                'category' => $category,
                'price' => get_post_meta($product_id, '_product_price', true),
                'image' => get_the_post_thumbnail_url($product_id, 'thumbnail')
            );
        }
        wp_reset_postdata();
    }
    
    // Localize the script with product data
    wp_localize_script(
        'product-filter',
        'productFilterData',
        array(
            'products' => $products,
            'ajaxUrl' => admin_url('admin-ajax.php')
        )
    );
}
add_action('wp_enqueue_scripts', 'enqueue_product_filter_scripts');

// In your product-filter.js file:
jQuery(document).ready(function($) {
    // Get products data from WordPress
    const products = productFilterData.products;
    
    // Rest of the JavaScript code from above, but modified to use
    // the products data from WordPress instead of the hardcoded array
});

Further Topics to Explore

AJAX and Asynchronous Content Loading

Learn how to fetch and update content without refreshing the entire page by using AJAX (Asynchronous JavaScript and XML) calls to your server or APIs.

JavaScript Animation Libraries

Explore libraries like GSAP, Anime.js, or CSS-based animation frameworks to create smooth, performance-optimized animations that enhance user experience.

State Management

Learn about managing application state effectively in more complex interactive interfaces, which will be essential for larger JavaScript applications.

WordPress REST API

Discover how to build interactive frontend experiences that communicate with WordPress through its built-in REST API.

Workshop Exercises

Exercise 1: Interactive Navigation Menu

Create a responsive navigation menu with dropdown functionality for desktop and a mobile hamburger menu that toggles visibility.

Exercise 2: Form Validation Enhancement

Add real-time validation to the contact form you created in the mini-project, with appropriate visual feedback and error messages.

Exercise 3: Image Gallery with Lightbox

Implement a simple image gallery with thumbnails that open larger versions in a lightbox overlay when clicked.

Additional Resources