← BEUVAULT Advanced Course

Advanced Web Development

Deep-dive concepts across HTML semantics, CSS architecture, and JavaScript runtime — the material that separates junior from senior engineers.

Part 1: Advanced HTML & Web Platform

1. Semantic HTML & Accessibility (ARIA / WCAG)

Semantic HTML conveys meaning — not just appearance. Accessibility relies on it.

ARIA Landmarks

ARIA landmark roles identify page regions for screen readers. Every page should use them:

<header role="banner">Site title & navigation</header>
<nav role="navigation" aria-label="Main">Primary links</nav>
<main role="main">Primary content</main>
<aside role="complementary">Sidebar</aside>
<footer role="contentinfo">Copyright, contact</footer>

Landmarks let assistive-technology users skip directly to any section without tabbing through every element.

WCAG 2.2 — Key Success Criteria

  • 1.4.3 Contrast (AA): Text must have a contrast ratio of at least 4.5:1 against its background. Large text (≥18px bold or ≥24px) needs at least 3:1.
  • 2.1.1 Keyboard: All functionality must be operable through a keyboard. Use tabindex="0" to make non-focusable elements focusable; manage :focus-visible rings for keyboard-only users.
  • 2.4.4 Link Purpose: Every link's purpose must be clear from its text alone (or from the text plus its aria-label). Avoid "click here."
  • 4.1.2 Name, Role, Value: All interactive elements must expose their name, role, and value to the accessibility tree. Native HTML elements do this automatically; custom components need ARIA attributes.

Practical Patterns

<button aria-label="Close dialog" aria-expanded="true">&times;</button>
<div role="alert" aria-live="polite">Form submitted successfully</div>
<img src="chart.png" alt="Bar chart: 2025 sales by quarter">

aria-live="polite" tells screen readers to announce dynamic updates without interrupting the current task. Use aria-live="assertive" only for urgent messages.

2. Web Security: XSS, CSRF, CSP & CORS

These four security mechanisms are the minimum every web developer must understand. Each addresses a different attack vector.

Cross-Site Scripting (XSS)

An attacker injects malicious scripts into a trusted website. Three types:

  • Reflected XSS: Script in URL/query param is immediately reflected in the server's response without sanitization.
  • Stored XSS: Script is saved on the server (e.g., in a comment, profile field) and served to every visitor. Most dangerous — affects all users.
  • DOM-based XSS: Script never touches the server. Client-side JS reads attacker-controlled input (URL hash, document.referrer) and writes it into the DOM unsafely.

Prevention: Always sanitize output — use textContent instead of innerHTML, escape user data in templates, and set the Content-Security-Policy header to block inline scripts.

Content Security Policy (CSP)

CSP is an HTTP response header (Content-Security-Policy) that tells the browser which resources are allowed to load. It's the most effective XSS defense:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:;

Key directives (per the CSP Level 3 specification, W3C, 2025):

  • default-src — fallback for all fetch directives
  • script-src — valid script sources. Use 'nonce-...' or 'strict-dynamic' instead of 'unsafe-inline'
  • object-src 'none' — block Flash/plugins entirely
  • base-uri 'self' — prevent injection of malicious <base> tags
  • upgrade-insecure-requests — automatically upgrade HTTP to HTTPS

Cross-Site Request Forgery (CSRF)

An attacker tricks an authenticated user into submitting a malicious request. Example: an external page auto-submits a form to POST /transfer-funds using the user's cookies, which are sent automatically.

Prevention: Use anti-CSRF tokens — a cryptographically random value embedded in the form and validated on the server. Modern frameworks (Django, Rails, Express with csurf) handle this. SameSite cookies (SameSite=Strict or Lax) also mitigate CSRF.

Cross-Origin Resource Sharing (CORS)

Browsers enforce a Same-Origin Policy: by default, a page at https://a.com cannot read responses from https://b.com. CORS is the mechanism that relaxes this selectively:

// Server response headers to allow cross-origin access
Access-Control-Allow-Origin: https://trusted-site.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true

