Bismillâh, kardeşim. You have ten years of experience buying things online. This post is about how a single product page is actually put together. Sixty lines of HTML, forty lines of CSS, ten lines of JavaScript. No React, no Shopify, no framework. Just the bones that every shopping cart on the internet sits on top of, from Amazon down to the local bakery with a Wix page.

The demo at the top of this post is a real product card: a figure with an image, a title, a rating, a price with a discount, and an add-to-cart button that acknowledges you. Hover it, click it, watch the colour shift. Then open the code panel and read how it's built.

1. Semantic HTML: nouns first, not divs

An e-commerce page is mostly nouns. A product is a product; a figure is a figure; a title is a heading. HTML has a word for each of these, and using the right one gives you three things for free: search-engine understanding, screen-reader support, and styling hooks that age well.

<article class="product">
  <figure class="product-image">
    <img src="walnut-table.jpg" alt="Walnut side table">
    <span class="badge">New</span>
  </figure>
  <div class="body">
    <h3 class="title">Walnut Side Table</h3>
    <p class="desc">Solid walnut, hand-oiled finish.</p>
    <div class="price">
      <span class="now">$189</span>
      <span class="was">$249</span>
    </div>
    <button type="button" class="cta">Add to cart</button>
  </div>
</article>

Four meaningful choices, worth naming:

  • <article> for the product. It's a self-contained unit: you could lift it out of this page and put it on the homepage, the search results, or a newsletter, and it would still make sense. That's the HTML definition of an article.
  • <figure> for the image block. Figure means "a piece of illustrative content that belongs to the article." It also gives you a natural place to anchor the New badge, because you can position: absolute inside it without adding wrappers.
  • <h3> for the product title. Choose the level that fits the surrounding page: h3 works inside a page that already has an h1 (site name) and an h2 (category). Screen readers read the heading out when the user navigates headings.
  • type="button" on the CTA. If you drop a <button> inside a form without setting a type, it defaults to submit and will post the form on click. Not what you want for "add to cart". Always set the type.

2. An SVG placeholder is better than a blurred image

The demo doesn't actually fetch an image of a table. It draws one in SVG with a three-stop gradient: top lighter, bottom darker, to suggest wood grain. Why? Because a product card you're building today will be looked at tomorrow without the real photo (during seeding, during tests, during a slow network). Good fallback imagery is part of the craft.

<svg viewBox="0 0 200 200">
  <defs>
    <linearGradient id="wood" x1="0%" y1="0%" x2="0%" y2="100%">
      <stop offset="0%" stop-color="#a67246"/>
      <stop offset="100%" stop-color="#6b3f20"/>
    </linearGradient>
  </defs>
  <rect x="30" y="150" width="140" height="8" rx="2" fill="url(#wood)"/>
  <rect x="40" y="158" width="6" height="30" fill="#5a3318"/>
  <rect x="154" y="158" width="6" height="30" fill="#5a3318"/>
  <rect x="70" y="80" width="60" height="70" rx="4" fill="url(#wood)"/>
  <ellipse cx="100" cy="80" rx="30" ry="6" fill="#8c5a32"/>
</svg>

Two tabletops and two legs. The ellipse is the top edge, given a slightly lighter colour so your eye reads it as perspective. Seven shapes total. When the real photo loads, swap the SVG for an <img>; the rest of the card doesn't need to change.

3. A price block that tells the truth

Prices are a small ethics question. Showing "$189 (was $249)" must actually reflect reality or it becomes sıdk's opposite. The HTML pattern we want is two spans: the current price, the original. The CSS gives the old price a line-through and a muted colour; no JavaScript required.

<div class="price">
  <span class="now">$189</span>
  <span class="was">$249</span>
  <span class="save">Save 24%</span>
</div>
.was {
  color: rgba(232, 225, 245, 0.35);
  text-decoration: line-through;
}
.save {
  background: rgba(126, 227, 160, 0.12);
  color: #7ee3a0;
  padding: 0.15rem 0.5rem;
  border-radius: 6px;
  font-size: 10px;
  font-weight: 700;
}

