Web Interfaces: HTML, CSS, and JavaScript

EE 547 - Unit 7

Dr. Brandon Franzke

Fall 2025

Outline

The Browser as Application Platform

From APIs to Visual Documents

  • Server sends HTML text, not JSON data
  • Browser transforms markup into pixels
  • Rendering happens on user’s CPU (80ms vs 7ms server time)
  • Same HTTP protocol, different content types

Three-Layer Architecture

  • HTML: Document structure (what exists)
  • CSS: Visual presentation (how it looks)
  • JavaScript: Interactive behavior (what happens)

HTML Evolution

  • 1995: 20 tags for documents
  • 2014: 140 tags for applications
  • Declarative markup, not programming
  • DOM tree enables manipulation

Client-Side Execution

JavaScript in the Sandbox

  • Full programming language
  • Cannot access files, database, or OS
  • Modifies DOM without server calls

Network Reality

  • Every request costs 150-400ms
  • Distance adds latency (light speed limits)
  • Parallel requests beat sequential (3×150ms → 150ms)

Static File Serving

  • URL maps directly to file path
  • No code execution
  • Nginx bypasses application entirely
  • Flask convention: /static/* → filesystem

From APIs to Browsers

Web Browsers Render Documents

Browser requests document from server

GET /profile HTTP/1.1
Host: app.example.com
Accept: text/html

Server responds with HTML document:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 2847
<!DOCTYPE html>
<html>
<head>
  <title>User Profile</title>
</head>
<body>
  <h1>Alice Chen</h1>
  <p>Email: alice@example.com</p>
  <p>Member since January 15, 2024</p>

  <section class="activity">
    <h2>Your Activity</h2>
    <div class="stat">
      <span class="number">47</span>
      <span class="label">Total Orders</span>
    </div>
    <div class="stat">
      <span class="number">$2,847.50</span>
      <span class="label">Total Spent</span>
    </div>
  </section>

  <div class="actions">
    <a href="/orders">View Orders</a>
    <a href="/settings">Settings</a>
  </div>
</body>
</html>

HTML describes document structure using markup tags.

Browser receives this text and renders visual display on screen.

Browser transforms markup into visual document:

  • Headings render larger and bold
  • Paragraphs flow with line wrapping
  • Sections group related content
  • Links appear clickable
  • Numbers formatted for emphasis

User sees formatted document, not HTML tags.

Same HTTP protocol as APIs - different content type and purpose.

Web Pages Combine Three Technologies With Distinct Roles

HTML Markup Language

Tags describe document structure

<h1>Alice Chen</h1>

<h1> tag marks heading Browser renders “Alice Chen” as large, bold text </h1> closes the heading tag

<p>Email: alice@example.com</p>

<p> tag marks paragraph Browser renders as normal text with spacing </p> closes the paragraph

<a href="/orders">View Orders</a>

<a> tag creates link href attribute specifies destination URL Browser renders “View Orders” as clickable text Click navigates to /orders

Nesting creates hierarchy:

<section>
  <h2>Your Activity</h2>
  <p>Total Orders: 47</p>
</section>

<section> contains heading and paragraph Indentation shows structure (not required, aids readability) Browser understands parent-child relationships

Attributes configure elements:

<div class="stat">
  <span class="number">47</span>
</div>

class attribute names element for styling Multiple elements can share same class Browser applies CSS rules matching class names

HTML Evolved From Document Exchange to Application Platform

HTML evolution 1995-2025:

Structure (1995-1997)

  • Tables for data (not layout)
  • Forms for basic interaction
  • Images and links

Separation (1997-2000)

  • CSS replaces style attributes
  • <font> deprecated
  • Semantic markup emphasized

Failed strictness (2000)

  • XHTML required perfect syntax
  • One error = blank page
  • Developers rejected complexity

Pragmatic evolution (2008-2014)

  • HTML5 accepts messy reality

  • Error recovery built-in

  • New elements for applications:

    • <canvas> for graphics
    • <video>/<audio> native
    • <section>, <article> semantics

Living standard (2014+)

  • No more version numbers
  • Continuous small updates
  • Focus on web applications
  • Web Components for reusability

Backward compatibility:

  • 1995 HTML still renders
  • Progressive enhancement works
  • 30-year-old sites still function

HTML is Declarative - WHAT Not HOW

Tags describe meaning, not appearance

<h1>Alice Chen</h1>

Declares: “This is a top-level heading”

Does NOT specify:

  • Font size (32px? 48px?)
  • Font family (Arial? Times?)
  • Color (black? blue?)
  • Weight (bold? normal?)

Browser applies default heading styles.

<strong>important</strong>

Declares: “This text is important”

Does NOT specify:

  • Bold font
  • Specific weight value
  • Color change

Browser chooses emphasis method (typically bold).

<p>This is a paragraph.</p>

Declares: “This is a paragraph of text”

Does NOT specify:

  • Line spacing
  • Margins above and below
  • Text alignment
  • Line wrapping behavior

Browser applies paragraph formatting.

HTML separates structure from presentation.

Same markup renders differently:

  • Desktop browser: Large fonts, wide layout
  • Mobile browser: Smaller fonts, narrow layout
  • Print: Black text, optimized spacing
  • Screen reader: Spoken aloud, no visual rendering

Not a programming language

HTML has no:

Variables:

<!-- This doesn't exist -->
<set var="userName" value="Alice" />
<p>{{ userName }}</p>

Loops:

<!-- This doesn't exist -->
<repeat count="5">
  <p>Item</p>
</repeat>

Conditionals:

<!-- This doesn't exist -->
<if condition="user.isAdmin">
  <button>Delete</button>
</if>

Functions:

<!-- This doesn't exist -->
<function name="formatDate">
  ...
</function>

Computation:

<!-- This doesn't exist -->
<p>Total: {{ price * quantity }}</p>

HTML only describes structure.

Processing and logic happen elsewhere:

  • CSS applies styling rules
  • JavaScript performs computation
  • Server generates dynamic content

HTML itself is static document structure.

Document Object Model - Tree Structure

Browser parses HTML into tree of nodes

HTML text:

<div id="profile">
  <h1>Alice Chen</h1>
  <p>Email: alice@example.com</p>
  <section>
    <h2>Activity</h2>
    <p>Orders: 47</p>
  </section>
</div>

Browser builds DOM tree:

div (id="profile")
├── h1
│   └── "Alice Chen"
├── p
│   └── "Email: alice@example.com"
└── section
    ├── h2
    │   └── "Activity"
    └── p
        └── "Orders: 47"

Each tag becomes node in tree Text becomes leaf nodes Parent-child relationships preserved from nesting

DOM allows programmatic access:

JavaScript can traverse and modify tree:

// Get element by ID
let profile = document.getElementById('profile');

// Get child elements
let heading = profile.querySelector('h1');

// Modify content
heading.textContent = 'Alice C. Chen';

// Add new element
let newPara = document.createElement('p');
newPara.textContent = 'Phone: 555-0123';
profile.appendChild(newPara);

Changes to DOM immediately reflected in display.

Tree structure enables:

Navigation between elements:

  • Parent node contains children
  • Children reference parent
  • Siblings at same level

Styling rules:

  • CSS matches elements by tag, class, or ID
  • Cascade applies from parent to children
  • Inheritance passes properties down tree

JavaScript manipulation:

  • Traverse tree to find elements
  • Add or remove nodes
  • Modify attributes and content
  • Attach event handlers to nodes

Browser maintains this tree in memory while page displayed.

CSS Applies Presentation

Stylesheet defines visual properties

HTML without styling:

<h1>Alice Chen</h1>
<p>Email: alice@example.com</p>

Browser applies default styles:

  • <h1> renders 32px, bold, Times New Roman
  • <p> renders 16px, normal weight, Times New Roman
  • Black text on white background

CSS overrides defaults:

h1 {
  font-size: 48px;
  color: #2c3e50;
  font-family: Arial, sans-serif;
  margin-bottom: 20px;
}

p {
  font-size: 18px;
  color: #34495e;
  line-height: 1.6;
}

Browser applies these rules to matching elements.

Class selectors target specific elements:

<div class="stat-card">
  <span class="number">47</span>
  <span class="label">Orders</span>
</div>
.stat-card {
  background-color: #f8f9fa;
  border-radius: 8px;
  padding: 20px;
}

.number {
  font-size: 36px;
  font-weight: bold;
  color: #3498db;
}

.label {
  font-size: 14px;
  color: #7f8c8d;
}

Elements with matching class names receive these styles.

CSS separates content from presentation:

Same HTML:

<h1>Alice Chen</h1>

Different stylesheets produce different appearance:

  • Desktop: Large heading, serif font
  • Mobile: Smaller heading, sans-serif
  • Print: Black text, minimal styling
  • Dark mode: Light text on dark background

Content unchanged - only presentation differs.

Cascade and inheritance:

body {
  font-family: Arial, sans-serif;
  color: #333;
}

.special {
  color: blue;
}

Paragraph inherits font from body Element with class="special" overrides color

Rules cascade from general to specific.

Client-Side Rendering

Browser transforms HTML into pixels on user’s machine

Server sends HTML text:

<h1>Welcome, Alice Chen</h1>
<p>Total Orders: 47</p>

Browser operations (client-side):

Parse HTML into DOM tree Apply CSS to calculate styles Compute layout (positions and sizes) Paint pixels to screen

All rendering happens on user’s computer, not server.

Server responsibility:

@app.route('/profile/<int:user_id>')
def show_profile(user_id):
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)

    html = f"""
    <h1>Welcome, {user.name}</h1>
    <p>Total Orders: {user.order_count}</p>
    """

    return html

Server queries database and generates HTML text (7ms) Server sends HTML to client Server done - rendering is client’s responsibility

Client responsibility:

Browser receives HTML text Parses into DOM Applies styles Calculates layout for screen size Renders pixels

Total client rendering time: 80ms on user’s CPU

Server handles 1000 users: 7 seconds total CPU Clients handle 1000 renderings: Distributed across 1000 machines

Rendering distribution:

1000 concurrent users viewing profiles

Server work:

  • 1000 HTML generations
  • 7ms each
  • Total: 7 seconds of server CPU

Client work:

  • 1000 renderings
  • 80ms each
  • Happens on 1000 different CPUs simultaneously
  • No server load

Each user’s browser uses their own CPU for rendering.

Server only generates markup - does not render pixels.

Same Protocol - Different Content Types

HTTP serves both documents and data

API request for data:

GET /users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGci...

Server returns structured data:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 156
{
  "user_id": 123,
  "email": "alice@example.com",
  "order_count": 47,
  "total_spent": 2847.50
}

Service consumes data programmatically.

Browser request for document:

GET /users/123 HTTP/1.1
Host: app.example.com
Accept: text/html
Cookie: session_id=abc123

Server returns marked-up document:

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 2847
<h1>Alice Chen</h1>
<p>Email: alice@example.com</p>
<p>Total Orders: 47</p>
<p>Total Spent: $2,847.50</p>

Browser renders document visually.

Both use HTTP - Content-Type header determines interpretation.

Flask serving both:

@app.route('/users/<int:user_id>')
def get_user(user_id):
    user = db.query(
        "SELECT * FROM users WHERE id = ?",
        user_id
    )

    accept = request.headers.get('Accept', '')

    if 'application/json' in accept:
        return {
            'user_id': user.id,
            'email': user.email,
            'order_count': user.orders
        }
    else:
        return render_template(
            'profile.html',
            user=user
        )

Same database query Different response format based on Accept header

APIs and browsers share HTTP infrastructure - serve different purposes.

Execution Boundary - Server vs Client

Code runs in two separate environments

Server-side (before response sent):

@app.route('/dashboard')
def dashboard():
    # Runs on server
    user_id = session.get('user_id')

    # Server accesses database
    user = db.query(
        "SELECT * FROM users WHERE id = ?",
        user_id
    )

    orders = db.query(
        "SELECT * FROM orders WHERE user_id = ?",
        user_id
    )

    # Server has secrets
    stripe_key = os.environ['STRIPE_SECRET_KEY']

    # Generate HTML
    html = render_template(
        'dashboard.html',
        user=user,
        orders=orders
    )

    return html
    # Server execution ends

Browser receives:

<h1>Alice Chen</h1>
<p>Orders: 47</p>

Client-side (after HTML received):

<script>
// Runs in browser
document.getElementById('refresh')
    .addEventListener('click', function() {
        // Browser makes HTTP request
        fetch('/api/orders')
            .then(r => r.json())
            .then(data => {
                // Update page
                updateOrderList(data);
            });
    });
</script>

JavaScript executes in browser - different environment than server Python.

Server environment:

Accesses database directly Reads files from disk Has environment variables with secrets Calls other backend services Stores session data

Client environment:

Modifies DOM elements Responds to user events (clicks, typing) Stores data in browser (localStorage, cookies) Cannot access database Cannot read server files Cannot access API keys

Communication only through HTTP requests.

Security Boundary

Client code and data visible to user

HTML and JavaScript sent to browser:

<script>
function submitOrder() {
    let price = document.getElementById('price').value;

    // Client-side validation
    if (price < 100) {
        alert('Minimum order $100');
        return;
    }

    // Submit to server
    fetch('/api/orders', {
        method: 'POST',
        body: JSON.stringify({price: price})
    });
}
</script>

User opens DevTools console and bypasses validation:

// Modify price directly
document.getElementById('price').value = 0.01;

// Call submit function
submitOrder();

// Or make request directly
fetch('/api/orders', {
    method: 'POST',
    body: JSON.stringify({price: 0.01})
});

Order submitted at $0.01 because client validation bypassed.

Server must enforce rules:

@app.route('/api/orders', methods=['POST'])
def create_order():
    price = request.json['price']

    # Server validation required
    if price < 100:
        return {'error': 'Minimum order $100'}, 400

    order = db.insert('orders', price=price)
    return {'order_id': order.id}, 201

Server validation cannot be bypassed - code not visible to user.

User can view all client-side code:

Browser View Source shows HTML and JavaScript DevTools shows network requests Console allows code execution Elements tab shows DOM structure

User cannot view server code:

Python code stays on server Database queries never sent to client Environment variables inaccessible Business logic hidden

Client-side validation improves user experience:

  • Immediate feedback on errors
  • Reduces unnecessary requests
  • Guides user input

Server-side validation enforces security:

  • Cannot be bypassed
  • Protects data integrity
  • Required for all operations

Both serve different purposes - client for usability, server for security.

Network Crossing Cost

Each HTTP request crosses network boundary

Local function call (same process):

def calculate_total(items):
    return sum(item.price for item in items)

total = calculate_total(cart_items)
# Execution: 0.00001ms

Function executes in process memory.

HTTP request to server:

fetch('/api/cart/total')
    .then(response => response.json())
    .then(data => displayTotal(data.total));

Request components:

  • DNS lookup: 20-120ms (if not cached)
  • TCP connection: 50-100ms
  • TLS handshake: 50-100ms (HTTPS)
  • Server processing: 10-50ms
  • Network transfer: 1ms each direction

Total: 150-400ms depending on distance and caching

Geographic distance:

  • Same datacenter: 1-5ms round-trip
  • Same city: 10-20ms
  • Same continent: 50-100ms
  • Cross-continent: 150-250ms
  • Transoceanic: 250-400ms

Round-trip time accumulates with each request.

Network crossing unavoidable:

Physical distance limits speed of light in fiber Signal must travel to server and back Cannot eliminate latency

Design implications:

Minimize number of requests Combine related data in single response Cache responses when appropriate Use connection reuse (HTTP/2)

Single request returning combined data:

@app.route('/api/checkout-data')
def checkout_data():
    return {
        'user': get_user(),
        'cart': get_cart(),
        'shipping': get_shipping(),
        'tax': calculate_tax()
    }

One request with all data vs four separate requests.

Multiple Requests

Sequential requests accumulate latency

Checkout page loading data sequentially:

// Get user first
fetch('/api/user')
    .then(r => r.json())
    .then(user => {
        displayUser(user);

        // Then get cart (waits for user)
        return fetch('/api/cart');
    })
    .then(r => r.json())
    .then(cart => {
        displayCart(cart);

        // Then shipping (waits for cart)
        return fetch('/api/shipping');
    })
    .then(r => r.json())
    .then(shipping => {
        displayShipping(shipping);
    });

Each request waits for previous to complete:

  • Request 1: 150ms
  • Request 2 starts, completes: 150ms
  • Request 3 starts, completes: 150ms

Total: 450ms (3 × 150ms)

Parallel requests start simultaneously:

Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/cart').then(r => r.json()),
    fetch('/api/shipping').then(r => r.json())
])
.then(([user, cart, shipping]) => {
    displayUser(user);
    displayCart(cart);
    displayShipping(shipping);
});

All three requests start at time 0:

  • All complete at 150ms

Total: 150ms

Parallel requests when data independent.

Dependencies require sequencing:

Some requests depend on previous responses:

// Must be sequential
fetch('/api/user')
    .then(r => r.json())
    .then(user => {
        // Need user.id for cart
        return fetch(`/api/cart?user_id=${user.id}`);
    });

Second request needs data from first.

Server-side combination:

Instead of client making multiple requests:

// Single request
fetch('/api/checkout-data')
    .then(r => r.json())
    .then(data => {
        displayUser(data.user);
        displayCart(data.cart);
        displayShipping(data.shipping);
    });

Server endpoint combines data:

@app.route('/api/checkout-data')
def checkout_data():
    user = get_user()
    cart = get_cart(user.id)
    shipping = get_shipping(user.zip)

    return {
        'user': user,
        'cart': cart,
        'shipping': shipping
    }

One client request Server makes internal queries (no network latency) Returns combined response

HTML - Structure and Meaning

Markup vs Programming Languages

HTML (Markup)

<article>
  <h1>Weather Report</h1>
  <p>Today: Sunny, 75°F</p>
  <ul>
    <li>Morning: 65°F</li>
    <li>Afternoon: 75°F</li>
    <li>Evening: 70°F</li>
  </ul>
</article>

Describes WHAT:

  • Article container
  • Heading level 1
  • Paragraph of text
  • Unordered list
  • No logic or computation
  • No control flow

Python (Programming)

def weather_report(temp):
    if temp > 70:
        status = "Warm"
    else:
        status = "Cool"

    forecast = []
    for hour in range(24):
        forecast.append(
            calculate_temp(hour)
        )

    return {
        "status": status,
        "temps": forecast
    }

Describes HOW:

  • Functions execute
  • Conditions branch
  • Loops iterate
  • Variables store
  • Logic processes
  • Returns results

HTML declares structure. CSS styles it. JavaScript adds behavior.

HTML alone cannot: validate input, calculate values, make decisions, or modify itself.

This separation is by design - each technology has a specific role.

HTML Document Anatomy

Tags, Elements, and Attributes

Multiple attributes example:

<a href="https://example.com"
   target="_blank"
   title="Visit our site"
   class="external-link">
  Click here
</a>

Each attribute:

  • Has a name and value
  • Separated by spaces
  • Order doesn’t matter
  • Values in quotes (single or double)

Common global attributes:

id="unique-name"

  • Must be unique on page
  • Used for JavaScript/CSS targeting
  • Fragment navigation (#section)

class="style-name"

  • Can be reused
  • Multiple classes allowed
  • CSS styling hook

data-*="custom"

  • Store custom data
  • JavaScript access
  • No visual effect

Boolean attributes:

<input type="checkbox" checked>
<button disabled>Can't Click</button>
<input required>

Presence = true, absence = false

Block vs Inline - Different Layout Behaviors

Default display behavior:

<!-- Block elements -->
<h1>Title</h1>
<p>First paragraph.</p>
<p>Second paragraph.</p>

<!-- Renders as: -->
Title
First paragraph.
Second paragraph.

Each block starts a new line.

<!-- Inline elements -->
<p>
  Text with <strong>bold</strong>
  and <a href="#">link</a> inside.
</p>

<!-- Renders as: -->
Text with bold and link inside.

Inline elements flow with text.

Layout implications:

Block elements:

  • Cannot place side-by-side without CSS
  • Height determined by content
  • Width fills parent container
  • Create document structure

Inline elements:

  • Cannot set width/height (except img, input)
  • Padding/margin behaves differently
  • Cannot contain block elements
  • Flow within text content

Invalid nesting:

<!-- WRONG: block inside inline -->
<span>
  <div>Content</div>
</span>

<!-- CORRECT: inline inside block -->
<div>
  <span>Content</span>
</div>

Character Encoding Matters

Complete declaration:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>International Site</title>
</head>
<body>
  <h1>Café München</h1>
  <p>Price: 50€</p>
  <p>北京 • москва • القاهرة</p>
  <p>Emoji: 🎉 🚀 ❤️</p>
</body>
</html>

UTF-8 Coverage:

  • ASCII: 1 byte (a, b, c)
  • Latin: 2 bytes (é, ñ, ü)
  • Asian: 3 bytes (中, 日, 한)
  • Emoji: 4 bytes (😀, 🎯)

Server should also send:

Content-Type: text/html; charset=UTF-8

Meta tag provides fallback when:

  • Server header missing
  • File opened locally
  • Saved for offline viewing

Without charset declaration, browsers guess encoding based on:

  • User’s system locale
  • Content patterns
  • Often wrong for international content

Character Encoding - Why It Matters

Characters require encoding to bytes

Same character “é” (U+00E1):

UTF-8:      C3 A9  (2 bytes)
UTF-16:     00 E1  (2 bytes)
ISO-8859-1: E1     (1 byte)

Code point (what) ≠ byte representation (how)

UTF-8 dominates (97% of web):

Variable length: 1-4 bytes per character

  • 1 byte: ASCII (0-127) a → 61

  • 2 bytes: Latin, Greek, Cyrillic (128-2047) é → C3 A9

  • 3 bytes: CJK, most common scripts (2048-65535) 中 → E4 B8 AD

  • 4 bytes: Emoji, rare scripts (65536+) 😀 → F0 9F 98 80

Continuation bytes start with 10 (self-synchronizing)

Backward compatible with ASCII

HTML declaration required:

<meta charset="UTF-8">

Wrong encoding = mojibake:

File saved UTF-8, browser reads Latin-1:

"Café" → "Café"

File saved Latin-1, browser reads UTF-8:

Shows: �

Common failure points:

Database connections, form submissions, file storage—all must use matching encoding.

-- Database uses utf8mb4
-- Connection defaults to latin1
-- Data corrupts on write
SET NAMES utf8mb4;

Common mojibake patterns:

Original Wrong Display Cause
Café Café UTF-8 as Latin-1
“smart” “smart†Windows-1252 as UTF-8
北京 ?????? Lost in conversion

Always declare encoding explicitly:

  1. HTTP Content-Type header
  2. <meta charset="UTF-8"> in first 1024 bytes
  3. Database connection encoding
  4. Form accept-charset attribute

HTML Character Entities

HTML uses <, >, & as syntax

To display these in content, use entities.

Reserved characters:

Character Entity Purpose
& &amp; Entity/reference start
< &lt; Tag start
> &gt; Tag end
" &quot; Attribute delimiter

Common symbols:

Symbol Entity Name
© &copy; Copyright
&euro; Euro
&mdash; Em dash
(space) &nbsp; Non-breaking space

Numeric references (any Unicode):

&#225;   <!-- Decimal: code point 225 -->
&#xE1;   <!-- Hexadecimal: code point 0xE1 -->

Both render: á

Non-breaking space:

&nbsp; prevents line break between words.

Price:&nbsp;$50

Keeps “Price:” and “$50” together.

Code examples need entities:

<pre>
&lt;script&gt;
  if (x &lt; 10) {
    console.log("x &lt; 10");
  }
&lt;/script&gt;
</pre>

Without entities, <script> would execute as JavaScript.

Use entities when:

  • Displaying code examples in HTML
  • Showing reserved characters
  • Ensuring characters don’t break parsing
  • Preventing line breaks (&nbsp;)

Semantic HTML - Meaning Over Presentation

Non-Semantic (Generic)

<div class="header">
  <div class="title">News</div>
</div>

<div class="nav">
  <div>Home</div>
  <div>About</div>
</div>

<div class="content">
  <div class="post">
    <div class="post-title">
      Article Title
    </div>
    <div>Content...</div>
  </div>
</div>

<div class="footer">
  © 2025
</div>

Problems:

  • No semantic meaning
  • Poor accessibility
  • SEO unfriendly

Semantic (HTML5)

<header>
  <h1>News</h1>
</header>

<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
</nav>

<main>
  <article>
    <h2>
      Article Title
    </h2>
    <p>Content...</p>
  </article>
</main>

<footer>
  © 2025
</footer>

Benefits:

  • Clear document structure
  • Screen reader friendly
  • Better SEO ranking

Semantic elements describe their purpose, not appearance.

Screen readers announce “navigation” for <nav>, “main content” for <main>.

Search engines understand page structure from semantic tags.

HTML5 Semantic Elements - Structure with Meaning

Semantic elements (HTML5):

Each tag describes its content purpose:

<header> - Introductory content <nav> - Navigation links <main> - Primary page content <article> - Self-contained content <section> - Thematic grouping <aside> - Tangential content <footer> - Footer information <figure> - Illustration with caption <figcaption> - Caption for figure <details> - Collapsible content <summary> - Details heading <mark> - Highlighted text <time> - Date/time data <address> - Contact information

Before HTML5 (div soup):

<div class="header">
  <div class="nav">...</div>
</div>
<div class="content">
  <div class="article">...</div>
  <div class="sidebar">...</div>
</div>
<div class="footer">...</div>

With HTML5 semantics:

<header>
  <nav>...</nav>
</header>
<main>
  <article>...</article>
  <aside>...</aside>
</main>
<footer>...</footer>

Benefits:

  • Search engines understand structure
  • Screen readers navigate better
  • Code self-documents purpose
  • CSS targeting more meaningful
  • HTML validators check nesting rules

Semantic = meaning, not style

DOM Tree Structure

Nesting creates hierarchy:

<article>
  <header>
    <h2>Article Title</h2>
    <time>Jan 15, 2025</time>
  </header>
  <section>
    <p>First paragraph...</p>
    <p>Second paragraph...</p>
  </section>
  <footer>
    <a href="/share">Share</a>
  </footer>
</article>

Tree relationships:

  • Parent: article contains all
  • Children: header, section, footer
  • Siblings: paragraphs at same level
  • Descendants: All nested elements

JavaScript access:

// Get element
let article =
  document.querySelector('article');

// Navigate tree
let title =
  article.querySelector('h2');

// Modify content
title.textContent = 'New Title';

// Add element
let newPara =
  document.createElement('p');
article.appendChild(newPara);

Browser maintains tree in memory. Changes immediately update display.

Text Hierarchy with Headings

Semantic importance, not size:

<h1>Research Paper</h1>
<p>Introduction text...</p>

<h2>Methodology</h2>
<p>We conducted...</p>

<h3>Data Collection</h3>
<p>Samples were...</p>

<h3>Analysis Method</h3>
<p>Statistical...</p>

<h2>Results</h2>
<p>Our findings...</p>

Screen reader navigation:

User hears: “Heading level 1: Research Paper” “Heading level 2: Methodology” “Heading level 3: Data Collection”

Can jump between headings with keyboard.

SEO impact:

Search engines:

  • h1 = primary topic
  • h2 = major sections
  • h3-h6 = supporting structure

Weight content by heading level.

Common mistakes:

<!-- WRONG: Using for size -->
<h1>Welcome</h1>
<h3>Subtitle text</h3>

<!-- RIGHT: Proper hierarchy -->
<h1>Welcome</h1>
<h2>Subtitle text</h2>

CSS controls size, HTML provides structure.

Lists - Ordered, Unordered, and Nested

Unordered list (no sequence):

  • Neural Networks
  • Decision Trees
  • Support Vector Machines
  • Random Forests
<ul>
  <li>Neural Networks</li>
  <li>Decision Trees</li>
  <li>Support Vector Machines</li>
  <li>Random Forests</li>
</ul>

Ordered list (sequence matters):

  1. Collect training data
  2. Preprocess and clean
  3. Split train/validation/test
  4. Train model
  5. Evaluate performance
<ol>
  <li>Collect training data</li>
  <li>Preprocess and clean</li>
  <li>Split train/validation/test</li>
  <li>Train model</li>
  <li>Evaluate performance</li>
</ol>

Nested lists (hierarchy):

  • Frontend
    • HTML
    • CSS
    • JavaScript
      • React
      • Vue
  • Backend
    • Python
    • Node.js
<ul>
  <li>Frontend
    <ul>
      <li>HTML</li>
      <li>CSS</li>
      <li>JavaScript
        <ul>
          <li>React</li>
          <li>Vue</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Backend
    <ul>
      <li>Python</li>
      <li>Node.js</li>
    </ul>
  </li>
</ul>

Browser changes bullet style per level. Can mix ordered and unordered.

Images with Purpose

Alt text purposes:

  1. Accessibility

    • Screen readers speak alt text
    • Describes image for blind users
  2. Failed loads

    • Shown when image doesn’t load
    • Network issues or 404 errors
  3. SEO value

    • Search engines index alt text
    • Helps image search ranking

Responsive images:

<img src="photo.jpg"
     srcset="photo-400.jpg 400w,
             photo-800.jpg 800w,
             photo-1200.jpg 1200w"
     sizes="(max-width: 600px) 100vw,
            (max-width: 1000px) 50vw,
            800px"
     alt="Product photo">

Browser chooses appropriate size:

  • Mobile: 400px version
  • Tablet: 800px version
  • Desktop: 1200px version

Common formats:

  • JPEG: Photos, gradients
  • PNG: Logos, transparency
  • WebP: Modern, smaller files
  • SVG: Scalable graphics

Container Elements - Structure Without Semantics

Tables for Tabular Data

Model Performance Comparison
Model Accuracy Speed
ResNet-50 94.2% 22ms
BERT 91.8% 45ms
GPT-2 93.5% 67ms

Complete structure:

<table>
  <caption>
    Model Performance
  </caption>
  <thead>
    <tr>
      <th>Model</th>
      <th>Accuracy</th>
      <th>Speed</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ResNet-50</td>
      <td>94.2%</td>
      <td>22ms</td>
    </tr>
  </tbody>
</table>

Table elements:

<table> - Container <caption> - Table title <thead> - Header section <tbody> - Body section <tfoot> - Footer section <tr> - Table row <th> - Header cell <td> - Data cell

Appropriate table usage:

  • Comparing data
  • Showing relationships
  • Displaying results
  • Structured information

Inappropriate table usage:

  • Page layout (use CSS)
  • Non-tabular lists
  • Visual alignment only

Accessibility:

  • Caption describes table
  • Headers provide context
  • Screen readers announce structure

Spanning cells:

<td colspan="2">
  Spans 2 columns
</td>
<td rowspan="3">
  Spans 3 rows
</td>

Creates merged cells for complex headers or grouped data.

IFrames - Embedding Pages and Security Boundaries

IFrame embeds another HTML page:

<iframe src="https://example.com/widget"
        width="600" height="450">
</iframe>

Browser makes HTTP request to src URL and renders the result inside a rectangle on the current page.

Common uses: Maps, videos (YouTube), payment forms (Stripe), third-party widgets

Security: Same-origin policy

Origin = protocol + domain + port

Different origins cannot access each other:

Parent (example.com):

<iframe id="map" src="https://maps.google.com/embed">
</iframe>

<script>
// BLOCKED - different origin
iframe.contentDocument.getElementById('map');
// SecurityError: Blocked
</script>

IFrame (maps.google.com):

// BLOCKED - different origin
parent.document.getElementById('header');
// SecurityError: Blocked

Same origin can access:

Parent (example.com):

<iframe src="/widget.html"></iframe>

<script>
// ALLOWED - same origin
iframe.contentDocument.getElementById('btn');
</script>

Origin comparison:

  • https://example.com:443http://example.com:80 (protocol)
  • https://example.comhttps://www.example.com (domain)
  • https://example.comhttps://example.com:8080 (port)

PostMessage enables controlled cross-origin communication

Parent sends message:

iframe.contentWindow.postMessage(
  {action: 'resize', height: 400},
  'https://widget.com' // Target origin
);

IFrame receives:

window.addEventListener('message', (event) => {
  // MUST verify sender origin
  if (event.origin !== 'https://parent.com') {
    return; // Ignore untrusted messages
  }

  if (event.data.action === 'resize') {
    // Handle message
  }
});

Always verify event.origin before processing messages.

IFrame Sandbox - Restricting Capabilities

Sandbox attribute restricts iframe capabilities

No sandbox (default - full permissions):

<iframe src="widget.html"></iframe>

Default IFrame permissions:

  • Execute JavaScript
  • Submit forms
  • Open popups
  • Navigate parent
  • Access parent (if same origin)

Full sandbox (maximum restriction):

<iframe src="widget.html" sandbox=""></iframe>

Sandboxed IFrame restrictions:

  • No JavaScript execution
  • No form submission
  • No popups
  • No parent navigation
  • Treated as unique origin (even if same domain)

Selective permissions:

<iframe src="widget.html"
        sandbox="allow-scripts allow-forms">
</iframe>

Enables only JavaScript and form submission.

Real-world examples:

Payment form (needs scripts, forms, origin):

<iframe src="https://stripe.com/checkout"
        sandbox="allow-scripts
                 allow-forms
                 allow-same-origin">
</iframe>

Third-party widget (scripts only, isolated):

<iframe src="https://widget.com/embed"
        sandbox="allow-scripts">
</iframe>

User-generated content (display only):

<iframe src="user-content.html"
        sandbox="">
</iframe>

Multiple permissions:

<iframe sandbox="allow-scripts
                 allow-same-origin
                 allow-forms">
</iframe>

Space-separated list enables selected capabilities.

HTML Forms - User Input

Form Submission Flow

Text Input Types

**Basic text:** **Email (validates format):** **Password (masked):** **Number (with constraints):** **URL (validates format):** **Search (with clear button):** **Date picker:** **Color picker:**
<input type="text"
       name="username"
       placeholder="Enter username"
       required>

<input type="email"
       name="email"
       placeholder="user@example.com"
       required>

<input type="password"
       name="password"
       placeholder="Password"
       minlength="8">

<input type="number"
       name="age"
       min="1"
       max="120"
       step="1">

<input type="url"
       name="website"
       placeholder="https://...">

<input type="search"
       name="query"
       placeholder="Search...">

<input type="date"
       name="birthday"
       max="2025-12-31">

<input type="color"
       name="theme"
       value="#3498db">

Input attributes:

  • name attribute required for submission
  • type determines validation and keyboard
  • Mobile keyboards adapt to type
  • Browser validates before submit

Selection Controls

Radio buttons (exclusive):



Checkboxes (multiple):



Select dropdown:

Multiple select:

Hold Ctrl/Cmd to select multiple

Radio button group:

<input type="radio"
       name="plan"
       value="basic"
       checked>
<label>Basic</label>

<input type="radio"
       name="plan"
       value="pro">
<label>Pro</label>

Same name = mutually exclusive. Only one value submitted.

Checkbox group:

<input type="checkbox"
       name="features"
       value="api"
       checked>
<label>API Access</label>

<input type="checkbox"
       name="features"
       value="support">
<label>Support</label>

Same name = array of values. Unchecked = not submitted.

Dropdown with groups:

<select name="timezone">
  <optgroup label="Americas">
    <option value="est">EST</option>
    <option value="pst">PST</option>
  </optgroup>
  <optgroup label="Europe">
    <option value="gmt">GMT</option>
    <option value="cet">CET</option>
  </optgroup>
</select>

Form Data Encoding

Textarea and Labels

Textarea for multi-line input:

Line breaks preserved in submission

Proper label association:

Notification Preferences

Textarea syntax:

<textarea name="comments"
          rows="5"
          cols="40"
          maxlength="500"
          placeholder="Enter text...">
  Default content here
</textarea>
  • Content between tags (not value attr)
  • Preserves newlines and spaces
  • Can set rows/cols or use CSS
  • Resizable by default

Label benefits:

<!-- Method 1: for/id -->
<label for="email">
  Email Address:
</label>
<input type="email"
       id="email"
       name="email">

<!-- Method 2: Wrapping -->
<label>
  <input type="checkbox"
         name="subscribe">
  Subscribe to newsletter
</label>

Label benefits:

  • Click label to focus input
  • Screen readers announce association
  • Better UX on mobile
  • Required for accessibility

Fieldset groups related inputs:

<fieldset>
  <legend>Shipping Address</legend>
  <input name="street">
  <input name="city">
  <input name="zip">
</fieldset>

Visual and semantic grouping.

Form Validation - Client Side

Try submitting this form:

Note: Browser validates before submission. Try invalid inputs to see error messages.

Validation attributes:

<!-- Required field -->
<input type="email"
       required>

<!-- Length constraints -->
<input type="text"
       minlength="3"
       maxlength="50">

<!-- Number range -->
<input type="number"
       min="0"
       max="100"
       step="5">

<!-- Pattern matching -->
<input type="text"
       pattern="[A-Z]{2,4}"
       title="2-4 uppercase letters">

<!-- Custom message -->
<input type="email"
       required
       oninvalid="this.setCustomValidity('Please enter valid email')"
       oninput="this.setCustomValidity('')">

Validation happens:

  1. On form submit
  2. When field loses focus
  3. As user types (some browsers)

CSS pseudo-classes:

input:valid {
  border-color: green;
}

input:invalid {
  border-color: red;
}

input:required {
  border-left: 3px solid blue;
}

Visual feedback for validation state.

Why Client Validation Isn’t Enough

Server must validate everything:

@app.route('/register', methods=['POST'])
def register():
    email = request.form['email']
    age = request.form['age']

    # Server validation (required!)
    if not '@' in email:
        return "Invalid email", 400

    if int(age) < 13 or int(age) > 120:
        return "Invalid age", 400

    # Only now safe to process
    create_user(email, age)

CSS - Presentation Layer

Browser Default Rendering

<!DOCTYPE html>
<html>
<head>
  <title>Event Registration</title>
</head>
<body>
  <h1>Tech Conference 2025</h1>
  <p>Join us for three days of talks and workshops.</p>

  <h2>Event Details</h2>
  <p>Date: March 15-17, 2025</p>
  <p>Location: Convention Center</p>
  <p>Price: $299</p>

  <h2>Register Now</h2>
  <p>Fill out the form below:</p>
  <form>
    <p>Name: <input type="text" name="name"></p>
    <p>Email: <input type="email" name="email"></p>
    <p><button type="submit">Register</button></p>
  </form>
</body>
</html>

No CSS provided - browser uses built-in defaults.

Tech Conference 2025

Join us for three days of talks and workshops.

Event Details

Date: March 15-17, 2025

Location: Convention Center

Price: $299

Register Now

Fill out the form below:

Name:

Email:

Browser defaults:

  • Times New Roman font
  • Black text, white background
  • Standard margins between elements
  • Blue underlined links
  • Basic form controls

Functional but generic - every site looks identical.

Same HTML with CSS Applied

body {
  font-family: -apple-system, BlinkMacSystemFont,
               'Segoe UI', Arial, sans-serif;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 40px;
  margin: 0;
}

h1 {
  font-size: 48px;
  text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
  margin-bottom: 10px;
}

h2 {
  border-bottom: 3px solid rgba(255,255,255,0.5);
  padding-bottom: 10px;
  margin-top: 30px;
}

form {
  background: rgba(255,255,255,0.1);
  padding: 30px;
  border-radius: 10px;
  backdrop-filter: blur(10px);
}

input {
  width: 100%;
  padding: 12px;
  border: none;
  border-radius: 5px;
  margin-top: 5px;
  font-size: 16px;
}

button {
  background: white;
  color: #667eea;
  padding: 15px 40px;
  border: none;
  border-radius: 25px;
  font-size: 18px;
  font-weight: bold;
  cursor: pointer;
  transition: transform 0.2s;
}

button:hover {
  transform: scale(1.05);
}

Tech Conference 2025

Join us for three days of talks and workshops.

Event Details

Date: March 15-17, 2025

Location: Convention Center

Price: $299

Register Now

Fill out the form below:

Name:

Email:

Same HTML structure:

  • Headings still h1, h2
  • Paragraphs still p
  • Form still has same inputs

CSS transforms presentation:

  • Modern font stack
  • Gradient background
  • Custom spacing
  • Styled form with glass effect
  • Interactive button hover

Content identical - only appearance changed.

Why Separation Matters

CSS Rule Structure

CSS connects to HTML through selectors:

HTML elements:

<h1>Page Title</h1>
<p>First paragraph text.</p>
<p>Second paragraph text.</p>

CSS rules:

h1 {
  color: #2c3e50;
  font-size: 32px;
  margin-bottom: 20px;
}

p {
  color: #555;
  line-height: 1.6;
}

Browser matches:

  1. Finds all <h1> elements
  2. Applies h1 rule properties
  3. Finds all <p> elements
  4. Applies p rule properties

Both paragraphs get same styling - selector matches element type.

Comments for documentation:

/* Navigation styles */
nav {
  background: white;
  /* padding: 20px; - removed */
  padding: 15px;
}