For "non-simple" requests (e.g., PUT, DELETE, or custom headers), the browser sends a preflight OPTIONS request. The server must respond with the allowed methods and headers before the actual request is sent.

3. Web Components: Custom Elements, Shadow DOM & Templates

Web Components are three browser-native APIs for creating reusable, encapsulated HTML elements. No framework needed.

Custom Elements (v1)

Define your own HTML tag with lifecycle callbacks (spec: HTML Standard §4.13, WHATWG, 2026):

class RateCard extends HTMLElement {
    connectedCallback() {
        // Runs when element is inserted into DOM
        const stars = this.getAttribute("stars") || "0";
        this.innerHTML = `★`.repeat(stars) + `☆`.repeat(5 - stars);
    }
    static get observedAttributes() { return ["stars"]; }
    attributeChangedCallback(name, oldVal, newVal) {
        if (name === "stars") this.connectedCallback();
    }
}
customElements.define("rate-card", RateCard);

Usage: <rate-card stars="4"></rate-card>

Lifecycle: constructorconnectedCallback (mount) → attributeChangedCallback (updates) → disconnectedCallback (unmount) → adoptedCallback (move document).

Shadow DOM

Encapsulates DOM and CSS. Styles inside the shadow tree cannot leak out, and external styles cannot leak in:

class Tooltip extends HTMLElement {
    connectedCallback() {
        const shadow = this.attachShadow({ mode: "open" });
        shadow.innerHTML = `
            <style>
                .tooltip { background: #333; color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; }
                :host { display: inline-block; }
            </style>
            <div class="tooltip"><slot></slot></div>`;
    }
}
customElements.define("my-tooltip", Tooltip);

<slot> projects light-DOM children into the shadow tree. This is how browsers implement <input>, <select>, and <video> — their internal UI is rendered via Shadow DOM.

HTML Templates

The <template> element holds inert HTML that is not rendered. Clone it in JS to create reusable fragments:

