ShopifyIntermediateApril 10, 2026

Product Detail Page

A Shopify-style product detail page with a sticky sidebar, variation selectors (size, handlebar), gallery, and a specs table. Two-column layout: scrollable left content column, fixed-width sticky right sidebar.

View Full Demo →
FRAMENOVATREX LOS ANGELES . COLUMBUS AIRPLANE ALU TUBING
FORKFULL CARBON FORK . TAPERED 1-1/8" . RAKE 45MM . 700X32C MAX
FRAME SET COLORJET BLACK GLOSS
HEADSETCOLUMBUS COMPASS . 1-1/8" CARBON
WHEEL SETNTVX®106 CARBON WHEEL SET . 60MM DEPTH . CLINCHER
CRANKSROTOR VEGAST . 165MM
CHAINRINGROTOR 49T
BBROTOR TRACK 30 BSA
HANDLEBAR (OPTION 01)NTVX®101 CARBON DROP BAR . 400MM
HANDLEBAR (OPTION 02)NTVX®102 CARBON FLAT BAR
STEMNTVX®103 ALU STEM . 100MM
SEATPOSTNTVX®105 CARBON SEATPOST . Ø 27,2MM
SADDLESELLE SAN MARCO . SHORTFIT DYNAMIC NARROW
SEAT CLAMPCOLUMBUS ALU SEAT CLAMP
GRIPSTRONG V TRACK GRIPS BLACK (INCLUDED ONLY WITH FLAT BAR)
COGNTVX CUSTOM STEEL COG . 18T
TIRESCONTINENTAL ULTRA SPORT III . 700X25 C . FOLDABLE . BLACK
PEDALSNOT INCLUDED
BIKE WEIGHT6410 GR (M SIZE - WITHOUT PEDALS)
demo.jsx
import ProductDetailPage from "./index.jsx";
import { productDetailPage } from "../demo-data.js";

export default function ProductDetailPageDemo() {
  return <ProductDetailPage product={productDetailPage} />;
}
index.jsx
"use client";

import { useState } from "react";
import styles from "./styles.module.css";

function Breadcrumbs({ items }) {
  return (
    <nav className={styles.breadcrumbs} aria-label="Breadcrumb">
      {items.map((item, i) => (
        <span key={i} className={styles.breadcrumbItem}>
          {i > 0 && <span className={styles.breadcrumbSep} aria-hidden="true">/</span>}
          {item.link ? (
            <a href={item.link} className={styles.breadcrumbLink}>{item.label}</a>
          ) : (
            <span>{item.label}</span>
          )}
        </span>
      ))}
    </nav>
  );
}

function Gallery({ images, description }) {
  return (
    <div className={styles.gallery}>
      <figure className={`${styles.productImage} ${styles.productImageHero}`}>
        <img src={images[0]} alt="" />
      </figure>

      <h3 className={styles.productDescription}>{description}</h3>

      {images.slice(1).map((src, i) => (
        <figure className={styles.productImage} key={i}>
          <img src={src} alt="" />
        </figure>
      ))}
    </div>
  );
}