/* comment */ ignored by browser.

Where CSS Lives

External stylesheet (recommended):

HTML file:

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="styles.css">
  <title>My Page</title>
</head>
<body>
  <h1>Welcome</h1>
</body>
</html>

styles.css file:

h1 {
  color: blue;
  font-size: 32px;
}

Benefits:

  • Separate files (separation of concerns)
  • Browser caches CSS file
  • One CSS file serves many HTML pages
  • Easy to maintain and update

Internal stylesheet:

<head>
  <style>
    h1 {
      color: blue;
      font-size: 32px;
    }
  </style>
</head>

CSS in <style> tag within HTML.

  • Quick prototyping
  • Page-specific styles
  • No separate file needed
  • Cannot be cached separately

Inline styles (avoid):

<h1 style="color: blue; font-size: 32px;">
  Welcome
</h1>

<p style="color: gray; line-height: 1.6;">
  Paragraph with inline styles.
</p>

Style attribute on each element.

Problems:

  • No separation of concerns
  • Cannot reuse styles
  • Hard to maintain
  • Highest specificity (overrides everything)
  • Must edit HTML to change appearance

Loading order matters:

<head>
  <!-- External first -->
  <link rel="stylesheet" href="base.css">

  <!-- Then internal -->
  <style>
    h1 { color: red; }  /* Overrides base.css */
  </style>
