2023 Web Development GitHub ↗

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

graph TD subgraph Pages["Pages"] A["index.html\nHero · Atmosphere · CTA"] B["menu.html\nFlavour Cards + Category Tabs"] C["gallery.html\nMasonry-style Photo Grid"] D["booking.html\nReservation Form + Validation"] E["contact.html\nLocation · Hours · Map"] end subgraph CSS["CSS Modules"] F["style.css\nDark Theme · Custom Properties · Layout"] G["animations.css\nKeyframes · Fade-in · Gradient Pulse"] end subgraph JS["JavaScript"] H["main.js\nMenu Tabs · IntersectionObserver\nBooking Validation · Mobile Menu"] end A --> F B --> F C --> F D --> F E --> F F --> G A --> H B --> H D --> H style A fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style B fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style C fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style D fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style E fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style F fill:#181818,stroke:#1e1e1e,color:#888 style G fill:#181818,stroke:#1e1e1e,color:#888 style H fill:#181818,stroke:#1e1e1e,color:#888

Tech Stack

  • HTML5 — Semantic page structure; aria-selected attributes on tab buttons for accessibility; aria-live on the booking confirmation message
  • CSS3 — Custom properties for the gold/dark colour scheme; @keyframes for balloon float, fade-in cascade, and gradient pulse animations; CSS columns for masonry gallery layout
  • CSS Animations — Scroll-triggered via IntersectionObserver; elements start opacity: 0; transform: translateY(20px) and transition to visible on entry
  • Vanilla JavaScript — Tab-based menu filtering, IntersectionObserver animation 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

1
Design Direction: Dark Ambient Theme

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;
}
2
Hero Section with Layered CSS Gradients

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;
}
3
Menu Page: Flavour Cards with JavaScript Tabs

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.

4
Gallery with CSS Columns Masonry Layout

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;
}
5
Booking Form with JavaScript Validation

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;
}
6
Scroll-Triggered Animations with IntersectionObserver

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

flowchart LR A["Visitor Arrives\nHero Page"] --> B["Atmospheric Hero\nCSS Gradient + Animation"] B --> C{"Explore Further"} C -->|"View flavours"| D["menu.html\nFlavour Cards + Tabs"] C -->|"See the space"| E["gallery.html\nMasonry Photo Grid"] C -->|"Make a booking"| F["booking.html\nReservation Form"] D --> F E --> F F --> G["JS Validates\nDate · Time · Party Size"] G -->|"Invalid"| H["Inline Error\nMessages Shown"] G -->|"Valid"| I["Confirmation\nMessage Shown + aria-live"] H --> F style A fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style B fill:#181818,stroke:#1e1e1e,color:#888 style C fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style D fill:#181818,stroke:#1e1e1e,color:#888 style E fill:#181818,stroke:#1e1e1e,color:#888 style F fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 style G fill:#181818,stroke:#1e1e1e,color:#888 style H fill:#181818,stroke:#1e1e1e,color:#888 style I fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0

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 columns property is a quick, JavaScript-free way to achieve masonry-style gallery layouts; break-inside: avoid is essential to prevent image splitting
  • IntersectionObserver is the performant, standards-based approach for scroll-triggered animations — polling with scroll event listeners causes layout thrashing
  • Using a single --delay CSS 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 :root block
HTML5 CSS3 Animations JavaScript Dark Theme Responsive