function Specs({ specs }) {
  return (
    <div className={styles.specsWrapper}>
      <table className={styles.specsTable}>
        <tbody>
          {specs.map((item, i) => (
            <tr key={i} className={styles.specsRow}>
              <td className={styles.specsLabel}>{item.label}</td>
              <td className={styles.specsValue}>{item.value}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

function VariationGroup({ label, options, selected, onSelect }) {
  return (
    <div className={styles.variationGroup}>
      <small className={styles.variationLabel}>{label}</small>
      <div className={styles.variationOptions}>
        {options.map((opt, i) => (
          <button
            key={i}
            className={`${styles.variationBtn} ${selected === opt ? styles.variationBtnSelected : ""}`}
            onClick={() => onSelect(opt)}
          >
            {opt}
          </button>
        ))}
      </div>
    </div>
  );
}

function Sidebar({ product }) {
  const [selectedSize, setSelectedSize] = useState(null);
  const [selectedHandlebar, setSelectedHandlebar] = useState(null);

  return (
    <aside className={styles.sidebar}>
      {product.logoText && (
        <p className={styles.logoText}>{product.logoText}</p>
      )}

      <h1 className={styles.productTitle}>{product.title}</h1>

      {product.subtitle && (
        <p className={styles.productSubtitle}>{product.subtitle}</p>
      )}

      <p className={styles.productPrice}>
        ${product.price} / {product.availability}
      </p>

      <div className={styles.variations}>
        {product.sizes?.length > 0 && (
          <VariationGroup
            label="Select Size"
            options={product.sizes}
            selected={selectedSize}
            onSelect={setSelectedSize}
          />
        )}

        {product.handlebars?.length > 0 && (
          <VariationGroup
            label="Select Handlebar"
            options={product.handlebars}
            selected={selectedHandlebar}
            onSelect={setSelectedHandlebar}
          />
        )}

        <button className={styles.addToCart}>ADD TO CART</button>
      </div>
    </aside>
  );
}

export default function ProductDetailPage({ product }) {
  return (
    <main className={styles.page}>
      <div className={styles.container}>
        <Breadcrumbs items={product.breadcrumbs} />

        <div className={styles.layout}>
          <div className={styles.content}>
            <Gallery images={product.images} description={product.description} />
            <Specs specs={product.specs} />
          </div>

          <Sidebar product={product} />
        </div>
      </div>
    </main>
  );
}
styles.module.css
.page {
  background: #fff;
  color: #000;
  min-height: 100vh;
  width: 88%;
}

.container {
  padding: 2rem 3.5rem 4rem;
}

/* ─── Breadcrumbs ─── */

.breadcrumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.375rem;
  font-size: 0.688rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 1.5rem;
}

.breadcrumbItem {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}

.breadcrumbSep {
  color: #000;
}

.breadcrumbLink:hover {
  text-decoration: underline;
}

/* ─── Two-column layout ─── */

.layout {
  display: flex;
  align-items: flex-start;
  gap: 4.5rem;
}

.content {
  flex: 1;
  min-width: 0;
}

.sidebar {
  width: 380px;
  flex-shrink: 0;
  position: sticky;
  top: 44px; /* offset for DemoNav */
  align-self: flex-start;
}

/* ─── Gallery ─── */

.productImage {
  margin: 0;
}

.gallery {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.productImage img {
  width: 100%;
  height: auto;
  display: block;
}

.productImageHero {
  height: 65vh;
}

.productImageHero img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.productDescription {
  font-size: clamp(1.25rem, 2.1vw, 1.875rem);
  font-weight: 700;
  text-transform: uppercase;
  line-height: 1.25;
  letter-spacing: -0.01em;
  margin: .25rem 0;
}

/* ─── Specs ─── */

.specsWrapper {
  margin-bottom: 4rem;
}

.specsTable {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.8rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.specsRow {
  border-bottom: 1px solid #000;
}

.specsRow:first-child {
  border-top: 1px solid #000;
}

.specsLabel {
  padding: 0.5rem 1rem 0.5rem 0;
  font-weight: 700;
  width: 28%;
  vertical-align: top;
  white-space: nowrap;
}

.specsValue {
  padding: 0.5rem 0;
  vertical-align: top;
}

/* ─── Sidebar ─── */

.logoText {
  font-family: cursive;
  font-size: 3.25rem;
  font-weight: normal;
  line-height: 1.05;
  margin-bottom: 1rem;
}

.productTitle {
  font-size: 1.75rem;
  font-weight: 700;
  text-transform: uppercase;
  line-height: 1.2;
  letter-spacing: -0.03em;
  margin-bottom: 0.75rem;
}

.productSubtitle {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  line-height: 1.55;
  margin-bottom: 1rem;
}

.productPrice {
  font-size: 1.75rem;
  font-weight: 700;
  margin-bottom: 1.5rem;
  letter-spacing: -0.03em;

}

.variations {
  display: flex;
  flex-direction: column;
  gap: 1.125rem;
  
}

/* ─── Variation group ─── */

.variationGroup {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;  letter-spacing: -0.03em;

}

.variationLabel {
  font-size: 0.688rem;
  text-transform: uppercase;
  letter-spacing: -0.03em;
  font-style: normal;
}

.variationOptions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

.variationBtn {
  border: 1px solid #000;
  border-radius: 9999px;
  padding: 0.5rem 1.25rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: -0.03em;
  font-weight: 700;
  cursor: pointer;
  background: transparent;
  color: #000;
  transition: background 0.15s, color 0.15s;
}

.variationBtn:hover {
  background: #f0f0f0;
}

.variationBtnSelected {
  background: #000;
  color: #fff;
}

.variationBtnSelected:hover {
  background: #000;
}

/* ─── Add to cart ─── */

.addToCart {
  width: 100%;
  border: 1px solid #000;
  border-radius: 9999px;
  padding: 0.625rem 1rem;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: -.03em;
  font-weight: 700;
  cursor: pointer;
  background: transparent;
  color: #000;
  transition: background 0.15s, color 0.15s;
}

.addToCart:hover {
  background: #000;
  color: #fff;
}

/* ─── Responsive ─── */

@media (max-width: 768px) {
  .container {
    padding: 1.5rem 1.25rem 3rem;
  }

  .layout {
    flex-direction: column;
  }

  .sidebar {
    width: 100%;
    position: static;
    order: -1;
    margin-bottom: 2rem;
  }
}