</head>

<body>
  <!-- Inline highest priority -->
  <h1 style="color: green;">Title</h1>
</body>

Later styles override earlier ones. Inline > Internal > External

External stylesheets cache across pages. Inline styles cannot be cached and mix structure with presentation.

CSS Selectors

Element Selectors Match Tag Names

Element selectors target HTML tags directly:

h1 {
  color: blue;
  font-size: 32px;
}

p {
  color: gray;
  line-height: 1.6;
}

a {
  color: green;
  text-decoration: none;
}

HTML:

<h1>Welcome</h1>
<p>First paragraph.</p>
<p>Second paragraph.</p>
<a href="/more">Read more</a>

Result:

  • h1 rule → ALL h1 elements turn blue
  • p rule → BOTH paragraphs turn gray
  • a rule → ALL links turn green

Universal selector targets everything:

* {
  margin: 0;
  padding: 0;
}

Asterisk matches every single element on page.

Resets all default spacing - common in CSS reset stylesheets.

Class Selectors for Reusable Styles

Classes identified by period (.):

CSS:

.highlight {
  background: yellow;
}

.important {
  font-weight: bold;
  color: red;
}

.hidden {
  display: none;
}

HTML:

<p class="highlight">
  Highlighted paragraph
</p>

<span class="important">
  Critical information
