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.
<!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.
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.
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:
* { 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
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.
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:
<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
.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
- The rotated-square logo mark is a single
<span>withtransform: rotate(45deg). Cheap to render, no SVG needed, easy to swap colors via CSS variables. - 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.
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:
<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>
.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; }
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.
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
<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
.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; }
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.
Featured products + footer
6.1 — Product grid
.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 */ .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.
<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>
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
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
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:
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
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'; }); });
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 --> <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
- Open/close via one class toggle.
.cart-overlayisdisplay: noneby default,.cart-overlay.openisdisplay: flex. Adding/removing one class drives the whole show/hide. inset: 0sets top, right, bottom, and left all to 0 in one line. Combined withjustify-content: flex-end, the panel hugs the right edge.- 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.
Responsive design & deployment
9.1 — Media queries
Drop these at the bottom of the <style> block:
@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)
# 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
- Create a new public repo on GitHub
- Rename your file to
index.htmland commit it - In repo Settings → Pages, choose
mainbranch as the source - 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. Addimg { width: 100%; height: 100%; object-fit: contain; }to the image containers. - Persist the cart: save the
cartobject tolocalStorageafter 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.
That's the whole build. The pattern from Step 7 scales out nicely — you can add many more features without ever introducing a framework.