Product Page Project
Build a responsive e-commerce product page with React, interactive product options, gallery preview, and cart state management
Time to implement the project: ~ 10-16 hours
- React Components
- Props and State
- Context API
- Product Gallery
- Size Selector
- Quantity Controls
- Responsive Design
- Cart Logic
In this beginner-level React project, you will build a responsive e-commerce product page that behaves like a real product detail screen in an online store. The page should present a product with a clean layout, image gallery, price, description, available sizes, quantity controls, and an add-to-cart interaction. The main goal is not only to make the page visually attractive, but also to understand how React manages dynamic user actions inside a reusable interface.
This project is especially useful for beginners because it combines several important React concepts in one practical task. You will split the page into components, pass data through props, store selected options in state, and use a cart context to keep product information available across the application. By the end, you should understand how a product page moves from static HTML into an interactive React interface with real user behavior.
Project Goal and Practical Learning Value
The purpose of this project is to help you practice React through a realistic e-commerce scenario. A product page may look simple at first, but it contains many frontend decisions that are common in real projects: how to organize components, where to store selected values, how to update the interface after user actions, and how to prevent confusing behavior when required options are missing.
You will learn how to build a page that reacts to user input. When a user selects a product size, changes the quantity, switches gallery images, or clicks the cart button, the interface should update immediately and predictably. This teaches one of the most important React habits: keeping the UI connected to state instead of manually changing elements in the DOM.
The project also introduces beginner-friendly cart architecture. Instead of storing everything inside one component, you will create a simple cart context that can hold selected products and quantities. This gives you a first practical understanding of shared state without jumping into advanced state management libraries too early.
Skills You Should Have Before Starting
This project is suitable for beginners, but it works best after you already understand the basic idea of React components and state. You do not need backend knowledge, payment integration, authentication, or a real database. The product data can be stored locally in a JavaScript object or imported from a simple data file.
The project should be approached as a frontend interface exercise. Focus on clean component structure, predictable state updates, readable JSX, and responsive CSS. Avoid making the project too complex at the beginning. A strong beginner implementation is better than an overloaded page with unfinished features.
- Basic understanding of React components, JSX, and props
- Ability to use useState for selected image, size, quantity, and UI feedback
- Basic knowledge of the Context API for sharing cart data between components
- Understanding of conditional rendering for selected states, disabled buttons, and validation messages
- Comfort with CSS, Flexbox, responsive layouts, and mobile-first interface adjustments
- Basic ability to organize files into components, data files, and styling files
Core Features of the React Product Page
The product page should feel like a simplified but realistic e-commerce interface. A visitor should be able to view product images, choose a size, change the quantity, and add the product to a cart state. Each feature should be implemented with beginner-friendly React logic, but the final result should still look clean, responsive, and professional.
| Feature | Implementation Focus |
| Product information layout | Display the product name, price, category, rating, short description, and key details in a structured layout. The content should be easy to scan on desktop and mobile. |
| Interactive image gallery | Build a gallery where users can click thumbnails and update the main product image. Store the active image in React state instead of changing the DOM manually. |
| Size selector | Allow users to choose one available size. The selected size should have a clear active style, and the page should prevent adding the product to the cart without a size. |
| Quantity controls | Add plus and minus buttons that update the selected quantity. Prevent invalid values such as zero or negative numbers to keep the cart logic predictable. |
| Add-to-cart behavior | When the user clicks the cart button, save the selected product, size, image, price, and quantity into cart context. Show a small confirmation message after the action. |
| Cart context structure | Create a simple React context to store cart items and expose functions such as addToCart, increaseQuantity, decreaseQuantity, or removeItem if you want to extend it. |
| Responsive product layout | On large screens, use a two-column layout with the gallery on one side and product details on the other. On mobile, stack all sections vertically with comfortable spacing. |
| User feedback and validation | Show helpful messages when a user tries to add a product without choosing a size. Use clear visual states for selected options, disabled actions, and successful cart updates. |
Implementation Guidance for React Beginners
Start by planning the component structure before writing the full interface. A good beginner structure may include ProductPage, ProductGallery, ProductInfo, SizeSelector, QuantitySelector, AddToCartButton, and CartProvider. This separation keeps the project easier to understand and prevents one large component from becoming difficult to edit.
Use local state for interface-specific choices such as the active image, selected size, quantity, and temporary success message. Use context only for data that should be available outside the product page, such as cart items. This distinction is important because beginners often put too much state in context too early, which makes the project harder to reason about.
Pay attention to user experience. The selected size should be visually obvious, the quantity buttons should feel reliable, and the add-to-cart button should clearly respond after being clicked. Small details such as hover states, active styles, spacing, and mobile readability make the project feel much closer to a real e-commerce page.
- Keep product data in a separate file so the JSX stays clean and focused on rendering
- Use props to pass product data into smaller components instead of duplicating content
- Store the selected gallery image in state and render the main image from that value
- Make the size selector controlled by React state, not by plain CSS classes only
- Prevent the quantity from going below one to avoid broken cart calculations
- Use Context API only for shared cart state, not for every small UI interaction
- Add basic validation before adding the item to the cart
- Test the layout on mobile, tablet, and desktop widths before calling the project finished
Common Mistakes When Building a Product Page Project
1. Mixing product data, selected quantity, and cart state in one object
An e-commerce product page looks simple at first: show a product, let users choose a quantity, and add it to the cart. The mistake is treating all of that as one object. Product data is mostly static: title, price, images, description, discount, and available variants. Selected quantity is temporary UI state. Cart state is a separate shopping decision that should survive after the user leaves the product detail area.
If these responsibilities are mixed together, the app becomes harder to reason about. For example, changing the quantity input may accidentally change the product object. Removing an item from the cart may also reset the product page. Adding the same product twice may create duplicate cart rows instead of increasing quantity. A clean product page should separate product information from user interaction state and cart state.
Problematic approach:
const [product, setProduct] = useState({
id: "sneakers-1",
title: "Fall Limited Edition Sneakers",
price: 125,
quantity: 0,
inCart: false
});
function increaseQuantity() {
setProduct({
...product,
quantity: product.quantity + 1
});
}
function addToCart() {
setProduct({
...product,
inCart: true
});
}
This makes the product object responsible for things that belong to the cart or UI. It works for one static product, but it does not scale to variants, multiple products, cart summaries, or checkout.
Better approach:
type Product = {
id: string;
title: string;
description: string;
price: number;
discountPercent?: number;
images: ProductImage[];
};
type CartItem = {
productId: string;
quantity: number;
selectedVariantId?: string;
};
const product: Product = {
id: "sneakers-1",
title: "Fall Limited Edition Sneakers",
description: "Low-profile sneakers for everyday wear.",
price: 250,
discountPercent: 50,
images: productImages
};
const [selectedQuantity, setSelectedQuantity] = useState(0);
const [cartItems, setCartItems] = useState<CartItem[]>([]);
Adding to cart safely:
function addToCart(productId: string, quantity: number) {
if (quantity <= 0) {
return;
}
setCartItems((currentItems) => {
const existingItem = currentItems.find((item) => {
return item.productId === productId;
});
if (!existingItem) {
return [
...currentItems,
{
productId,
quantity
}
];
}
return currentItems.map((item) => {
if (item.productId !== productId) {
return item;
}
return {
...item,
quantity: item.quantity + quantity
};
});
});
setSelectedQuantity(0);
}
Pay attention to: Keep product data, temporary quantity selection, and cart state separate. This makes the product page easier to extend into a real e-commerce flow.
2. Building the image gallery with fragile index logic
Product images are a major part of an e-commerce product page. Users expect thumbnails, a large preview, mobile carousel behavior, and sometimes a desktop lightbox. A common mistake is building the gallery around array indexes only. Index-based logic works with fixed demo images, but it becomes fragile when images are reordered, removed, loaded from an API, or connected to color variants.
A stronger gallery uses stable image IDs and stores the selected image by ID. This makes thumbnails, lightbox state, carousel navigation, and product variants easier to manage. The same image data should also include alt text, because product images are not decorative. A user who cannot see the image still needs useful information about what the image shows.
Problematic approach:
const [selectedIndex, setSelectedIndex] = useState(0);
function showNextImage() {
setSelectedIndex(selectedIndex + 1);
}
return (
<div>
<img src={images[selectedIndex]} />
{images.map((image, index) => (
<button key={index} onClick={() => setSelectedIndex(index)}>
<img src={image} />
</button>
))}
</div>
);
This can break when selectedIndex becomes larger than the image list. It also uses index keys and misses image alt text.
Better image model:
type ProductImage = {
id: string;
src: string;
thumbnailSrc: string;
alt: string;
};
const productImages: ProductImage[] = [
{
id: "front",
src: "/images/product-front.jpg",
thumbnailSrc: "/images/product-front-thumb.jpg",
alt: "Sneakers shown from the front"
},
{
id: "side",
src: "/images/product-side.jpg",
thumbnailSrc: "/images/product-side-thumb.jpg",
alt: "Sneakers shown from the side"
}
];
Stable gallery state:
const [selectedImageId, setSelectedImageId] = useState(productImages[0].id);
const selectedImage = productImages.find((image) => {
return image.id === selectedImageId;
}) || productImages[0];
function showNextImage() {
const currentIndex = productImages.findIndex((image) => {
return image.id === selectedImageId;
});
const nextIndex = (currentIndex + 1) % productImages.length;
setSelectedImageId(productImages[nextIndex].id);
}
Accessible thumbnails:
<div className="product-gallery">
<button
type="button"
className="product-gallery__main"
onClick={() => setLightboxOpen(true)}
aria-label="Open product image gallery"
>
<img src={selectedImage.src} alt={selectedImage.alt} />
</button>
<div className="product-gallery__thumbnails">
{productImages.map((image) => (
<button
key={image.id}
type="button"
onClick={() => setSelectedImageId(image.id)}
aria-pressed={image.id === selectedImageId}
aria-label={`Show image: ${image.alt}`}
>
<img src={image.thumbnailSrc} alt="" aria-hidden="true" />
</button>
))}
</div>
</div>
Pay attention to: Product galleries should use stable image IDs, meaningful alt text, bounded carousel navigation, and accessible thumbnail buttons.
3. Allowing invalid quantity values
Quantity controls are small, but they create many edge cases. Users should not be able to add zero items, negative items, decimal values, or unrealistic quantities. If
the quantity state is not controlled carefully, the cart can show invalid totals or create confusing behavior such as an item with quantity 0.
The UI should make invalid actions impossible where possible. The decrement button should be disabled at zero. The “Add to cart” button should be disabled until the user selects at least one item. If the project includes stock count, the increment button should also respect available inventory. These rules are simple, but they make the page feel much more realistic.
Problematic approach:
const [quantity, setQuantity] = useState(0);
function decreaseQuantity() {
setQuantity(quantity - 1);
}
function addProductToCart() {
addToCart(product.id, quantity);
}
This allows negative quantity and does not prevent adding invalid cart items.
Better quantity logic:
const MIN_QUANTITY = 0;
const MAX_QUANTITY = 10;
function increaseQuantity() {
setQuantity((currentQuantity) => {
return Math.min(currentQuantity + 1, MAX_QUANTITY);
});
}
function decreaseQuantity() {
setQuantity((currentQuantity) => {
return Math.max(currentQuantity - 1, MIN_QUANTITY);
});
}
function handleAddToCart() {
if (quantity <= 0) {
return;
}
addToCart(product.id, quantity);
setQuantity(0);
}
Button state:
<button
type="button"
onClick={decreaseQuantity}
disabled={quantity === MIN_QUANTITY}
aria-label="Decrease quantity"
>
-
</button>
<span aria-live="polite">{quantity}</span>
<button
type="button"
onClick={increaseQuantity}
disabled={quantity === MAX_QUANTITY}
aria-label="Increase quantity"
>
+
</button>
<button
type="button"
onClick={handleAddToCart}
disabled={quantity === 0}
>
Add to cart
</button>
Pay attention to: Quantity is business logic, not just UI state. Prevent negative values, disable invalid actions, and reset temporary quantity only after a successful cart update.
4. Calculating prices from formatted strings
Product pages often display original price, discount percentage, sale price, quantity, and cart subtotal. A common mistake is storing prices as display strings such as
"$125.00" and then trying to calculate totals from those strings. This leads to parsing problems, rounding issues, and messy code when discounts or cart
totals are added.
Store prices as numbers in the smallest practical unit, such as cents. Format prices only when rendering them. This makes calculations predictable and keeps display logic separate from business logic. Even for a small Frontend Mentor-style product page, this habit makes the implementation feel closer to real e-commerce work.
Problematic approach:
const product = {
price: "$250.00",
discount: "50%"
};
function getDiscountedPrice() {
const price = Number(product.price.replace("$", ""));
const discount = Number(product.discount.replace("%", ""));
return `$${price * (discount / 100)}`;
}
This mixes display format with calculations. It also makes cart totals harder to compute reliably.
Better price model:
type Product = {
id: string;
title: string;
priceCents: number;
discountPercent?: number;
};
function getFinalPriceCents(product: Product): number {
if (!product.discountPercent) {
return product.priceCents;
}
return Math.round(
product.priceCents * (1 - product.discountPercent / 100)
);
}
function formatPrice(priceCents: number): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
}).format(priceCents / 100);
}
Cart subtotal:
function getCartItemSubtotal(product: Product, quantity: number): number {
return getFinalPriceCents(product) * quantity;
}
const finalPrice = getFinalPriceCents(product);
const subtotal = getCartItemSubtotal(product, cartItem.quantity);
Rendering:
<div className="product-price">
<strong>{formatPrice(finalPrice)}</strong>
{product.discountPercent && (
<span>{product.discountPercent}%</span>
)}
{product.discountPercent && (
<del>{formatPrice(product.priceCents)}</del>
)}
</div>
Pay attention to: Store numeric prices for calculations and formatted prices for display. This prevents fragile parsing and makes discounts, totals, and order summaries easier to implement.
5. Making the cart dropdown depend on visual state only
Cart dropdowns are easy to make visually but harder to make reliable. A common mistake is showing a dropdown when the cart icon is clicked, but not handling empty state, remove actions, keyboard focus, outside clicks, or local persistence. The cart then works only in the happy path: add one item, open cart, look at it. A better product page should behave predictably when the cart is empty, when quantity changes, when the user removes an item, and when the page reloads.
If the project uses React Context API, cart logic should live in a cart provider or dedicated store, not inside the navbar dropdown. The dropdown should render the current cart state and call cart actions. This keeps cart behavior reusable if you later add a checkout page, order summary, or multiple product pages.
Problematic approach:
function CartDropdown({ product, quantity }) {
return (
<div className="cart">
<p>{product.title}</p>
<p>{quantity}</p>
<button>Checkout</button>
</div>
);
}
This cart component assumes there is always a product and does not support remove actions, empty state, or cart persistence.
Better cart context shape:
type CartContextValue = {
items: CartItem[];
addItem: (productId: string, quantity: number) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
clearCart: () => void;
};
State-aware dropdown:
function CartDropdown({ items, onRemoveItem }) {
return (
<aside className="cart-dropdown" aria-label="Shopping cart">
<h2>Cart</h2>
{!items.length ? (
<p>Your cart is empty.</p>
) : (
<div>
{items.map((item) => (
<CartItemRow
key={item.productId}
item={item}
onRemove={() => onRemoveItem(item.productId)}
/>
))}
<button type="button">Checkout</button>
</div>
)}
</aside>
);
}
Local persistence example:
useEffect(() => {
localStorage.setItem("cart-items", JSON.stringify(cartItems));
}, [cartItems]);
Pay attention to: Cart UI should be state-aware. Handle empty cart, removal, quantity updates, persistence, and accessible dropdown behavior instead of only showing a visual popover.
6. Forgetting responsive and accessible behavior for the lightbox and mobile gallery
Many product page challenges include a desktop lightbox and a mobile image carousel. Beginners often build the visual version but miss interaction details. A lightbox should close with a button, support keyboard navigation, trap or manage focus, and not leave the page scrollable behind it. A mobile carousel should use clear previous/next controls and should not depend only on tiny thumbnails that are hard to tap.
Responsive behavior also affects the layout. On desktop, product images and product details often sit side by side. On mobile, the gallery should come first, followed by product information and actions. If the layout is built with fixed widths, it may overflow or make the add-to-cart section uncomfortable to use.
Problematic lightbox:
{isLightboxOpen && (
<div className="lightbox">
<img src={selectedImage.src} />
</div>
)}
This opens an overlay but does not provide a close button, accessible role, keyboard handling, or image alt text.
Better lightbox structure:
{isLightboxOpen && (
<div
className="lightbox"
role="dialog"
aria-modal="true"
aria-label="Product image gallery"
>
<button
type="button"
className="lightbox__close"
onClick={() => setLightboxOpen(false)}
aria-label="Close image gallery"
>
×
</button>
<button
type="button"
onClick={showPreviousImage}
aria-label="Show previous product image"
>
Previous
</button>
<img src={selectedImage.src} alt={selectedImage.alt} />
<button
type="button"
onClick={showNextImage}
aria-label="Show next product image"
>
Next
</button>
</div>
)}
Responsive layout:
.product-page {
width: min(100%, 1120px);
margin: 0 auto;
padding: 24px;
display: grid;
gap: 32px;
}
@media (min-width: 768px) {
.product-page {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
align-items: center;
gap: 64px;
}
}
.product-actions {
display: grid;
gap: 16px;
}
@media (min-width: 768px) {
.product-actions {
grid-template-columns: 160px 1fr;
}
}
Pay attention to: A product page is judged by interaction quality. Make the gallery, lightbox, cart controls, and layout comfortable on both desktop and mobile.
After completing this project, you will have a strong beginner React portfolio piece that demonstrates more than just static layout skills. You will show that you understand components, props, state, conditional rendering, context, and responsive interface behavior. This project is a practical step toward building real e-commerce interfaces, because product pages are used in almost every online store and require many of the same frontend patterns you will meet in professional React projects.
Reference Implementations Worth Studying
Direct Frontend Mentor product page reference:
SvetlanaStoycheva - E-commerce Product Page FM React
This is the closest direct reference for the Product Page Project. It is a React implementation of an e-commerce product page where users can toggle the navbar/sidebar, open a lightbox gallery, use a mobile image slider carousel, switch the main product image through thumbnails, add items to the cart, view the cart, remove items, and keep cart updates in localStorage.
Pay particular attention to:
- How the product gallery handles both desktop lightbox behavior and mobile carousel behavior.
- How thumbnail selection changes the large product image.
- How cart state is updated when the user adds or removes products.
- How localStorage keeps cart behavior persistent after page reload.
- How sidebar navigation supports smaller screens without duplicating the whole layout.
Use this repository as the main feature reference. It matches the common product-page challenge closely and gives learners a practical example of gallery, cart, navigation, and responsive interaction in one page.
Context API cart reference:
anwarhossainbd - React Ecommerce Using Context API
This implementation is useful because it expands the product-page idea into a single-page e-commerce website using React Context API. Users can add products to the cart, increase or decrease product quantity, manage multiple products in the cart, remove quantities, and move toward payment through Stripe integration.
When studying the code, focus on:
- How Context API can centralize cart state instead of keeping cart logic inside one product component.
- How quantity controls work both on the product view and inside the cart section.
- How the cart changes when multiple products are added instead of only one demo product.
- How checkout or payment behavior changes the structure of a simple product page.
- What should be separated more clearly in your own version: product data, cart actions, UI state, and pricing helpers.
Use this repository as the cart-state reference. It is especially helpful if you want the Product Page Project to feel closer to a real e-commerce app instead of only a static product detail screen.
Modern React and TypeScript product-page reference:
mehmetbacik - Product Page
This repository is valuable as a modern implementation direction. It is an e-commerce product page built with Vite, React, and TypeScript, using Tailwind CSS and SCSS for styling. The project allows users to browse products, add them to the cart, and view order summaries.
While reviewing this project, examine:
- How TypeScript can make product and cart data models more reliable.
- How Vite supports a fast React development workflow for frontend projects.
- How Tailwind CSS and SCSS can work together for layout and component styling.
- How order summaries differ from a simple cart dropdown.
- How a product-page project can evolve into a larger e-commerce interface with multiple products.
Use this implementation as the TypeScript and modern tooling comparison point. It helps learners move beyond a single static product page toward a more structured, reusable, and scalable React e-commerce project.