</span>

<div class="highlight important">
  Both classes applied
</div>

<section class="hidden">
  Not displayed
</section>

Class advantages:

  • Apply to any element type
  • Reuse across multiple elements
  • Combine multiple classes
  • Semantic naming

Class naming conventions:

  • Use descriptive names
  • Lowercase with hyphens
  • .error-message not .red-text
  • Name by purpose, not appearance

ID Selectors for Unique Elements

IDs identified by hash (#):

CSS:

#header {
  background: navy;
  padding: 20px;
}

#sidebar {
  width: 300px;
  float: left;
}

#main-content {
  margin-left: 320px;
}

#footer {
  background: gray;
  text-align: center;
}

HTML:

<header id="header">
  <h1>Site Title</h1>
</header>

<aside id="sidebar">
  <nav>...</nav>
</aside>

<main id="main-content">
  <p>Page content...</p>
</main>

<footer id="footer">
  <p>Copyright 2025</p>
</footer>

Each ID must be unique on the page - only one id="header" allowed.

When to use each:

Classes:

  • Buttons, cards, alerts
  • Any reusable component
  • Multiple on same page
  • Styling groups of elements

IDs:

  • Page sections (header, footer)
  • JavaScript hooks
  • Fragment navigation (#section)
  • Form label associations

Specificity matters:

.text { color: blue; }
#special { color: red; }
<p class="text" id="special">
  This text is red
</p>

ID wins over class due to higher specificity.

Descendant Selectors - Context Matters

Style based on location:

/* Paragraphs in articles */
article p {
  font-size: 18px;
  line-height: 1.8;
}

/* Paragraphs in asides */
aside p {
  font-size: 14px;
  color: #666;
}

/* Links in navigation */
nav a {
  text-decoration: none;
  padding: 10px;
}

/* Links in content */
article a {
  color: blue;
  text-decoration: underline;
}

Nested descendants:

header nav ul li a {
  color: white;
}

Matches links inside list items inside unordered lists inside nav inside header.

Can traverse any depth - not just direct children.

Direct child selector (>):

/* Any depth */
ul li { }

/* Direct children only */
ul > li { }

> restricts to immediate children only.

Combining Selectors

Combining for specificity:

Element and class:

p.intro {
  font-size: 20px;
}

Only <p class="intro">, not other .intro elements.

Multiple classes:

.button.primary {
  background: blue;
}

Must have BOTH classes.

<!-- Matched -->
<button class="button primary">
  Yes
</button>

<!-- Not matched -->
<button class="button">
  No (missing primary)
</button>

<!-- Not matched -->
<button class="primary">
  No (missing button)
</button>

Adjacent sibling (+):

h1 + p {
  font-size: 120%;
}

First paragraph after any h1 gets larger font.

Used for styling lead paragraphs after headings.

Pseudo-Classes - Dynamic States

Interactive states:

/* Normal state */
a {
  color: blue;
  text-decoration: none;
}

/* Mouse hover */
a:hover {
  color: darkblue;
  text-decoration: underline;
}

/* Keyboard focus */
input:focus {
  border: 2px solid #3498db;
  outline: none;
  background: #f0f8ff;
}

/* Button pressed */
button:active {
  transform: scale(0.95);
}

/* Visited link */
a:visited {
  color: purple;
}

/* Disabled form field */
input:disabled {
  background: #f0f0f0;
  cursor: not-allowed;
}

Browser applies styles based on element state.

No JavaScript needed - pure CSS interactivity.

Structural pseudo-classes:

/* First child */
li:first-child {
  font-weight: bold;
}

/* Last child */
li:last-child {
  margin-bottom: 0;
}

/* Nth child patterns */
tr:nth-child(odd) {
  background: #f8f8f8;
}

tr:nth-child(even) {
  background: white;
}

/* Every 3rd item */
li:nth-child(3n) {
  color: red;
}

/* Not matching */
p:not(.special) {
  color: gray;
}
Row 1 (odd)
Row 2 (even)
Row 3 (odd)
Row 4 (even)

Alternating row colors without adding classes to HTML.

Specificity - Which Rule Wins?

Specificity calculation:

p { }                    /* 0-0-1 = 1 */
.class { }              /* 0-1-0 = 10 */
#id { }                 /* 1-0-0 = 100 */
p.class { }             /* 0-1-1 = 11 */
#id p { }               /* 1-0-1 = 101 */
#id .class p { }        /* 1-1-1 = 111 */

Format: ID-Class-Element

Higher specificity wins regardless of declaration order (unless !important used).

Override with !important (avoid):

p {
  color: red !important;
}

Overrides everything except other !important rules.

Makes CSS hard to maintain - use sparingly.

Specificity Calculation - Point System

Specificity battle example:

/* Score: 0-0-1-1 = 11 */
div.content {
  color: blue;
}

/* Score: 0-1-0-1 = 101 WINS */
div#main {
  color: green;
}

