I Built and Deployed a Full Food Delivery Website With Pure HTML, CSS, and JavaScript
By Alhan Bellary · GitHub: @alhaannn · Python Developer · Hubballi, India
Every frontend tutorial ends the same way: "Now deploy it!" — and then either doesn't tell you how, or points you to a paid service. I wanted to build something real, deploy it for free, and have it actually work for a real use case in my city.
The result is CraveCourier — a fully functional food delivery website for Hubballi, Karnataka. It's live at cravecourier-website.netlify.app, built with zero frameworks, zero backend, and zero cost to run. Just HTML5, CSS3, and vanilla JavaScript.
In this post I'll walk through the full architecture, every key design decision, and why I chose a pure frontend approach over the more "correct" backend-first solution.
Why No Backend?
The obvious question: a food delivery site needs order management, a database, admin tools — how does it work without a backend?
The honest answer is: for a portfolio project or MVP, it doesn't need one yet. What it needs is proof that the UX is right — that users can browse, build a cart, customise their order, and check out. That core loop can be validated entirely in the browser.
The architecture uses LocalStorage for everything that would normally require a server:
- User accounts — stored as JSON in LocalStorage on registration
- Session state — login status persisted across page refreshes via LocalStorage
- Shopping cart — cart items maintained in LocalStorage during the session
This is not production auth. But it demonstrates the complete user journey end-to-end — registration, login, session persistence, protected actions, cart management, checkout — which is what a portfolio project needs to show.
File Structure
The project is deliberately minimal:
CraveCourier/
├── index.html # Everything — nav, hero, menu, cart modal, auth modal
├── style.css # Full theming, responsive layout, all animations
├── script.js # Auth system + cart logic + all UI interactions
└── images/ # Logo (SVG) + 12 food photos
No build step. No npm. No webpack. Open index.html in any browser and it works.
The Color System
The design uses a dark theme based on #0F2027 — a deep navy-black — with pure white text and a warm amber accent for CTAs. This combination is deliberate:
- Dark backgrounds make food photography pop — the colors in the images become the visual focus
- High contrast improves readability on mobile in variable lighting conditions
- Warm accent (#F5A623) is used only for primary actions (Add to Cart, Checkout, Login) — it draws the eye to exactly the right places
:root {
--bg-primary: #0F2027;
--bg-card: #1a2a3a;
--text-primary: #ffffff;
--text-secondary: #b0bec5;
--accent: #F5A623;
--success: #2ecc71;
--danger: #e74c3c;
}
Every color in the site comes from this palette. Consistency at this level is what makes a UI feel designed rather than assembled.
Authentication System — LocalStorage Without a Backend
The auth system implements the full login/register flow using LocalStorage as the data store:
// Registration
function registerUser(name, email, password) {
const users = JSON.parse(localStorage.getItem('users') || '[]');
// Check for existing email
if (users.find(u => u.email === email)) {
showNotification('Email already registered', 'error');
return false;
}
users.push({ name, email, password, createdAt: Date.now() });
localStorage.setItem('users', JSON.stringify(users));
return true;
}
// Login
function loginUser(email, password) {
const users = JSON.parse(localStorage.getItem('users') || '[]');
const user = users.find(u => u.email === email && u.password === password);
if (user) {
localStorage.setItem('currentUser', JSON.stringify({ name: user.name, email }));
updateUIForLoggedInUser(user.name);
return true;
}
return false;
}
// Session persistence on page load
function checkExistingSession() {
const current = JSON.parse(localStorage.getItem('currentUser') || 'null');
if (current) {
updateUIForLoggedInUser(current.name);
}
}
What this correctly demonstrates:
- Duplicate email prevention on registration
- Session persistence across page refreshes (
checkExistingSessionruns on every load) - Protected actions —
addToCart()checkslocalStorage.getItem('currentUser')before allowing adds - User name display in the header after login
What this intentionally omits: password hashing (bcrypt belongs on a server), server-side session validation, CSRF protection. These are backend concerns and would be the first things added when connecting a real backend.
Shopping Cart — Real-Time Updates
The cart is a JavaScript object array maintained in a module-scoped variable and reflected into the DOM on every update:
let cart = [];
function addToCart(name, price, customNote = '') {
const currentUser = localStorage.getItem('currentUser');
if (!currentUser) {
showAuthModal();
showNotification('Please login to add items to cart', 'error');
return;
}
const existingItem = cart.find(item => item.name === name);
if (existingItem) {
existingItem.quantity++;
} else {
cart.push({ name, price, quantity: 1, note: customNote });
}
updateCartUI();
showNotification(`${name} added to cart!`, 'success');
}
function updateCartUI() {
const count = cart.reduce((sum, item) => sum + item.quantity, 0);
const total = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
document.getElementById('cart-count').textContent = count;
document.getElementById('cart-total').textContent = `₹${total}`;
renderCartItems();
}
Key design decisions:
- Quantity stacking — adding the same item again increments quantity instead of creating a duplicate entry
-
Protected add — if the user isn't logged in,
addToCartredirects them to the auth modal instead of silently failing -
Real-time total —
updateCartUI()is called on every mutation so the header cart count and the cart modal total are always in sync - Custom notes — every item accepts a text note for order customisation, stored alongside the cart item
CSS Architecture — No Framework Needed
The layout uses CSS Grid for the page sections and Flexbox for component-level alignment. The responsive breakpoints handle three viewport sizes:
/* Menu grid — 3 columns on desktop */
.menu-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
/* 2 columns on tablet */
@media (max-width: 768px) {
.menu-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 1 column on mobile */
@media (max-width: 480px) {
.menu-grid {
grid-template-columns: 1fr;
}
}
The animation system uses CSS transitions with transform (not top/left) for performance. Hover effects on cards and buttons use transform: translateY(-4px) — GPU-composited, no layout reflow:
.food-card {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.food-card:hover {
transform: translateY(-8px);
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
}
Notification system — success/error toasts slide in from the top right using a CSS class toggle:
.notification {
position: fixed;
top: 20px;
right: 20px;
transform: translateX(200%);
transition: transform 0.4s ease;
}
.notification.show {
transform: translateX(0);
}
JavaScript adds the show class, waits 3 seconds, removes it. The CSS handles the animation entirely.
Modal System — No Library Required
Both the auth modal (login/register) and the cart modal are built without any JavaScript modal library. The pattern is a fixed overlay <div> that starts with display: none and becomes display: flex on open:
function showCartModal() {
document.getElementById('cart-modal').style.display = 'flex';
document.body.style.overflow = 'hidden'; // Prevent background scroll
renderCartItems();
}
function closeCartModal() {
document.getElementById('cart-modal').style.display = 'none';
document.body.style.overflow = '';
}
// Close on backdrop click
document.getElementById('cart-modal').addEventListener('click', function(e) {
if (e.target === this) closeCartModal();
});
Body scroll lock (overflow: hidden) is critical for modals — without it, users can scroll the background content while the modal is open, which is disorienting.
Backdrop click to close is implemented by checking e.target === this — if the click target is the overlay itself (not a child element), close the modal. One line, no library.
Deployment on Netlify
The deployment is a drag-and-drop: zip the project folder, drop it on netlify.com/drop. Netlify assigns a URL and handles HTTPS automatically. For a static site with no build step, this is genuinely the fastest path from code to live URL.
Live site: cravecourier-website.netlify.app
What I'd Do Differently for a Real Production Version
The LocalStorage auth is the obvious first thing to replace. A real version would use:
-
Django backend with
django.contrib.authfor user management and sessions - PostgreSQL for orders, users, and menu items
- Django REST Framework API consumed by the frontend
- Stripe or Razorpay for payment processing
The frontend JavaScript would stay largely the same — fetch() calls to the API replace the LocalStorage reads. The cart logic, animation system, and CSS are production-ready as-is.
Full Source Code
Everything is on GitHub, and the site is live:
👉 github.com/alhaannn/CraveCourier-Website
🌐 cravecourier-website.netlify.app
About the Author
I'm Alhan Bellary (@alhaannn), a Python and frontend developer from Hubballi, Karnataka, India.
Other projects:
- gary-bot — Multi-channel automated gold trading bot (MT5 + Groq AI)
- AutoPilot-Judge — AI vision automation tool
- secure-validator — Hackathon Python password security tool
- Agricultural Market Price Intelligence — Django ML agri platform
- Crop Yield Forecasting System — AI farm management platform
Certifications: Oracle Certified Data Science Professional · IBM Certified AI Literacy
Tags: javascript · html · css · web-development · netlify · frontend · food-delivery · localstorage · vanilla-js · alhan-bellary · alhaannn
Top comments (0)