← JS Examples Shopwave
HTML · CSS · Vanilla JS · ~25 KB
View Sample ›
▸ Step-by-step tutorial

Build Shopwave in a Single HTML File

A practical walkthrough for building a one-file e-commerce homepage — no Node, no npm, no build step. Just open in a browser and it works.

9
Build steps
1
Output file
0
Dependencies
~25KB
Total size
Step 01

Create the file skeleton

Create a new file called shopwave.html and paste in the skeleton below. This is the scaffolding that every other step will fill in.

shopwave.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Shopwave - Ride the Wave of Savings</title>
  <style>
    /* Step 2 styles go here */
  </style>
</head>
<body>

  <!-- Step 3: top nav -->
  <!-- Step 4: sub nav -->
  <!-- Step 5: hero + card grid -->
  <!-- Step 6: featured products -->
  <!-- Step 7: footer -->
  <!-- Step 8: cart drawer -->

  <script>
    /* Step 9: data and behavior */
  </script>

</body>
</html>

Save it. Open it in a browser. You should see a blank page with the tab title "Shopwave - Ride the Wave of Savings". That's the starting point for everything below.

TIP

Keep the file open in your editor and the browser tab open at the same time. After every change, hit save then refresh. Tight feedback loops make this fun instead of frustrating.

Step 02

Base styles & the Shopwave palette

Before building any structure, lock in the colors. A consistent palette is what makes the site feel cohesive. Add these inside the <style> block:

CSS — base reset
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: "Inter", system-ui, Arial, sans-serif;
  background: #F4F4F6;
  color: #1A1A1A;
  font-size: 14px;
}