<template id="card-template">
    <style>.card { border: 1px solid #ddd; padding: 1rem; border-radius: 6px; }</style>
    <div class="card">
        <h3><slot name="title">Card</slot></h3>
        <slot></slot>
    </div>
</template>

Part 2: Advanced CSS Architecture

1. Modern Selectors & Cascade Control

The :has() Selector — "Parent Selector"

Baseline Widely Available since December 2023 (Chrome 105, Firefox 121, Safari 15.4). :has() selects an element based on its descendants:

/* Style a card only if it contains an image */
.card:has(img) { grid-column: span 2; }

/* Highlight a form group with validation errors */
.form-group:has(input:invalid) { border-color: red; }

/* Style the entire row when a checkbox is checked */
tr:has(input[type="checkbox"]:checked) { background: #f0fdf4; }

This eliminates JavaScript DOM-traversal patterns. Note: :has() is a forgiving selector list — if the browser doesn't support it, the entire rule fails gracefully when used with :is() or :where().

CSS Cascade Layers (@layer)

Before @layer, specificity and source order determined which rules won. Layers let authors define explicit precedence tiers:

@layer reset, base, components, utilities;

@layer reset {
    *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
}
@layer base {
    body { font-family: system-ui; line-height: 1.6; color: #333; }
}
@layer components {
    .card { background: white; border-radius: 8px; padding: 1.5rem; }
}
@layer utilities {
    .mt-4 { margin-top: 1rem; }
}

Rules in later layers always beat earlier ones regardless of specificity. Unlayered styles win over all layered styles. This completely replaces the old "specificity hack" pattern.

Specificity Calculation

Specificity is calculated as (inline, ID, class, element):

  • #nav .item a → (0, 1, 1, 1)
  • style="color:red" → (1, 0, 0, 0) — always wins without !important
  • li.active → (0, 0, 1, 1)
  • ul li → (0, 0, 0, 2)

!important inverts the cascade — use sparingly. Layers and :where() (which always contributes 0 specificity) are better alternatives.

2. Container Queries & Modern Responsive Patterns

Container Queries (@container)

Well established across browsers since February 2023 (Chrome 105, Firefox 110, Safari 16). Unlike media queries that respond to the viewport, container queries respond to a parent element's size:

.card-wrapper { container-type: inline-size; container-name: card; }

@container card (min-width: 400px) {
    .card { display: grid; grid-template-columns: 200px 1fr; gap: 1rem; }
}
@container card (max-width: 399px) {
    .card { display: flex; flex-direction: column; }
}

container-type: inline-size creates a query container on the inline axis. The @container rule conditionally applies styles. This enables truly reusable components that adapt to their parent, not the screen size.

clamp(), min(), max()

Fluid responsive design without media queries:

font-size: clamp(1rem, 2.5vw, 2rem);
/* Minimum 1rem, preferred 2.5vw, maximum 2rem */

width: min(100%, 1200px);
/* Shrink to fit, but never exceed 1200px */

padding: max(1rem, 3vw);
/* Always at least 1rem, scale up with viewport */

Subgrid

Child grid items can inherit their parent's grid tracks using subgrid:

.parent { display: grid; grid-template-columns: 200px 1fr 200px; gap: 1rem; }
.child { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; }

This is essential for aligning nested content (e.g., cards in a grid where each card has a header, body, footer that must align across rows).

Logical Properties

Instead of margin-left / margin-right, write direction-aware properties:

margin-inline: auto;    /* margin-left + margin-right */
padding-block: 1rem;    /* padding-top + padding-bottom */
border-inline-start: 2px solid;  /* border-left in LTR */

These adapt automatically when the user's language direction changes (e.g., Arabic, Hebrew).

3. Custom Properties Deep Dive & @property

CSS Custom Properties Registered via @property

Baseline 2024 (Chrome 85+, Firefox 128+, Safari 16.4+). The @property at-rule (CSS Properties and Values API Level 1, W3C) lets you define type-checked custom properties with inheritance rules:

@property --my-color {
    syntax: "<color>";
    inherits: false;
    initial-value: #3b82f6;
}

@property --spacing {
    syntax: "<length>";
    inherits: true;
    initial-value: 1rem;
}

@property --scale {
    syntax: "<number>";
    inherits: false;
    initial-value: 1;
}

.card {
    --spacing: 2rem;
    padding: var(--spacing);
    background: var(--my-color);
    transform: scale(var(--scale));
    transition: --scale 0.3s, --my-color 0.3s;
}
.card:hover { --scale: 1.05; --my-color: #ec4899; }

Without @property, the browser cannot animate custom properties because it doesn't know their type. With registration, it knows --scale is a <number> and can interpolate between 1 and 1.05.

CSS registerProperty() in JavaScript

CSS.registerProperty({
    name: "--gradient-angle",
    syntax: "<angle>",
    inherits: false,
    initialValue: "0deg"
});

If both @property in CSS and registerProperty() in JS define the same property name, the JavaScript registration wins.

Part 3: Advanced JavaScript Runtime

1. The Event Loop: How JavaScript Actually Executes

JavaScript is single-threaded with a concurrent model based on an event loop. Understanding this is the single most important step in mastering JS.

The Execution Order

The event loop processes tasks in a strict sequence: sync code → microtasks → requestAnimationFrame → macrotasks.

console.log("1: sync");                       // 1st

setTimeout(() => console.log("4: macrotask"), 0); // 4th

Promise.resolve().then(() => console.log("2: microtask")); // 2nd

queueMicrotask(() => console.log("3: another microtask")); // 3rd

requestAnimationFrame(() => console.log("3a: RAF"));       // ~3a (before render)

console.log("1b: sync");                       // 1st

Output: 1: sync, 1b: sync, 2: microtask, 3: another microtask, 3a: RAF, 4: macrotask

Event Loop Phases (Node.js / Browser)

1
■ Call Stack
Sync functions execute LIFO
2
■ Microtask Queue
Promise.then, queueMicrotask
3
■ Render
RAF, style, layout, paint
4
■ Macrotask Queue
setTimeout, setInterval, I/O

Each loop iteration: 1 macrotask → drain all microtasks → render → next macrotask.

This means setTimeout(cb, 0) does not run immediately — it waits for all microtasks to clear first. Never use setTimeout(fn, 0) to defer; use queueMicrotask() or Promise.resolve().then() instead.

2. Promises, Async Patterns & Error Handling

Promise States & Static Methods

A Promise is in one of three states: pending (initial), fulfilled (resolved), or rejected (error thrown). Static methods for coordination:

// Wait for ALL to settle (resolve or reject)
const results = await Promise.allSettled([
    fetch("/api/users"),
    fetch("/api/courses"),
    fetch("/api/grades"),
]);
// Returns [{status:"fulfilled", value}, {status:"rejected", reason}]

// First to fulfill wins (rejects only if ALL reject)
const fastest = await Promise.any([
    fetch("https://mirror1.example.com/data"),
    fetch("https://mirror2.example.com/data"),
]);

// Settle only when ALL are fulfilled; single reject rejects entire batch
const [users, courses] = await Promise.all([
    fetch("/api/users").then(r => r.json()),
    fetch("/api/courses").then(r => r.json()),
]);

Promise.withResolvers() (ES2024)

Creates a promise with external resolve and reject functions — no executor callback needed:

const { promise, resolve, reject } = Promise.withResolvers();

// Use with event-driven APIs
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = "photo.jpg";

await promise; // resolves when image loads

This simplifies converting callback-based APIs to promises, especially recurring events (streams, WebSocket messages, custom event emitters).

Async/Await Error Patterns

// Pattern 1: try/catch for expected errors
async function loadProfile(id) {
    try {
        const res = await fetch(`/api/users/${id}`);
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return await res.json();
    } catch (err) {
        console.error("Profile load failed:", err);
        return { error: true, message: err.message };
    }
}

// Pattern 2: Top-level error with .catch() (prevents unhandled rejections)
loadProfile(42).catch(err => reportError(err));

3. Performance APIs & Observable Patterns

IntersectionObserver — Lazy Loading & Infinite Scroll

Observes when an element enters or exits the viewport. Much more efficient than scroll-event listeners:

const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;    // load actual image
            img.classList.remove("lazy");
            observer.unobserve(img);       // stop observing
        }
    });
}, {
    rootMargin: "200px",  // trigger 200px before visible
    threshold: 0.01,      // at least 1% visible
});