The save chip does the percent calculation for the reader: (249 - 189) / 249 ≈ 24%. If you're about to reach for JavaScript here, don't. The number is fixed for this product; type it in. We'll automate discount calculations later in the series when we get to the cart.

4. The CTA, with acknowledgement

A checkout button that doesn't react to your click feels broken. Nothing worse than tapping "Add to cart" and being unsure whether anything happened. So the CTA has three states, and JavaScript handles the transition between them:

const cta = document.querySelector('#cta')
const note = document.querySelector('#note')
let state = 'idle'

cta.addEventListener('click', () => {
  if (state !== 'idle') return
  state = 'busy'
  cta.classList.add('added')
  cta.innerHTML = '<span class="cta-icon">✓</span> Added to cart'
  note.textContent = 'Nice. One walnut table waiting for you.'
  note.classList.add('visible')
  setTimeout(() => {
    cta.classList.remove('added')
    cta.innerHTML = '<span class="cta-icon">+</span> Add to cart'
    note.classList.remove('visible')
    state = 'idle'
  }, 2400)
})

Eight lines of logic. Note three tiny, boring, important things:

  • A state variable. The button ignores clicks while it's in busy. Without that guard, rage-clicking the button would queue up twenty "Added" toasts. State machines in two lines.
  • The confirmation line is separate. The <p class="note"> under the button carries the acknowledgement. Changing text in a button alone isn't enough; screen readers may skip the silent transformation. A separate text node reads naturally.
  • The colour change is all CSS. cta.classList.add('added'), and the transition to green happens via the stylesheet's transition rule. JavaScript doesn't manage colour; it just toggles a class.

5. Making it breathe: hover and elevation

One CSS trick lifts the card into a "tangible" state on hover:

.product {
  transition: transform 0.25s, box-shadow 0.25s, border-color 0.25s;
}
.product:hover {
  transform: translateY(-3px);
  border-color: rgba(165, 133, 255, 0.5);
  box-shadow: 0 28px 70px -18px rgba(93, 0, 255, 0.45);
}

A 3-pixel lift, a warmer border, a longer shadow: the card tilts "toward you." The transition line lists the three properties we animate, so the browser knows to tween only what matters. Never write transition: all without a very good reason. It makes unrelated changes (a class toggle, a layout shift) visibly jitter.

Tricks worth carrying

  • aspect-ratio locks the image box before the image arrives. The demo uses aspect-ratio: 1.2 on the figure so the card doesn't jump when the real photo loads. This one line made CLS (Cumulative Layout Shift) a solved problem.
  • The rating is text. <span class="stars">★★★★★</span> renders five filled stars in any font. If you need partial stars, use a width-clipped overlay; otherwise, text is copy-paste-able and translatable for free.
  • button:active { transform: scale(0.97) } gives the button a "depressed" feel. Two lines of CSS, no library.
  • Always set alt (or aria-label). A product without alt text is invisible to screen readers, Google image search, and slow connections showing broken-image glyphs. Write one clear sentence per image.

What comes next in the series

  1. This card. A single product, presented honestly.
  2. A cart badge that remembers across reloads (localStorage).
  3. A product grid that folds across devices in one line of CSS.
  4. A search box that filters the grid with ten lines of JS.
  5. Bir alışveriş sepeti that actually does math (Intl.NumberFormat).
  6. The cheapest checkout form: HTML5 validation all the way.

Build the card first, the foundation is honest. Then we wire it up to state, grids, filters, and finally a cart. Each piece small, each piece readable, nothing you can't replicate on your own in an afternoon.

Next post: "A cart badge that remembers: localStorage in 12 lines." We'll turn those "Add to cart" clicks into a real counter that survives refreshes.

Happy selling, kardeşim. Build honestly; ship simply.