/* Score: 0-0-2-3 = 23 */
body div.content.highlight {
  color: red;
}

Green wins despite being in middle.

Common misconceptions:

11 classes (110) beats 1 ID (100)? NO

.a.b.c.d.e.f.g.h.i.j.k { } /* 110 */
#single { }                /* 100 */

ID still wins - categories don’t overflow.

Debugging specificity:

Browser DevTools shows:

  • Crossed out = overridden
  • Which rule is winning
  • Computed final value

When specificity fails:

p { color: red !important; }
#specific p { color: blue; }

Red wins - !important changes rules.

Only another !important with higher specificity can override.

CSS Properties and Values

Why Elements Don’t Fit As Expected

Common layout problem:

.sidebar {
  width: 300px;
  padding: 20px;
  border: 2px solid gray;
}

.content {
  width: 700px;
  padding: 20px;
}

Container width: 1000px

Expected: Sidebar + Content = 1000px

Actual result: Content wraps to next line

Why? Total width exceeds container.

Hidden calculation:

Sidebar actual width:

  • Content width: 300px
  • Padding left: 20px
  • Padding right: 20px
  • Border left: 2px
  • Border right: 2px
  • Total: 344px (not 300px!)

Content actual width:

  • Content width: 700px
  • Padding left: 20px
  • Padding right: 20px
  • Total: 740px

344px + 740px = 1084px > 1000px

Elements don’t fit - layout breaks.

Understanding the Box Model prevents these errors.

Box Model - Foundation of Layout

Every HTML element is a rectangular box with four areas:

Content - Text, images, or other content Padding - Space inside the border Border - Edge of the element Margin - Space outside to other elements

Text and Font Properties

/* Font family stack */
body {
  font-family: -apple-system,
               BlinkMacSystemFont,
               "Segoe UI",
               Helvetica,
               Arial,
               sans-serif;
}

/* Font sizing */
h1 {
  font-size: 32px;      /* Fixed pixels */
  font-weight: 700;     /* Bold */
  line-height: 1.2;     /* Tight */
}

p {
  font-size: 1rem;      /* Relative to root */
  font-weight: 400;     /* Normal */
  line-height: 1.6;     /* Comfortable */
  letter-spacing: 0.02em;
}

/* Text styling */
.lead {
  font-size: 1.25rem;
  font-weight: 300;    /* Light */
  color: #555;
  text-align: left;
}

/* Text transforms */
.uppercase {
  text-transform: uppercase;
  letter-spacing: 0.1em;
}

Heading Text

Regular paragraph text with comfortable line height for readability. Notice how spacing between lines makes text easier to read.

Lead paragraph with larger, lighter text to create visual hierarchy.

Uppercase with letter spacing

Font weights:

  • 100-300: Light
  • 400: Normal
  • 500-600: Medium
  • 700-900: Bold

Units:

  • px: Fixed size
  • em: Relative to parent
  • rem: Relative to root
  • %: Percentage of parent

Colors and Values

Gradients:

/* Linear gradient */
.hero {
  background: linear-gradient(
    135deg,
    #667eea 0%,
    #764ba2 100%
  );
}

/* Radial gradient */
.spotlight {
  background: radial-gradient(
    circle at center,
    rgba(255,255,255,0.8),
    transparent
  );
}

Shadows:

.card {
  box-shadow:
    0 4px 6px rgba(0,0,0,0.1),
    0 1px 3px rgba(0,0,0,0.08);
}

.text-glow {
  text-shadow:
    0 0 20px rgba(52,152,219,0.5);
}

Layered shadows create depth and hierarchy.

Display Property - Element Flow

Inline-block combines both:

.card {
  display: inline-block;
  width: 200px;
  padding: 20px;
  margin: 10px;
  vertical-align: top;
}

Cards sit side-by-side but accept width/height/padding like blocks.

Positioning Elements

Traditional Positioning Cannot Handle Modern Layouts

Classic layout challenges:

Centering vertically:

/* This doesn't work as expected */
.parent {
  height: 400px;
}
.child {
  position: relative;
  top: 50%;  /* Not centered! */
}

Equal-height columns:

/* Columns have different heights */
.column {
  float: left;
  width: 33.33%;
  /* No way to match heights */
}

Spacing between items:

/* Manual margin calculation */
.item {
  float: left;
  margin-right: 20px;
}
.item:last-child {
  margin-right: 0; /* Remove last */
}

Why these fail:

Traditional CSS layout designed for documents, not applications:

  • Floats designed for text wrapping around images
  • Positioning removes elements from normal flow
  • No relationship between sibling heights
  • Manual calculation of spacing
  • Clearfix hacks required

Modern requirements:

  • Navigation bars with evenly spaced items
  • Card layouts with equal heights
  • Centering content horizontally AND vertically
  • Responsive grids that reflow
  • Sidebars with flexible content areas

Flexbox designed specifically for these UI patterns.

Flexbox - Modern Layout

/* Container becomes flex */
.container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
}

/* Children flex */
.item {
  flex: 1;  /* Equal width */
}

.item.wide {
  flex: 2;  /* Double width */
}

/* Direction control */
.vertical {
  display: flex;
  flex-direction: column;
}

/* Wrapping */
.gallery {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

/* Alignment */
.center-everything {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 200px;
}
flex: 1
flex: 2 (double width)
flex: 1
Start
Center
End
Centered with Flexbox

Main axis control:

  • justify-content: Horizontal alignment
  • align-items: Vertical alignment
  • gap: Space between items

Perfect for:

  • Navigation bars
  • Card layouts
  • Centering content
  • Responsive designs

Responsive Design with Media Queries

/* Mobile first approach */
.container {
  width: 100%;
  padding: 10px;
}

.card {
  width: 100%;
  margin-bottom: 20px;
}

/* Tablet and up */
@media (min-width: 768px) {
  .container {
    max-width: 750px;
    margin: 0 auto;
    padding: 20px;
  }

  .card {
    width: 48%;
    display: inline-block;
  }
}

/* Desktop */
@media (min-width: 1024px) {
  .container {
    max-width: 1200px;
    padding: 40px;
  }

  .card {
    width: 31%;
  }
}

/* Print styles */
@media print {
  .no-print {
    display: none;
  }

  body {
    font-size: 12pt;
    color: black;
  }
}

Breakpoints:

  • Mobile: < 768px
  • Tablet: 768-1024px
  • Desktop: > 1024px

Mobile-first benefits:

  • Faster mobile loading
  • Progressive enhancement
  • Better default experience

JavaScript - Client-Side Behavior

JavaScript is a Programming Language in the Browser

Full programming language features:

// Variables and data types
let userName = "Alice";
const maxAttempts = 3;
let attempts = 0;
let isLoggedIn = false;

// Functions
function validateEmail(email) {
  const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return pattern.test(email);
}

// Conditionals
if (attempts >= maxAttempts) {
  console.log("Too many attempts");
  lockAccount();
} else {
  attempts++;
}

// Loops
const items = ["Home", "About", "Contact"];
for (let item of items) {
  createNavLink(item);
}

// Objects and arrays
const user = {
  name: "Alice",
  email: "alice@example.com",
  orders: [101, 102, 103]
};

JavaScript has everything Python has:

  • Variables, functions, objects
  • Control flow (if/else, loops)
  • Data structures (arrays, objects)
  • Error handling (try/catch)

Different from server languages:

JavaScript CANNOT:

  • Read/write local files
  • Access database directly
  • Run system commands
  • Access other programs

JavaScript CAN:

  • Modify web page (DOM)
  • Make HTTP requests
  • Store data in browser
  • Respond to user events

DOM Manipulation - Changing the Page

JavaScript modifies HTML after loading:

Initial HTML:

<div id="message">Welcome</div>
<ul id="list">
  <li>Item 1</li>
  <li>Item 2</li>
</ul>
<button id="addBtn">Add Item</button>

JavaScript manipulation:

// Get element by ID
const message = document.getElementById('message');

// Change text content
message.textContent = 'Hello, Alice!';

// Change styles
message.style.color = 'blue';
message.style.fontSize = '24px';

// Add CSS class
message.classList.add('highlight');

// Create new element
const newItem = document.createElement('li');
newItem.textContent = 'Item 3';

// Add to existing list
const list = document.getElementById('list');
list.appendChild(newItem);

// Remove element
const firstItem = list.firstElementChild;
firstItem.remove();

// Change HTML content
message.innerHTML = '<strong>Welcome</strong> back!';

Page updates immediately without server request.

Common DOM methods:

// Finding elements
getElementById('id')
querySelector('.class')
querySelectorAll('p')

// Modifying elements
element.textContent = 'text'
element.innerHTML = '<b>html</b>'
element.style.property = 'value'
element.classList.add('class')
element.setAttribute('attr', 'val')

// Creating/removing
createElement('tag')
appendChild(element)
removeChild(element)
element.remove()

Changes happen instantly in browser. No network request needed.

Event-Driven Programming

Code runs in response to user actions:

// Click event
const button = document.getElementById('submitBtn');
button.addEventListener('click', function(event) {
  console.log('Button clicked!');
  // Do something
});

// Form submission
const form = document.getElementById('loginForm');
form.addEventListener('submit', function(event) {
  event.preventDefault(); // Stop page reload

  const email = document.getElementById('email').value;
  const password = document.getElementById('password').value;

  if (validateLogin(email, password)) {
    submitLogin(email, password);
  } else {
    showError('Invalid credentials');
  }
});

// Input changes
const searchBox = document.getElementById('search');
searchBox.addEventListener('input', function(event) {
  const query = event.target.value;

  if (query.length > 2) {
    fetchSearchResults(query);
  }
});

// Mouse events
element.addEventListener('mouseover', highlight);
element.addEventListener('mouseout', unhighlight);

// Keyboard events
document.addEventListener('keydown', function(event) {
  if (event.key === 'Escape') {
    closeModal();
  }
});

Event object provides details:

button.addEventListener('click', function(event) {
  // Mouse position
  console.log(event.clientX, event.clientY);

  // Which element clicked
  console.log(event.target);

  // Prevent default action
  event.preventDefault();

  // Stop bubbling up
  event.stopPropagation();
});

Asynchronous Operations - fetch()

Making HTTP requests from JavaScript:

// Simple GET request
fetch('/api/users')
  .then(response => response.json())
  .then(data => {
    console.log(data);
    updateUserList(data);
  })
  .catch(error => {
    console.error('Error:', error);
  });

// POST request with data
fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Alice',
    email: 'alice@example.com'
  })
})
.then(response => response.json())
.then(newUser => {
  addUserToList(newUser);
});