document.querySelectorAll("img[data-src]").forEach(img => observer.observe(img));

ResizeObserver — Responsive Components

Fires when an element's size changes. Unlike window resize, it fires per-element and gives exact dimensions:

const ro = new ResizeObserver(entries => {
    for (const entry of entries) {
        const { inlineSize, blockSize } = entry.contentBoxSize[0];
        entry.target.style.fontSize = `${Math.max(0.8, inlineSize / 50)}rem`;
    }
});
ro.observe(document.querySelector(".responsive-text"));

Debounce & Throttle

Two critical patterns for rate-limiting expensive operations:

// Debounce: wait until activity stops, then fire ONCE
function debounce(fn, ms = 300) {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn(...args), ms);
    };
}
// Use: input search (wait for user to stop typing)
input.addEventListener("input", debounce(doSearch, 400));

// Throttle: fire at most once per interval
function throttle(fn, ms = 100) {
    let last = 0;
    return (...args) => {
        const now = Date.now();
        if (now - last >= ms) { last = now; fn(...args); }
    };
}
// Use: scroll position, resize handler
window.addEventListener("scroll", throttle(savePosition, 200));

Memoization

Cache expensive function results based on arguments:

function memoize(fn) {
    const cache = new Map();
    return (...args) => {
        const key = JSON.stringify(args);
        if (cache.has(key)) return cache.get(key);
        const result = fn(...args);
        cache.set(key, result);
        return result;
    };
}

const expensiveFib = memoize((n) => n <= 1 ? n : expensiveFib(n - 1) + expensiveFib(n - 2));