Project: A Product Page
An image gallery, a specification list, a quantity form and structured data. The page where hidden fields, tables and forms all have to be right at once.
-
HTML Basics
- What is HTML: The Structure Layer of Every Web Page
- HTML Document Structure: DOCTYPE, html, head and body
- Elements, Tags and Attributes: The Vocabulary of HTML
- HTML Comments: Notes That Ship With Your Code
- Block Level and Inline Elements
- Writing and Running Your First HTML Page
- How a Browser Turns Markup Into a Page
- Text and Formatting
- Links and Navigation
- Images and Media
- Lists
- Tables
-
Forms
- Form Structure: form, action and method
- Input Types: Text, Email, Number, Date and the Rest
- Labels: The Most Important Element in a Form
- Checkboxes, Radio Buttons and Grouping
- select, option, optgroup and datalist
- textarea, File Uploads and Hidden Fields
- Buttons: submit, reset and button
- Built In Form Validation
- GET or POST: What Happens When a Form Is Submitted
- Semantic HTML
- HTML5 Features
- Head and Metadata
- HTML with CSS
- HTML with JavaScript
- Accessibility
-
HTML SEO
- How Google Works: Crawling, Indexing and Ranking
- SEO Friendly HTML Structure
- Titles and Descriptions That Earn Clicks
- Headings and Content Structure for Search
- Internal Linking and Anchor Text
- robots.txt and XML Sitemaps
- Canonical URLs and Duplicate Content
- Structured Data and JSON-LD
- Image SEO
- Core Web Vitals and Mobile Friendliness
- DevTools and Debugging
- Editor Productivity
- HTML Best Practices
- HTML Projects
- Advanced Projects
- Practice and Exams
The brief
Build a product detail page for a shop: gallery, description, specifications, price, an add to cart form and reviews.
Requirements
- A gallery with a main image and thumbnails that swap it.
- Specifications as a description list.
- An add to cart form sending a product identifier, not a price.
- Stock status conveyed by more than colour.
- Reviews as nested
articleelements. Productstructured data with real values.- Breadcrumb navigation.
The structure
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/kitchen">Kitchen</a></li>
<li><a href="/kitchen/kettles">Kettles</a></li>
<li><a href="/products/kettle-2l" aria-current="page">Two litre kettle</a></li>
</ol>
</nav>
<main id="main-content" tabindex="-1">
<article class="product">
<h1>Two litre electric kettle</h1>
<section class="gallery" aria-label="Product images">
<img id="main-image"
src="/images/kettle-800.jpg"
srcset="/images/kettle-400.jpg 400w, /images/kettle-800.jpg 800w, /images/kettle-1600.jpg 1600w"
sizes="(max-width: 50rem) 100vw, 30rem"
alt="A stainless steel two litre kettle, viewed from the front"
width="1600" height="1600"
fetchpriority="high">
<ul class="thumbs" role="list">
<li>
<button type="button"
data-full="/images/kettle-800.jpg"
data-alt="A stainless steel two litre kettle, viewed from the front"
aria-pressed="true">
<img src="/images/kettle-thumb.jpg" alt="Front view" width="80" height="80">
</button>
</li>
<li>
<button type="button"
data-full="/images/kettle-lid-800.jpg"
data-alt="The kettle with the lid open, showing the water level marks"
aria-pressed="false">
<img src="/images/kettle-lid-thumb.jpg" alt="Lid open" width="80" height="80">
</button>
</li>
</ul>
</section>
<section class="buy">
<p class="price">
<span class="visually-hidden">Price:</span>
<s>Rs 2,999</s>
<strong>Rs 2,499</strong>
<span class="saving">Save Rs 500</span>
</p>
<p class="stock stock--in">
<svg aria-hidden="true" width="16" height="16"><use href="#icon-tick"/></svg>
In stock, dispatched within two working days
</p>
<form action="/cart/add" method="post">
<input type="hidden" name="product_id" value="kettle-2l">
<input type="hidden" name="csrf_token" value="...">
<p>
<label for="quantity">Quantity</label>
<input type="number" id="quantity" name="quantity"
value="1" min="1" max="10" step="1">
</p>
<button type="submit">Add to cart</button>
</form>
</section>
<section>
<h2>Description</h2>
<p>Boils two litres in ninety seconds. Concealed element, so it is easy to clean.</p>
</section>
<section>
<h2>Specifications</h2>
<dl class="spec">
<div><dt>Capacity</dt><dd>2 litres</dd></div>
<div><dt>Power</dt><dd>1500 watts</dd></div>
<div><dt>Material</dt><dd>Stainless steel</dd></div>
<div><dt>Warranty</dt><dd>Two years</dd></div>
</dl>
</section>
<section>
<h2>Reviews</h2>
<p>Rated 4.4 out of 5 from 87 reviews.</p>
<article class="review">
<h3>Fast and quiet</h3>
<p>
<span class="visually-hidden">Rated</span> 5 out of 5 ·
Arun · <time datetime="2026-07-14">14 July 2026</time>
</p>
<p>Boils faster than the one it replaced and does not rattle.</p>
</article>
</section>
</article>
</main>The decisions that matter
The form sends an identifier, not a price
<!-- dangerous: the reader can edit this to 1 -->
<input type="hidden" name="price" value="2499">
<!-- safe: the server looks up the price -->
<input type="hidden" name="product_id" value="kettle-2l">Everything in the page source is editable in DevTools. Never send a value the server will trust.
Stock status is not colour alone
The status has an icon, text and a colour. A reader who cannot distinguish green from red still gets the message from two other channels.
The price is announced sensibly
A visually hidden Price: label and an s element on the old price mean a screen reader announces the sale clearly rather than reading two numbers with no relationship.
The thumbnails are buttons
Swapping the main image is an action, not navigation, so the control is a button. aria-pressed marks which view is current.
const main = document.getElementById("main-image");
const thumbs = document.querySelector(".thumbs");
thumbs.addEventListener("click", (event) => {
const button = event.target.closest("button[data-full]");
if (!button) return;
main.src = button.dataset.full;
main.alt = button.dataset.alt;
main.removeAttribute("srcset");
thumbs.querySelectorAll("button").forEach((b) => {
b.setAttribute("aria-pressed", String(b === button));
});
});Note that the alt text changes with the image. A gallery that swaps the picture and leaves the description behind is worse than no description at all.
Structured data
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Two litre electric kettle",
"image": [
"https://example.com/images/kettle-1600.jpg",
"https://example.com/images/kettle-lid-800.jpg"
],
"description": "Boils two litres in ninety seconds. Concealed element.",
"sku": "KET-2L-001",
"brand": { "@type": "Brand", "name": "Riverside" },
"offers": {
"@type": "Offer",
"url": "https://example.com/products/kettle-2l",
"priceCurrency": "INR",
"price": "2499",
"availability": "https://schema.org/InStock",
"priceValidUntil": "2026-12-31"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.4",
"reviewCount": "87"
}
}
</script>Every value here must match what is visible on the page. Invented ratings are a policy violation with a real penalty attached.
Checking your work
- Validate.
- Edit the hidden field in DevTools and confirm your server would reject it.
- Tab through the gallery: are the thumbnails reachable and is the current one announced?
- Emulate colour blindness in the Rendering panel; is the stock status still clear?
- Test the
Productstructured data. - Check which image variant is fetched at three widths.
Extensions
- Add a size or colour selector using radio buttons in a fieldset.
- Add a delivery estimate form using a postcode field.
- Add a review form with proper validation.
- Add a lightbox for the gallery, built on
dialog. - Add a comparison table against two similar products.
Practice
- Build the page and confirm the add to cart form sends only an identifier.
- Make the gallery keyboard operable with correct
aria-pressed. - Add
Productstructured data matching the visible values exactly. - Check the stock indicator under a colour blindness emulation.