// Modern async/await syntax
async function loadUserData() {
  try {
    const response = await fetch('/api/user/123');
    const user = await response.json();

    displayUser(user);

    // Load related data
    const ordersResponse = await fetch(`/api/orders?user=${user.id}`);
    const orders = await ordersResponse.json();

    displayOrders(orders);
  } catch (error) {
    showError('Failed to load data');
  }
}

// Page stays responsive during fetch
loadUserData(); // Non-blocking
console.log('This runs immediately');

No page reload:

Traditional form submission:

  1. Submit form
  2. Page reloads completely
  3. New HTML from server
  4. User loses scroll position

With fetch():

  1. JavaScript sends request
  2. Page stays as is
  3. Response updates part of page
  4. User continues working

Asynchronous requests prevent UI blocking - user interaction continues during 200ms network round trip.

Browser APIs Available to JavaScript

Browser provides APIs beyond DOM manipulation for storage, networking, and media.

JavaScript Example Uses

Form Validation - Immediate Feedback

Client-side validation before submission:

const form = document.getElementById('signupForm');
const emailInput = document.getElementById('email');
const passwordInput = document.getElementById('password');
const submitBtn = document.getElementById('submitBtn');

// Real-time email validation
emailInput.addEventListener('input', function() {
  const email = this.value;
  const emailError = document.getElementById('emailError');

  if (!email) {
    emailError.textContent = 'Email is required';
    emailError.className = 'error';
  } else if (!isValidEmail(email)) {
    emailError.textContent = 'Invalid email format';
    emailError.className = 'error';
  } else {
    emailError.textContent = 'Valid email';
    emailError.className = 'success';
  }

  updateSubmitButton();
});

// Password strength checker
passwordInput.addEventListener('input', function() {
  const password = this.value;
  const strength = checkPasswordStrength(password);
  const meter = document.getElementById('strengthMeter');

  if (strength < 30) {
    meter.style.width = '30%';
    meter.style.backgroundColor = 'red';
    meter.textContent = 'Weak';
  } else if (strength < 70) {
    meter.style.width = '60%';
    meter.style.backgroundColor = 'orange';
    meter.textContent = 'Medium';
  } else {
    meter.style.width = '100%';
    meter.style.backgroundColor = 'green';
    meter.textContent = 'Strong';
  }
});

// Prevent invalid submission
form.addEventListener('submit', function(event) {
  if (!validateForm()) {
    event.preventDefault();
    showErrors();
  }
});

Sign Up

Invalid email format
Medium

Benefits of client-side validation:

  • Instant feedback (no server round-trip)
  • Better user experience
  • Reduces server load
  • Shows password strength
  • Enables/disables submit button

BUT always validate on server too:

@app.route('/signup', methods=['POST'])
def signup():
    email = request.form['email']
    password = request.form['password']

    # Server must re-validate
    if not is_valid_email(email):
        return {'error': 'Invalid email'}, 400

    if len(password) < 8:
        return {'error': 'Password too short'}, 400

    # Client validation can be bypassed
    # Server validation cannot

Dynamic Content Loading

Infinite scroll implementation:

let page = 1;
let loading = false;

// Detect scroll near bottom
window.addEventListener('scroll', function() {
  const scrollHeight = document.documentElement.scrollHeight;
  const scrollTop = document.documentElement.scrollTop;
  const clientHeight = document.documentElement.clientHeight;

  // Near bottom?
  if (scrollTop + clientHeight >= scrollHeight - 100) {
    loadMoreContent();
  }
});

async function loadMoreContent() {
  if (loading) return; // Already loading

  loading = true;
  showLoadingSpinner();

  try {
    const response = await fetch(`/api/items?page=${page}`);
    const items = await response.json();

    if (items.length > 0) {
      appendItemsToList(items);
      page++;
    } else {
      showEndOfContent();
    }
  } catch (error) {
    showError('Failed to load more');
  }

  loading = false;
  hideLoadingSpinner();
}

Search suggestions:

const searchInput = document.getElementById('search');
let searchTimeout;

searchInput.addEventListener('input', function() {
  clearTimeout(searchTimeout);
  const query = this.value;

  if (query.length < 2) {
    hideSuggestions();
    return;
  }

  // Debounce: Wait 300ms after typing stops
  searchTimeout = setTimeout(() => {
    fetchSuggestions(query);
  }, 300);
});

async function fetchSuggestions(query) {
  const response = await fetch(`/api/search?q=${query}`);
  const suggestions = await response.json();
  displaySuggestions(suggestions);
}

Interactive UI Components

Tab switching without reload:

// Tab implementation
const tabs = document.querySelectorAll('.tab');
const panels = document.querySelectorAll('.panel');

tabs.forEach(tab => {
  tab.addEventListener('click', function() {
    const targetId = this.dataset.panel;

    // Hide all panels
    panels.forEach(p => p.classList.remove('active'));

    // Remove active from all tabs
    tabs.forEach(t => t.classList.remove('active'));

    // Show selected panel
    document.getElementById(targetId).classList.add('active');
    this.classList.add('active');
  });
});

Profile content shown here. Click tabs to switch content without page reload.

Modal dialog:

const modal = document.getElementById('modal');
const openBtn = document.getElementById('openModal');
const closeBtn = document.getElementById('closeModal');

openBtn.addEventListener('click', () => {
  modal.style.display = 'block';
  document.body.style.overflow = 'hidden';
});

closeBtn.addEventListener('click', () => {
  modal.style.display = 'none';
  document.body.style.overflow = 'auto';
});

// Close on outside click
modal.addEventListener('click', (e) => {
  if (e.target === modal) {
    modal.style.display = 'none';
  }
});

Drag and drop interface:

let draggedItem = null;

// Make items draggable
document.querySelectorAll('.draggable').forEach(item => {
  item.draggable = true;

  item.addEventListener('dragstart', function(e) {
    draggedItem = this;
    this.style.opacity = '0.5';
  });

  item.addEventListener('dragend', function(e) {
    this.style.opacity = '';
  });
});

// Set up drop zones
document.querySelectorAll('.dropzone').forEach(zone => {
  zone.addEventListener('dragover', function(e) {
    e.preventDefault();
    this.style.backgroundColor = '#e8f4ff';
  });

  zone.addEventListener('dragleave', function(e) {
    this.style.backgroundColor = '';
  });

  zone.addEventListener('drop', function(e) {
    e.preventDefault();
    if (draggedItem) {
      this.appendChild(draggedItem);
      this.style.backgroundColor = '';
      updateOrder();
    }
  });
});
📝 Task 1
📝 Task 2

Drag tasks here

Without JavaScript, every interaction would require full page reload.

Single Page Applications (SPAs)

SPA routing example:

// Simple client-side router
class Router {
  constructor() {
    this.routes = {};
    window.addEventListener('popstate', () => this.handleRoute());
  }

  addRoute(path, handler) {
    this.routes[path] = handler;
  }

  navigate(path) {
    window.history.pushState({}, '', path);
    this.handleRoute();
  }

  handleRoute() {
    const path = window.location.pathname;
    const handler = this.routes[path] || this.routes['/404'];
    handler();
  }
}

// Usage
const router = new Router();

router.addRoute('/', () => loadHomePage());
router.addRoute('/about', () => loadAboutPage());
router.addRoute('/contact', () => loadContactPage());

// Navigation without page reload
document.querySelectorAll('a[data-route]').forEach(link => {
  link.addEventListener('click', (e) => {
    e.preventDefault();
    router.navigate(e.target.href);
  });
});

Trade-offs:

  • SPAs eliminate reload delay but increase initial load time
  • SPAs suit applications, traditional navigation suits content sites
  • SPAs need JavaScript frameworks (React, Vue, Angular)

JavaScript Security

JavaScript Has Strict Boundaries

JavaScript capabilities shown:

  • Modify any element on the page
  • Make HTTP requests to servers
  • Store data in browser storage
  • Respond to user events
  • Create animations and transitions
  • Validate form inputs
  • Load content dynamically

What if malicious code tried to:

// Read your tax documents?
const files = readUserFiles('/Documents/taxes');

// Install malware?
executeProgram('virus.exe');

// Access your passwords?
const passwords = readFile('/etc/passwd');

The security problem:

Every website runs JavaScript:

  • News sites with ads
  • Social media with third-party widgets
  • Shopping sites with analytics
  • Blogs with comment systems

If JavaScript had system access:

  • Any website could read your files
  • Ads could install software
  • Compromised sites could steal everything

The solution: Browser Sandbox

JavaScript runs in isolated environment:

  • No file system access
  • No program execution
  • No network access except HTTP
  • No access to other tabs/windows
  • No system information beyond basics

Browser enforces these limits - not optional.

Browser Sandbox - Protection Model

Sandbox prevents malicious code from:

  • Reading your files
  • Installing software
  • Accessing other websites’ data
  • Mining cryptocurrency using your CPU
  • Stealing passwords from other tabs

Client-Side Code is Completely Visible

Anyone can view and modify JavaScript:

Original code in your file:

// api-key.js
const API_KEY = 'sk-abc123secret456';
const ADMIN_PASSWORD = 'supersecret';

function checkPremiumUser() {
  return user.subscription === 'premium';
}

function calculatePrice(items) {
  let total = 0;
  for (let item of items) {
    total += item.price * item.quantity;
  }

  // Apply secret discount
  if (user.vip) {
    total *= 0.5; // 50% off for VIPs
  }

  return total;
}

async function processPayment(amount) {
  const response = await fetch('/api/payment', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({ amount })
  });
  return response.json();
}