button { font-family: inherit; cursor: pointer; border: none; background: none; }
a { color: #0066FF; text-decoration: none; }
a:hover { color: #0048B8; text-decoration: underline; }
ul { list-style: none; }

The palette, explained

#1A1A1A
Charcoal — top nav
#2D2D33
Sub-nav / footer
#F4F4F6
Page background
#0066FF
Electric blue — accent
#4D8FFF
Soft blue — hover
#0048B8
Deep blue — pressed
#FFD814
CTA yellow
#FFA41C
Star rating
NOTE

Inter is a free, open-source font from Google Fonts — load it via a <link> tag in <head> or let the stack fall back to system fonts for an even smaller file.

Step 03

Top navigation

The top nav is the most recognizable element of any e-commerce site. Seven cells laid out in a flex row, with the search bar taking up all the leftover space in the middle.

3.1 — HTML structure

Paste this inside <body>, replacing the "top nav" comment:

HTML — top nav
<header>
  <div class="nav-top">

    <!-- Logo with diamond mark -->
    <button class="nav-cell">
      <div class="logo">
        <span class="logo-mark"></span>
        <span class="logo-text">Shop<span style="color:#4D8FFF">wave</span></span>
      </div>
    </button>

    <!-- Search bar -->
    <div class="nav-search">
      <button class="nav-search-cat">All ▾</button>
      <input id="search" type="text" placeholder="Search Shopwave">
      <button class="nav-search-btn">🔍</button>
    </div>

    <!-- Cart with live counter -->
    <button class="nav-cart" onclick="openCart()">
      🛒 <span id="cart-count">0</span> Cart
    </button>

  </div>
</header>

3.2 — Styling

CSS — nav-top
.nav-top {
  background: #1A1A1A;
  color: white;
  display: flex;
  align-items: stretch;
  padding: 4px 8px;
  gap: 4px;
}

.logo-mark {
  width: 22px; height: 22px;
  background: #0066FF;
  transform: rotate(45deg);
  display: inline-block;
}

.nav-cell {
  padding: 6px 9px;
  border: 1px solid transparent;
  color: white;
}
.nav-cell:hover { border-color: white; }

.nav-search {
  flex: 1;
  display: flex;
  height: 40px;
  border-radius: 4px;
  overflow: hidden;
}
.nav-search:hover { box-shadow: 0 0 0 3px #0066FF; }

.nav-search-btn { background: #0066FF; color: white; padding: 0 16px; }

3.3 — Two key tricks

  1. The rotated-square logo mark is a single <span> with transform: rotate(45deg). Cheap to render, no SVG needed, easy to swap colors via CSS variables.
  2. Hover-to-show-border on each nav cell uses a transparent 1px border by default, then turns it white on hover. This keeps the layout from shifting when the border appears.
Step 04

Sub-nav strip

Below the main nav is a slightly lighter charcoal bar with category links. Add this directly inside <header>, after the closing tag of nav-top:

HTML — sub nav
<div class="nav-sub">
  <button class="nav-sub-all">☰ All</button>
  <button>Today's Deals</button>
  <button>Customer Service</button>
  <button>Registry</button>
  <button>Gift Cards</button>
  <button>Wave+</button>
  <button class="nav-sub-deal">Limited time deals →</button>
</div>
CSS — sub nav
.nav-sub {
  background: #2D2D33;
  color: white;
  display: flex;
  padding: 4px 8px;
  font-size: 14px;
}
.nav-sub button {
  color: white;
  padding: 6px 8px;
  border: 1px solid transparent;
}
.nav-sub button:hover { border-color: white; }

.nav-sub-deal {
  color: #4D8FFF !important;
  font-weight: 700;
  margin-left: auto;
}
NOTE

The margin-left: auto on the deals button pushes it to the far right of the flex row. Same trick to right-align any single item in a flex container — no floats needed.

Step 05

The category card grid

This is the layout that defines the homepage. A 4-column grid of square white cards, each containing either a 2×2 mini-grid of category tiles or one large image.

5.1 — Hero gradient backdrop

HTML + CSS
<div class="hero-wrap">
  <div class="hero-gradient"></div>
  <div class="container">
    <div class="card-grid" id="card-grid"></div>
  </div>
</div>

<style>
.hero-wrap { position: relative; }
.hero-gradient {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 320px;
  background: linear-gradient(to bottom, #2D2D33, #6B7B95 50%, transparent);
}
.container { max-width: 1500px; margin: 0 auto; padding: 16px; position: relative; }
.card-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
</style>

5.2 — Individual cards

CSS — cards
.card { background: white; padding: 20px; }
.card h2 { font-size: 21px; font-weight: 700; margin-bottom: 12px; }

.card-tiles {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-bottom: 12px;
}

.card-tile-img {
  background: #F3F3F3;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 52px;
}

.card-link { font-size: 14px; color: #0066FF; }
.card-link:hover { color: #0048B8; text-decoration: underline; }
KEY

The aspect-ratio: 1 property makes each tile a perfect square regardless of how wide its column is. This gives the homepage its tidy proportions across all screen sizes.

Step 06

Featured products + footer

6.1 — Product grid

CSS — products
.product-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 16px;
}

.product-img {
  background: #F7F7F7;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 84px;
}

.product-title {
  font-size: 14px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

6.2 — Three-size price typography

A simple trick borrowed from financial print: small dollar sign superscripted, large dollars, small cents superscripted. Reads as price-emphasis at a glance.

CSS + HTML — price
/* CSS */
.price-sym, .price-cents {
  font-size: 12px;
  vertical-align: top;
  position: relative;
  top: 2px;
}
.price-dollars { font-size: 19px; font-weight: 500; }

<!-- Usage -->
<span class="price-sym">$</span><span class="price-dollars">49</span><span class="price-cents">99</span>

6.3 — Footer

Three layers: four columns of links, a center band with language/currency pickers, and a dark bottom strip with copyright.

HTML — footer
<footer>
  <div class="footer-cols">
    <div><h3>Get to Know Us</h3>
      <ul><li>Careers</li><li>Blog</li></ul>
    </div>
    <!-- three more columns -->
  </div>
  <div class="footer-bottom">
    <p>© 2026, Shopwave Inc.</p>
  </div>
</footer>
Step 07

Data & behavior in vanilla JS

Time to bring it to life. All the cards, products, and cart logic live inside the <script> tag at the bottom of the file.

7.1 — Define the data

JavaScript — data
const CARDS = [
  {
    title: 'Get your game on',
    items: [
      { l: 'Consoles',  e: '🎮' },
      { l: 'Headsets',  e: '🎧' },
      { l: 'Controllers', e: '🕹️' },
      { l: 'PC gaming', e: '💻' }
    ],
    link: 'Shop gaming'
  },
  { title: 'Toys under $25', big: true, image: '🧸', link: 'Shop now' }
];

const PRODUCTS = [
  { id: 1, title: 'Wave Smart Speaker', price: 49.99, rating: 4.7, image: '🔊' },
  { id: 2, title: 'Wave eReader 16GB',  price: 139.99, rating: 4.6, image: '📱' }
];

let cart = {};

7.2 — Render the cards

JavaScript — render
function renderCards() {
  const grid = document.getElementById('card-grid');
  grid.innerHTML = CARDS.map(card => {
    let inner;
    if (card.big) {
      inner = `<div class="card-big-img">${card.image}</div>`;
    } else {
      inner = `<div class="card-tiles">${card.items.map(it => `
        <button class="card-tile">
          <div class="card-tile-img">${it.e}</div>
          <div class="card-tile-label">${it.l}</div>
        </button>`).join('')}</div>`;
    }
    return `<div class="card"><h2>${card.title}</h2>${inner}
      <a class="card-link">${card.link}</a></div>`;
  }).join('');
}

7.3 — Cart logic

The cart is a plain object where keys are product IDs and values are quantities. Three small functions handle all mutations:

JavaScript — cart
function addToCart(id) {
  cart[id] = (cart[id] || 0) + 1;
  updateCart();
}

function removeOne(id) {
  if (cart[id] > 1) cart[id]--;
  else delete cart[id];
  updateCart();
}

function updateCart() {
  const count = Object.values(cart).reduce((a, b) => a + b, 0);
  document.getElementById('cart-count').textContent = count;
  renderCartBody();
}

7.4 — Live search

JavaScript — search
document.getElementById('search').addEventListener('input', (e) => {
  const q = e.target.value.toLowerCase();
  document.querySelectorAll('.product').forEach((el, i) => {
    el.style.display = PRODUCTS[i].title.toLowerCase().includes(q)
      ? 'flex' : 'none';
  });
});
Step 08

Slide-in cart drawer

The cart drawer is a fixed-position overlay that slides in from the right when the cart icon is clicked.

HTML + CSS — cart drawer
<!-- HTML -->
<div class="cart-overlay" id="cart-drawer"
     onclick="if(event.target===this)this.classList.remove('open')">
  <div class="cart-panel">
    <div class="cart-header">
      <h2>Shopping Cart</h2>
      <button onclick="closeCart()"></button>
    </div>
    <div id="cart-body"></div>
  </div>
</div>

/* CSS */
.cart-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  z-index: 50;
  display: none;
  justify-content: flex-end;
}
.cart-overlay.open { display: flex; }

.cart-panel {
  background: white;
  width: 100%;
  max-width: 450px;
  height: 100%;
  overflow-y: auto;
}

The three clever bits

  1. Open/close via one class toggle. .cart-overlay is display: none by default, .cart-overlay.open is display: flex. Adding/removing one class drives the whole show/hide.
  2. inset: 0 sets top, right, bottom, and left all to 0 in one line. Combined with justify-content: flex-end, the panel hugs the right edge.
  3. Backdrop click closes, panel click doesn't. The if(event.target===this) check closes the cart when clicking the dim backdrop, but ignores clicks inside the panel.
Step 09

Responsive design & deployment

9.1 — Media queries

Drop these at the bottom of the <style> block:

CSS — responsive
@media (max-width: 1100px) {
  .card-grid    { grid-template-columns: repeat(2, 1fr); }
  .product-grid { grid-template-columns: repeat(3, 1fr); }
}

@media (max-width: 700px) {
  .card-grid    { grid-template-columns: 1fr; }
  .product-grid { grid-template-columns: repeat(2, 1fr); }
  .nav-cell .line1, .nav-cell .line2 { display: none; }
}

9.2 — Test checklist

Tap each item as you verify it:

  • Page opens and renders without errors
  • Resize window — grid collapses to 2 columns at 1100px, 1 at 700px
  • Type in search bar — products filter live
  • Click "Add to cart" — counter in nav increments
  • Click cart icon — drawer slides in from right
  • Use +/− buttons in cart — quantities update
  • Click Delete — item removes entirely
  • Click backdrop outside cart — drawer closes

9.3 — Deployment options

Option A — Just open the file

Double-click shopwave.html. Opens in your browser. Works offline. No server needed.

Option B — Drop on a web server (Nginx/Apache on Ubuntu)

Bash — deploy
# On the server
sudo cp shopwave.html /var/www/html/
sudo chown www-data:www-data /var/www/html/shopwave.html
sudo chmod 644 /var/www/html/shopwave.html

# Now reachable at https://yourdomain.com/shopwave.html

Option C — GitHub Pages

  1. Create a new public repo on GitHub
  2. Rename your file to index.html and commit it
  3. In repo Settings → Pages, choose main branch as the source
  4. Live in a minute at https://yourusername.github.io/reponame/

9.4 — Where to take it next

  • Real product images: swap emoji for <img> tags. Add img { width: 100%; height: 100%; object-fit: contain; } to the image containers.
  • Persist the cart: save the cart object to localStorage after every change so it survives refreshes.
  • Add a backend: convert to PHP/SQLite3 — keep the same single-file front end, but post cart contents to a PHP endpoint that writes to SQLite. Two files total.
  • Product detail pages: use URL hash routing (#/product/1) so the back button works. About 20 lines of vanilla JS.
  • Sponsored listings, reviews, recommendations: each is just a new data array and a new render function.
DONE

That's the whole build. The pattern from Step 7 scales out nicely — you can add many more features without ever introducing a framework.