Complete beginners who know what a browser and a text editor are.
2 hours (guided, hands-on).
By the end of this lesson, the student will have built a simple responsive image gallery laid out with CSS Grid and a tiny bit of JavaScript to open an image in a lightbox/modal.
- 0–10 min — Setup: Create project folder and three files: index.html, style.css, script.js.
- 10–30 min — Basic HTML skeleton: Write the HTML structure, header, and container for the gallery.
- 30–60 min — CSS basics + grid layout: Add reset, base typography, and a responsive grid for the gallery.
- 60–90 min — Add images & captions: Add sample images and semantic markup (figures/figcaptions).
- 90–110 min — Simple JavaScript lightbox: Add click handler so clicking a thumbnail opens a larger view.
- 110–120 min — Polish & next steps: Accessibility tweaks, spacing adjustments, small UX improvements, and homework ideas.
index.html— Markup for the page structure.style.css— Styling for layout and visuals.script.js— Tiny behavior for the lightbox interaction.
Create a folder called image-gallery. Inside it, create three empty files:
image-gallery/
├─ index.html
├─ style.css
└─ script.js
Open the folder in your text editor (e.g., VS Code, Notepad++) and open index.html in a browser to verify. We'll link the CSS and JS files in the next step.
Start with a minimal semantic HTML page, including a header and an empty gallery container. Add links to style.css and script.js. Save this in index.html and refresh your browser—it should show the header text.
<!-- index.html (Step 1) -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Simple Image Gallery</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="site-header">
<h1>Simple Image Gallery</h1>
<p class="lead">A responsive grid gallery built with HTML, CSS Grid and a little JavaScript.</p>
</header>
<main>
<section class="gallery-section">
<div class="gallery" id="gallery">
<!-- thumbnails will go here -->
</div>
</section>
</main>
<script src="script.js" defer></script>
</body>
</html>Notes: The defer attribute on the script ensures it loads after the HTML. The gallery div is empty for now—we'll add images later.
Add a CSS reset to normalize styles across browsers, plus base typography and colors. Save this in style.css. Refresh the browser to see the header styled (e.g., centered text, background color).
/* style.css (Step 2) */
:root {
--bg: #f7f7f8;
--card: #ffffff;
--muted: #666;
--accent: #0b6efd;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial;
background: var(--bg);
color: #111;
line-height: 1.4;
}
.site-header {
text-align: center;
padding: 2rem 1rem;
}
.site-header .lead { color: var(--muted); margin-top: 0.25rem; }
main { max-width: 1100px; margin: 0 auto; padding: 1rem; }
.gallery { display: grid; gap: 12px; }
/* We'll add grid columns and figure styles in later steps */
.figure-card {
background: var(--card);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(10,10,10,0.06);
}
.figure-card img { display: block; width: 100%; height: 100%; object-fit: cover; }
.figure-caption { padding: 0.6rem 0.75rem; font-size: 0.95rem; color: var(--muted); }Notes: Using CSS variables (:root) makes it easy to tweak colors later. The reset ensures consistent box-sizing and removes default margins.
Now populate the gallery with semantic markup using <figure> elements. Use placeholder images from picsum.photos (or your own local images). Add 6–9 figures inside the #gallery div in index.html. Refresh to see stacked images with captions.
<!-- index.html (Step 3) - Add these inside the #gallery div -->
<figure class="figure-card">
<img src="https://picsum.photos/id/1015/600/400" alt="Mountain lake" loading="lazy" data-full="https://picsum.photos/id/1015/1200/800">
<figcaption class="figure-caption">Mountain lake</figcaption>
</figure>
<figure class="figure-card">
<img src="https://picsum.photos/id/1025/600/400" alt="Dog portrait" loading="lazy" data-full="https://picsum.photos/id/1025/1200/800">
<figcaption class="figure-caption">Dog portrait</figcaption>
</figure>
<figure class="figure-card">
<img src="https://picsum.photos/id/1035/600/400" alt="Forest path" loading="lazy" data-full="https://picsum.photos/id/1035/1200/800">
<figcaption class="figure-caption">Forest path</figcaption>
</figure>
<figure class="figure-card">
<img src="https://picsum.photos/id/1043/600/400" alt="City skyline" loading="lazy" data-full="https://picsum.photos/id/1043/1200/800">
<figcaption class="figure-caption">City skyline</figcaption>
</figure>
<figure class="figure-card">
<img src="https://picsum.photos/id/1050/600/400" alt="Desert" loading="lazy" data-full="https://picsum.photos/id/1050/1200/800">
<figcaption class="figure-caption">Desert</figcaption>
</figure>
<figure class="figure-card">
<img src="https://picsum.photos/id/1062/600/400" alt="Seaside" loading="lazy" data-full="https://picsum.photos/id/1062/1200/800">
<figcaption class="figure-caption">Seaside</figcaption>
</figure>Notes: The data-full attribute stores the URL for the larger image (for the lightbox later). loading="lazy" improves performance by deferring off-screen image loads.
Update style.css to turn the gallery into a responsive grid using grid-template-columns. Adjust figure cards for consistent aspect ratios and overlay captions. Refresh to see the images in a grid that adapts to screen size.
/* style.css (Step 4 additions) */
.gallery {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.figure-card { height: 0; padding-bottom: 66%; position: relative; }
.figure-card img { position: absolute; inset: 0; width: 100%; height: 100%; }
.figure-caption {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, transparent, rgba(0,0,0,0.45));
color: white;
padding: 0.5rem 0.6rem;
font-size: 0.95rem;
}
/* Small responsiveness tweak */
@media (min-width: 900px) {
.site-header { padding: 3rem 1rem; }
}Notes: auto-fit and minmax make the grid responsive without media queries for columns. The padding-bottom trick maintains a 3:2 aspect ratio.
Add simple JS to create a lightbox modal on image click. Save in script.js. Test by clicking an image—it should open larger in an overlay. Close with click or Esc key.
// script.js (Step 5)
document.addEventListener('DOMContentLoaded', () => {
const gallery = document.getElementById('gallery');
// Create lightbox elements
const lightbox = document.createElement('div');
lightbox.id = 'lightbox';
lightbox.style.cssText = 'position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.75);z-index:9999;padding:20px;';
const img = document.createElement('img');
img.style.maxWidth = '95%';
img.style.maxHeight = '95%';
img.alt = '';
lightbox.appendChild(img);
document.body.appendChild(lightbox);
// Open on click
gallery.addEventListener('click', (e) => {
const target = e.target.closest('img');
if (!target) return;
const full = target.dataset.full || target.src;
img.src = full;
img.alt = target.alt || '';
lightbox.style.display = 'flex';
});
// Close on click or Esc
lightbox.addEventListener('click', () => { lightbox.style.display = 'none'; img.src = ''; });
window.addEventListener('keydown', (e) => { if (e.key === 'Escape') lightbox.style.display = 'none'; });
});Notes: This dynamically creates the lightbox to keep HTML clean. It uses event delegation for efficiency.
Add final tweaks: Style the lightbox image in style.css, ensure basic accessibility (e.g., add role="dialog" to lightbox via JS), and adjust spacing if needed. Test on mobile/desktop.
Update style.css with:
/* style.css (Step 6 additions) */
/* Lightbox basic style */
#lightbox img { border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.6); }Update script.js for accessibility (add after creating lightbox):
// script.js (Step 6 additions)
lightbox.setAttribute('role', 'dialog');
lightbox.setAttribute('aria-modal', 'true');Notes: For UX, add a close button if time allows (e.g., an 'X' icon). Discuss testing in different browsers.
Copy these into your project files for the complete gallery.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Simple Image Gallery</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<header class="site-header">
<h1>Simple Image Gallery</h1>
<p class="lead">A responsive grid gallery built with HTML, CSS Grid and a little JavaScript.</p>
</header>
<main>
<section class="gallery-section">
<div class="gallery" id="gallery">
<figure class="figure-card">
<img src="https://picsum.photos/id/1015/600/400" alt="Mountain lake" loading="lazy" data-full="https://picsum.photos/id/1015/1200/800">
<figcaption class="figure-caption">Mountain lake</figcaption>
</figure>
<figure class="figure-card">
<img src="https://picsum.photos/id/1025/600/400" alt="Dog portrait" loading="lazy" data-full="https://picsum.photos/id/1025/1200/800">
<figcaption class="figure-caption">Dog portrait</figcaption>
</figure>
<figure class="figure-card">
<img src="https://picsum.photos/id/1035/600/400" alt="Forest path" loading="lazy" data-full="https://picsum.photos/id/1035/1200/800">
<figcaption class="figure-caption">Forest path</figcaption>
</figure>
<figure class="figure-card">
<img src="https://picsum.photos/id/1043/600/400" alt="City skyline" loading="lazy" data-full="https://picsum.photos/id/1043/1200/800">
<figcaption class="figure-caption">City skyline</figcaption>
</figure>
<figure class="figure-card">
<img src="https://picsum.photos/id/1050/600/400" alt="Desert" loading="lazy" data-full="https://picsum.photos/id/1050/1200/800">
<figcaption class="figure-caption">Desert</figcaption>
</figure>
<figure class="figure-card">
<img src="https://picsum.photos/id/1062/600/400" alt="Seaside" loading="lazy" data-full="https://picsum.photos/id/1062/1200/800">
<figcaption class="figure-caption">Seaside</figcaption>
</figure>
</div>
</section>
</main>
<script src="script.js" defer></script>
</body>
</html>:root {
--bg: #f7f7f8;
--card: #ffffff;
--muted: #666;
--accent: #0b6efd;
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body {
margin: 0;
font-family: system-ui, -apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial;
background: var(--bg);
color: #111;
line-height: 1.4;
}
.site-header {
text-align: center;
padding: 2rem 1rem;
}
.site-header .lead { color: var(--muted); margin-top: 0.25rem; }
main { max-width: 1100px; margin: 0 auto; padding: 1rem; }
.gallery {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.figure-card {
background: var(--card);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 6px rgba(10,10,10,0.06);
height: 0;
padding-bottom: 66%;
position: relative;
}
.figure-card img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.figure-caption {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(180deg, transparent, rgba(0,0,0,0.45));
color: white;
padding: 0.5rem 0.6rem;
font-size: 0.95rem;
}
@media (min-width: 900px) {
.site-header { padding: 3rem 1rem; }
}
/* Lightbox basic style */
#lightbox img { border-radius: 6px; box-shadow: 0 8px 30px rgba(0,0,0,0.6); }document.addEventListener('DOMContentLoaded', () => {
const gallery = document.getElementById('gallery');
const lightbox = document.createElement('div');
lightbox.id = 'lightbox';
lightbox.style.cssText = 'position:fixed;inset:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.75);z-index:9999;padding:20px;';
lightbox.setAttribute('role', 'dialog');
lightbox.setAttribute('aria-modal', 'true');
const img = document.createElement('img');
img.style.maxWidth = '95%';
img.style.maxHeight = '95%';
img.alt = '';
lightbox.appendChild(img);
document.body.appendChild(lightbox);
gallery.addEventListener('click', (e) => {
const target = e.target.closest('img');
if (!target) return;
const full = target.dataset.full || target.src;
img.src = full;
img.alt = target.alt || '';
lightbox.style.display = 'flex';
});
lightbox.addEventListener('click', () => { lightbox.style.display = 'none'; img.src = ''; });
window.addEventListener('keydown', (e) => { if (e.key === 'Escape') lightbox.style.display = 'none'; });
});- Keyboard Navigation: Add left/right arrow key support to navigate between images in the lightbox.
- Filters: Add categories (e.g., tags) and a button bar to filter images by type.
- Custom Images: Replace placeholders with your own photos; adjust aspect ratios if needed.
- Advanced Accessibility: Trap focus in the modal (prevent tabbing outside), add
aria-hiddento background elements, and ensure screen reader compatibility. - Bonus: Make the lightbox zoomable or add swipe gestures for mobile.