User opens DevTools:

  1. Views all source code
  2. Sees API keys and passwords
  3. Modifies functions in console
  4. Bypasses client-side checks

What NOT to put in JavaScript:

  • API keys
  • Passwords
  • Secret algorithms
  • Premium content
  • Security logic
  • User authentication

Server must enforce all rules:

@app.route('/api/payment', methods=['POST'])
def payment():
    # Server-side validation
    amount = request.json['amount']
    user = get_current_user()

    # Recalculate on server
    real_amount = calculate_price_server_side()

    if amount != real_amount:
        return {'error': 'Invalid amount'}, 400

    # Process payment with server-side API key
    process_payment(real_amount, SERVER_API_KEY)

Performance Constraints

JavaScript blocks rendering:

<!-- Blocks HTML parsing -->
<script src="large-library.js"></script>
<p>This won't appear until JS loads</p>

<!-- Better: Async loading -->
<script src="large-library.js" async></script>
<p>This appears immediately</p>

<!-- Better: Defer until DOM ready -->
<script src="app.js" defer></script>

Bundle size matters:

// Bad: Including entire library
import _ from 'lodash'; // 70KB
const unique = _.uniq([1, 2, 2, 3]);

// Good: Import only needed function
import uniq from 'lodash/uniq'; // 2KB
const unique = uniq([1, 2, 2, 3]);

Mobile performance:

Desktop with fast CPU:

  • Parse 1MB JS: 100ms
  • Execute complex function: 10ms

Mobile with slow CPU:

  • Parse 1MB JS: 1000ms
  • Execute same function: 100ms

Critical rendering path:

// Bad: Runs immediately, blocks page
for (let i = 0; i < 1000000; i++) {
  // Heavy computation
}

// Good: Defer heavy work
requestIdleCallback(() => {
  // Heavy computation when idle
});

// Good: Web Worker for heavy tasks
const worker = new Worker('heavy-task.js');
worker.postMessage({cmd: 'start'});

Mobile users on slow connections suffer most from large JavaScript bundles.

Security Best Practices

Remember: Client-side JavaScript is for user experience, not security.

All security enforcement must happen on the server where users cannot modify the code.

Static File Serving

Web Server Maps URL to File System Path

HTTP request contains URL path:

GET /docs/api.html HTTP/1.1
Host: example.com
Accept: text/html

Web server receives request on port 80 (HTTP) or 443 (HTTPS).

Server extracts path component: /docs/api.html

Path resolution algorithm:

document_root = /var/www/html
requested_path = /docs/api.html

file_path = document_root + requested_path
        = /var/www/html/docs/api.html

Server attempts to open file at computed path.

If file exists:

  • Read file contents into memory
  • Determine MIME type from extension (.html → text/html)
  • Send HTTP 200 response with file contents

If file doesn’t exist:

  • Send HTTP 404 Not Found response

No code execution occurs between request and response.

Server performs pure I/O operation:

  1. Map URL to file system path
  2. Read bytes from disk
  3. Write bytes to network socket

This mapping is deterministic - same URL always maps to same file path.

URL components:

  • Protocol: http://
  • Host: example.com
  • Path: /docs/api.html ← used for file mapping
  • Query parameters: ignored by static server

Common document roots:

  • Apache: /var/www/html
  • Nginx: /usr/share/nginx/html
  • Flask: project_directory/static/

Security consideration:

Path traversal attempts blocked:

GET /../../../etc/passwd HTTP/1.1

Server validates path doesn’t escape document root.

Flask Static File Convention

Flask project structure:

project/
  app.py
  templates/
    index.html
    results.html
  static/
    styles.css
    app.js
    images/
      logo.png

Flask establishes two conventions:

templates/ directory:

Contains HTML files returned by route functions:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/results')
def results():
    # This could pass data to template
    return render_template('results.html')

Route / executes index() function. Function returns contents of templates/index.html.

static/ directory:

Contains CSS, JavaScript, images, data files.

No route function needed - Flask automatically maps:

  • URL /static/styles.css → file static/styles.css
  • URL /static/images/logo.png → file static/images/logo.png

HTML references static files:

<link rel="stylesheet" href="/static/styles.css">
<script src="/static/app.js"></script>
<img src="/static/images/logo.png">

Development server behavior:

if __name__ == '__main__':
    app.run(debug=True)

Flask development server handles both:

  • Route execution for templates
  • Static file serving

Production deployment:

Nginx serves static files directly:

location /static {
    alias /path/to/project/static;
    expires 1y;
}

location / {
    proxy_pass http://flask_app;
}

Static files bypass Flask entirely in production.

Browser Loads Page Through Multiple Requests

Initial HTML request:

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="/static/main.css">
  <link rel="stylesheet" href="/static/theme.css">
  <script src="/static/vendor.js"></script>
  <script src="/static/app.js"></script>
</head>
<body>
  <img src="/static/logo.png">
  <img src="/static/hero.jpg">
</body>
</html>

Browser parses HTML sequentially.

Each external resource reference triggers new HTTP request.

Request sequence:

  1. GET / → Returns HTML (45ms)
  2. Browser parses <head>, finds two <link> tags
  3. GET /static/main.css (parallel)
  4. GET /static/theme.css (parallel)
  5. Browser finds two <script> tags
  6. GET /static/vendor.js (parallel)
  7. GET /static/app.js (parallel)
  8. Browser parses <body>, finds <img> tags
  9. GET /static/logo.png (parallel)
  10. GET /static/hero.jpg (parallel)

CSS may reference additional resources:

/* main.css */
@font-face {
  font-family: 'Custom';
  src: url('/static/fonts/custom.woff2');
}

body {
  background: url('/static/bg.jpg');
}
  1. GET /static/fonts/custom.woff2
  2. GET /static/bg.jpg

Total: 9 HTTP requests to display one page.

Browser constraints:

  • HTTP/1.1: Maximum 6 concurrent connections per domain
  • Must wait for HTML before discovering CSS/JS
  • Must wait for CSS before discovering CSS-referenced images
  • Render-blocking resources (CSS, synchronous JS) delay page display

Performance implications:

Each file requires:

  • DNS lookup (cached after first)
  • TCP connection establishment
  • HTTP request/response cycle
  • Download time based on file size

Optimization strategies:

  • Bundle multiple CSS/JS files
  • Inline critical CSS
  • Use CSS sprites for small images
  • Enable HTTP/2 for multiplexing
  • Set cache headers for static assets

Session State Problem in Static Serving

User visits three pages in sequence:

  1. User visits /login.html
    • Enters username and password
    • Clicks submit button
  2. Browser redirects to /dashboard.html
    • Should show “Welcome, Alice”
    • Should display user’s data
  3. User clicks link to /settings.html
    • Should show Alice’s settings
    • Should allow updates

Static file limitations:

Page 1: /login.html

<form action="/authenticate" method="POST">
  <input name="username">
  <input name="password">
  <button>Login</button>
</form>

Form submission fails - no /authenticate handler exists.

Page 2: /dashboard.html

<h1>Welcome, [WHO?]</h1>
<p>Your data: [WHAT DATA?]</p>

Server has no memory of login attempt. No way to identify user.

Page 3: /settings.html

<h2>Settings for [WHO?]</h2>
<form>
  <input value="[CURRENT VALUE?]">
</form>

Each request independent - no connection between pages.

HTTP statelessness prevents session tracking:

Each request stands alone - no built-in connection between requests.

Static server processes requests independently:

  • Request 1: Read login.html, return it
  • Request 2: Read dashboard.html, return it
  • Request 3: Read settings.html, return it

No mechanism to:

  • Remember who logged in
  • Carry identity across requests
  • Maintain user context between pages

Session mechanism (requires code execution):

# Login endpoint
@app.route('/authenticate', methods=['POST'])
def authenticate():
    username = request.form['username']
    password = request.form['password']

    user = verify_credentials(username, password)
    if user:
        session_id = generate_token()
        store_session(session_id, user.id)

        response = redirect('/dashboard')
        response.set_cookie('session', session_id)
        return response

# Dashboard with session
@app.route('/dashboard')
def dashboard():
    session_id = request.cookies.get('session')
    user_id = lookup_session(session_id)
    user_data = get_user_data(user_id)

    return render_template('dashboard.html',
                          user=user_data)

Static serving cannot execute any of this logic.

Product Search Demonstrates Query Complexity

User searches for “laptop under 1000”

Search requires:

  1. Parse search query into terms and constraints
  2. Build database query with appropriate filters
  3. Execute query against product database
  4. Format results as HTML
  5. Handle pagination for large result sets

Breaking down the search:

Text parsing:

  • “laptop” → product name/description match
  • “under” → price comparison operator
  • “1000” → price constraint value

SQL query construction:

SELECT * FROM products
WHERE (name LIKE '%laptop%'
       OR description LIKE '%laptop%')
  AND price < 1000
ORDER BY relevance DESC
LIMIT 20 OFFSET 0

Consider search variations:

  • “gaming laptop” → two terms to match
  • “laptop 500-1000” → price range
  • “dell laptop” → brand + category
  • “laptop with 16gb ram” → specification filter
  • “cheapest laptop” → sort by price ascending
  • “laptop -gaming” → exclusion filter

Each variation requires different query logic.

Pagination adds another dimension:

Page 1: OFFSET 0 Page 2: OFFSET 20 Page 3: OFFSET 40

Combined with search terms: infinite URL combinations.

Relevance scoring:

SELECT *,
  (CASE WHEN name LIKE '%laptop%' THEN 10 ELSE 0 END +
   CASE WHEN brand = 'Dell' THEN 5 ELSE 0 END +
   CASE WHEN description LIKE '%laptop%' THEN 3 ELSE 0 END)
  AS relevance
FROM products
WHERE ...
ORDER BY relevance DESC

Requires computation impossible in static files.

Query variations multiply infinitely:

Search terms × Price ranges × Brands × Specifications × Sort orders × Pages

“laptop” → 47 results “gaming laptop” → 12 results “dell laptop” → 23 results “laptop 16gb” → 8 results

Each requires different SQL query.

Cannot pre-generate HTML for every possible search.

Static Sites Cannot Process Forms - Data Must Flow to Dynamic Handlers

The form paradox:

Static HTML can create forms:

<!-- In register.html (static file) -->
<form action="/api/register" method="POST">
  <input name="email" type="email">
  <input name="password" type="password">
  <button>Sign Up</button>
</form>

But static server cannot:

  • Receive POST data
  • Validate inputs
  • Store in database
  • Send emails
  • Create sessions

Form data requires processing.

Static files only serve - they cannot compute.

Form data flow:

  1. User fills form in static HTML
  2. Browser submits to action URL
  3. Server routes to application code
  4. Application processes data
  5. Response sent back to user

The transformation pipeline:

Static HTML Form
    ↓ (user input)
Browser Encoding
    ↓ (POST request)
Server Routing
    ↓ (to application)
Data Processing
    ↓ (validation, storage)
Response Generation

Understanding this flow explains why static sites need API backends for any user interaction.

Form Data Transformation Pipeline

Registration form submission lifecycle:

Browser sends raw form data:

POST /register HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email=alice%40example.com&password=secretpass123&
country=US&newsletter=on

Step 1: Parse URL encoding

# URL encoded string
raw = "email=alice%40example.com&password=secretpass123"

# Parse into dictionary
data = {
    'email': 'alice@example.com',  # %40 decoded to @
    'password': 'secretpass123',
    'country': 'US',
    'newsletter': 'on'
}

Step 2: Validate each field

errors = []

