Faiaz Shisha
Multi-page website for a shisha lounge with flavour menu and booking
Overview
Faiaz Shisha is a multi-page website for a shisha lounge that leads with atmosphere. The design uses a deep dark background palette with gold accent colours, layered CSS gradients, and scroll-triggered fade-in animations to create the immersive, ambient feel the business wanted online. The site spans five pages: a hero home page establishing the mood, a flavour menu categorised into fruit, mint, and mixed blends, a photo gallery with a masonry-inspired CSS layout, a booking information page with a validated reservation form, and a contact/location page. JavaScript-powered tabs on the menu page let customers browse by category without leaving the page. CSS IntersectionObserver-triggered animations reveal content as users scroll, reinforcing the atmospheric design direction throughout the experience.
Architecture
Tech Stack
- HTML5 — Semantic page structure;
aria-selectedattributes on tab buttons for accessibility;aria-liveon the booking confirmation message - CSS3 — Custom properties for the gold/dark colour scheme;
@keyframesfor balloon float, fade-in cascade, and gradient pulse animations; CSS columns for masonry gallery layout - CSS Animations — Scroll-triggered via
IntersectionObserver; elements startopacity: 0; transform: translateY(20px)and transition to visible on entry - Vanilla JavaScript — Tab-based menu filtering,
IntersectionObserveranimation trigger, booking form validation (date range, time slot, party size limits), mobile menu toggle - Responsive Design — Mobile-first base with breakpoints at 480 px, 768 px, and 1024 px; dark theme maintained consistently at all sizes
Build Process
Established the visual language with deep charcoal backgrounds (#0d0d0d, #1a1a1a), gold accent colours (#c9a84c, #e8c96e), and off-white body text. All values were encoded as CSS custom properties so the entire theme can be adjusted from one declaration block.
:root {
--color-bg: #0d0d0d;
--color-surface: #1a1a1a;
--color-gold: #c9a84c;
--color-gold-lt: #e8c96e;
--color-text: #e8e0d5;
--color-muted: #888;
--font-display: 'Playfair Display', serif;
--font-body: 'Inter', sans-serif;
}
The hero uses a background image overlaid with a multi-stop linear-gradient from near-opaque dark at the bottom to semi-transparent at the top, creating depth without a separate overlay element. A subtle CSS @keyframes animation pulses the gradient opacity to give the hero a breathing, atmospheric feel on desktop.
.hero {
background-image:
linear-gradient(to top, rgba(13,13,13,0.95) 0%, rgba(13,13,13,0.4) 60%, transparent 100%),
url('images/hero-bg.jpg');
background-size: cover;
background-position: center;
min-height: 100svh;
display: flex;
align-items: flex-end;
}
Each flavour card has a data-category attribute (fruit, mint, mixed). Tab buttons use aria-selected for accessibility. The JavaScript filter works identically to the product filter pattern — reading data-category on each card and setting visibility based on the active tab, ensuring the DOM is the single source of truth.
The gallery uses the CSS columns property to achieve a masonry-style layout without JavaScript. Images flow into column tracks naturally, with a break-inside: avoid on each figure to prevent images splitting across column boundaries.
.gallery-masonry {
columns: 1;
gap: 1rem;
}
@media (min-width: 480px) { .gallery-masonry { columns: 2; } }
@media (min-width: 768px) { .gallery-masonry { columns: 3; } }
.gallery-masonry figure {
break-inside: avoid;
margin-bottom: 1rem;
}
The booking form validates: date (must be future date using Date.now() comparison), time slot (valid hours 17:00–01:00), and party size (1–20). Invalid states display inline error messages adjacent to the affected field. On valid submission a confirmation message receives focus for screen-reader accessibility.
function validateBooking(form) {
const date = new Date(form.date.value);
const hour = parseInt(form.time.value.split(':')[0]);
const size = parseInt(form.partySize.value);
const validHours = (hour >= 17) || (hour < 2);
if (date <= new Date()) return showError(form.date, 'Date must be in the future');
if (!validHours) return showError(form.time, 'Bookings available 17:00 – 01:00');
if (size < 1 || size > 20) return showError(form.partySize, 'Party size: 1 to 20');
return true;
}
Elements that should animate on scroll are given a .reveal class. A single IntersectionObserver watches all .reveal elements and adds .is-visible when they enter the viewport. CSS handles the transition entirely — .reveal starts at opacity: 0; transform: translateY(20px) and .reveal.is-visible transitions to opacity: 1; transform: none. Cascade delays are set with inline --delay custom properties.
// JS — set up observer
const observer = new IntersectionObserver((entries) => {
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('is-visible'); observer.unobserve(e.target); } });
}, { threshold: 0.15 });
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
/* CSS */
.reveal { opacity: 0; transform: translateY(20px); transition: opacity 0.5s ease var(--delay, 0s), transform 0.5s ease var(--delay, 0s); }
.reveal.is-visible { opacity: 1; transform: none; }
Visitor Experience Flow
Challenges & Solutions
IntersectionObserver Browser Support. Older browsers — primarily Internet Explorer 11 and early Edge — do not support IntersectionObserver. A feature-detection check (if ('IntersectionObserver' in window)) was used: if the API is available, the observer is set up; if not, all .reveal elements have .is-visible added immediately so they render fully visible without animation. This polyfill-free graceful degradation covers the vast majority of real-world users without adding any external dependency.
Booking Form Time Slot Restrictions. The HTML time input alone cannot restrict allowed hours to a business-hours window. Pure HTML min/max attributes on a time input only handle the simple case of a contiguous window and cannot handle the 17:00–01:00 window that wraps midnight. Custom JavaScript validation was required, parsing the hour from the input value and checking it against the allowed range with a logical OR for before-midnight and after-midnight hours.
What I Learned
- Layered CSS gradients over background images are more maintainable than a separate overlay element and give finer control over the blend
- The CSS
columnsproperty is a quick, JavaScript-free way to achieve masonry-style gallery layouts;break-inside: avoidis essential to prevent image splitting IntersectionObserveris the performant, standards-based approach for scroll-triggered animations — polling withscrollevent listeners causes layout thrashing- Using a single
--delayCSS custom property on each element for staggered cascade animations avoids duplicating transition rules and keeps the cascade flexible - Time-range validation that wraps midnight cannot be handled with HTML input attributes alone — custom JavaScript logic is necessary
- Dark-theme design systems benefit significantly from CSS custom properties; a palette change requires editing only the
:rootblock