# Email validation
if not re.match(r'^[^@]+@[^@]+\.[^@]+$', data['email']):
    errors.append('Invalid email format')

# Password strength
if len(data['password']) < 8:
    errors.append('Password too short')

# Country code check
if data['country'] not in VALID_COUNTRIES:
    errors.append('Invalid country')

Step 3: Business logic checks

# Check email uniqueness
existing = db.execute(
    "SELECT id FROM users WHERE email = ?",
    data['email']
).fetchone()

if existing:
    return error("Email already registered")

# Rate limiting
recent = db.execute(
    "SELECT COUNT(*) FROM users
     WHERE ip_address = ?
     AND created_at > datetime('now', '-1 hour')",
    request.remote_addr
).fetchone()[0]

if recent > 5:
    return error("Too many registrations")

Step 4: Password transformation

# Never store plain text
plain_password = data['password']

# Generate salt and hash
salt = bcrypt.gensalt(rounds=12)
password_hash = bcrypt.hashpw(
    plain_password.encode('utf-8'),
    salt
)
# Result: $2b$12$KIXx...QW8e (60 bytes)

Step 5: Database insertion

user_id = db.execute(
    """INSERT INTO users
       (email, password_hash, country,
        newsletter, created_at, ip_address)
       VALUES (?, ?, ?, ?, datetime('now'), ?)""",
    data['email'], password_hash, data['country'],
    data['newsletter'] == 'on', request.remote_addr
).lastrowid

Each step requires computation unavailable in static serving.

Static Serving Examples

Documentation sites work well with static serving:

<!DOCTYPE html>
<html>
<head>
  <title>API Reference - Model Inference</title>
  <link rel="stylesheet" href="/static/docs.css">
  <link rel="stylesheet" href="/static/syntax.css">
</head>
<body>
  <nav>
    <a href="/docs/quickstart.html">Quick Start</a>
    <a href="/docs/authentication.html">Authentication</a>
    <a href="/docs/endpoints.html">Endpoints</a>
    <a href="/docs/errors.html">Error Codes</a>
  </nav>

  <main>
    <h1>POST /api/v1/predict</h1>

    <p>Submit input data for model inference.</p>

    <h2>Request Format</h2>
    <pre><code class="language-json">{
  "model_id": "resnet50-v2",
  "inputs": [[0.1, 0.2, ...], ...],
  "parameters": {
    "temperature": 0.7,
    "top_k": 10
  }
}</code></pre>

    <h2>Response Format</h2>
    <pre><code class="language-json">{
  "predictions": [0.92, 0.03, ...],
  "model_version": "2.1.0",
  "inference_time_ms": 47
}</code></pre>
  </main>
</body>
</html>

Why static works here:

  1. Content stability - API contracts change infrequently
  2. No personalization - Same documentation for all users
  3. Read-only access - Users consume, don’t modify
  4. No computation - Code examples are literal text
  5. Version control friendly - HTML/Markdown in git

Deployment simplicity:

# Build documentation
markdown_to_html docs/*.md output/

# Deploy to GitHub Pages
git add output/
git commit -m "Update docs"
git push origin gh-pages

# Or S3 static hosting
aws s3 sync output/ s3://docs.example.com

Blog with static site generator:

# _posts/2024-01-15-model-deployment.md
---
title: "Deploying ML Models at Scale"
date: 2024-01-15
author: "Dr. Smith"
tags: [ml, deployment, kubernetes]
---

When deploying models to production...

Build process:

# Jekyll/Hugo generates HTML from Markdown
jekyll build
# Creates:
# _site/2024/01/15/model-deployment.html
# _site/index.html (with post list)
# _site/tags/ml.html (tag archive)

Generated HTML is static:

<article>
  <header>
    <h1>Deploying ML Models at Scale</h1>
    <time>January 15, 2024</time>
    <span>by Dr. Smith</span>
  </header>

  <p>When deploying models to production...</p>

  <footer>
    Tags: <a href="/tags/ml.html">ml</a>,
          <a href="/tags/deployment.html">deployment</a>
  </footer>
</article>

Appropriate because:

  • Posts don’t change after publication
  • No user-specific content
  • Comments handled by third-party service (Disqus)
  • Search via client-side JavaScript (lunr.js)

Limitations accepted:

  • Rebuild needed for new posts
  • No real-time content
  • No authenticated areas
  • Search limited to pre-indexed content

Static serving matches use case constraints.

Putting It Together

Complete Request/Response Cycle - Timeline View

User clicks “View Orders” link:

<a href="/orders">View Orders</a>

Browser initiates request sequence:

Phase 1: DNS Lookup (15ms) Browser cache: app.example.com → miss OS cache: miss DNS query: app.example.com93.184.216.34

Phase 2: TCP Connection (25ms) Three-way handshake:

  • SYN →
  • ← SYN-ACK
  • ACK → Connection established to port 443 (HTTPS)

Phase 3: TLS Handshake (40ms) Certificate verification Cipher suite negotiation Session key exchange

Phase 4: HTTP Request (5ms)

GET /orders HTTP/1.1
Host: app.example.com
Cookie: session=abc123def456

Phase 5: Server Processing (85ms)

  • Nginx receives request (1ms)

  • Routes to Flask application (2ms)

  • Flask executes route function (82ms):

    • Verify session (5ms)
    • Query database for orders (45ms)
    • Render template with data (32ms)

Phase 6: Response Transfer (30ms) Server sends 15KB HTML document Network transfer at 4Mbps

Total time: 200ms from click to HTML received

Timing breakdown:

  • DNS cached after first request (saves 15ms)
  • TLS handshake only on new connections
  • Database query dominates server time (45ms of 85ms)
  • Network transfer time depends on document size
  • User perceives delay of ~200ms as “instant”

Server processing (85ms):

  1. Session validation against Redis cache
  2. SQL query: SELECT * FROM orders WHERE user_id = 42
  3. Template engine combines data with HTML
  4. Response compression (gzip)

Server Routes Request to Code or Files

Nginx receives all HTTP requests first:

server {
    listen 443 ssl;
    server_name app.example.com;

    # Static files - served directly
    location /static {
        alias /var/www/app/static;
        expires 1y;
        add_header Cache-Control "public";
    }

    # Media uploads - served directly
    location /media {
        alias /var/www/app/uploads;
        expires 30d;
    }

    # Everything else - proxy to Flask
    location / {
        proxy_pass http://localhost:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Routing decision tree:

  1. Request arrives: GET /orders HTTP/1.1
  2. Check location blocks in order
  3. /orders doesn’t match /static or /media
  4. Matches / (catch-all)
  5. Proxy request to Flask application

Flask application receives proxied request:

@app.route('/orders')
def view_orders():
    # This code executes
    user_id = session.get('user_id')
    orders = Order.query.filter_by(
        user_id=user_id
    ).all()
    return render_template('orders.html',
                         orders=orders)

Static file requests never reach Flask.

Performance comparison:

Request Type Processing Time Operations
Static CSS 0.3ms Read file from disk
Static Image 0.5ms Read file from disk
Dynamic Page 85ms Session + DB + Template
API Endpoint 45ms Session + DB + JSON

Static files bypass application code - direct disk-to-network transfer (0.5ms) vs database queries and template rendering (45ms).

Browser Constructs Page from Multiple Files

HTML response references multiple resources:

<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="/static/base.css">
  <link rel="stylesheet" href="/static/orders.css">
  <script src="/static/vendor.js"></script>
  <script src="/static/app.js"></script>
</head>
<body>
  <img src="/static/logo.png" alt="Logo">
  <h1>Your Orders</h1>
  <div class="order-list">
    <!-- Order data here -->
  </div>
</body>
</html>

Browser discovers resources progressively:

  1. Parse HTML line by line
  2. Find <link> tag → Request base.css
  3. Find second <link> → Request orders.css
  4. Find <script> tags → Request both JS files
  5. Continue parsing, find <img> → Request logo.png

CSS may reference more resources:

/* base.css */
@font-face {
  font-family: 'Custom';
  src: url('/static/fonts/custom.woff2');
}

body {
  background: url('/static/patterns/grid.svg');
}

/* orders.css */
.order-icon {
  background: url('/static/icons/package.svg');
}

Browser can’t discover these until CSS loads and parses.

Cascade effect: Must load A before discovering B exists.

Connection limit creates “rounds”:

With 6 parallel connections and 9 resources:

  • Round 1: HTML (1 connection)
  • Round 2: 5 resources from HTML (5 connections)
  • Round 3: 3 resources from CSS (3 connections)

Each round adds latency. HTTP/2 multiplexing eliminates this bottleneck.

Render Pipeline Transforms Data to Pixels

Browser converts HTML/CSS to visual display:

Step 1: Parse HTML → DOM Tree

<div class="order">
  <h2>Order #1234</h2>
  <p>Total: $89.99</p>
</div>

Creates tree structure:

div (class="order")
├── h2
│   └── "Order #1234"
└── p
    └── "Total: $89.99"

Step 2: Parse CSS → CSSOM

.order {
  border: 1px solid blue;
  padding: 10px;
}
h2 {
  font-size: 24px;
  color: navy;
}

Creates style rules tree.

Step 3: Combine DOM + CSSOM → Render Tree

Each DOM node gets computed styles:

  • div.order: border, padding inherited from CSS
  • h2: 24px navy text
  • p: Default styles

Step 4: Layout (Reflow)

Calculate exact position and size:

  • div.order: x=10, y=50, width=400, height=80
  • h2: x=20, y=60, width=380, height=30
  • p: x=20, y=95, width=380, height=20

Step 5: Paint

Draw pixels to screen in order:

  1. Background colors
  2. Borders
  3. Text
  4. Images

Performance implications:

  • Reflow (layout recalculation): Expensive
    • Triggered by: Size changes, position changes
    • Affects all child elements
  • Repaint: Moderate cost
    • Triggered by: Color changes, visibility
    • Doesn’t recalculate positions
  • Composite: Cheap
    • Triggered by: Opacity, transforms
    • GPU accelerated

JavaScript DOM changes restart pipeline from step 1.

Developer Tools Reveal the Process

Network tab shows request waterfall:

Name            Status  Type    Size    Time
─────────────────────────────────────────────
orders          200     html    15KB    85ms
base.css        200     css     8KB     50ms
orders.css      200     css     5KB     45ms
vendor.js       200     js      45KB    100ms
app.js          200     js      12KB    70ms
logo.png        200     png     3KB     30ms
custom.woff2    200     font    25KB    30ms
grid.svg        200     svg     2KB     15ms
package.svg     200     svg     1KB     15ms
─────────────────────────────────────────────
                        Total: 111KB    440ms

Waterfall reveals:

  • Parallel requests limited to 6
  • CSS files loaded before fonts discovered
  • Blocking resources delay page render

Elements tab shows current DOM:

View Source shows:

<div id="count">0</div>

Elements tab shows (after JavaScript):

<div id="count">42</div>

JavaScript modified DOM after page load.

Console shows JavaScript activity:

[10:23:45.123] Fetching user data...
[10:23:45.234] API response: 200 OK
[10:23:45.245] Updated 3 elements
[10:23:45.267] Error: Cannot read property
               'name' of undefined at line 47

Errors appear immediately with file and line number.

What DevTools reveals:

  1. Network: Every HTTP request with timing
  2. Elements: Current DOM state (after JS changes)
  3. Console: JavaScript execution and errors
  4. Sources: Set breakpoints in JavaScript

DevTools reveals runtime behavior:

  • View Source: Shows original HTML sent by server
  • Inspect Element: Shows current DOM after JavaScript