The browser side of the web, in one mental model. AI will write these files for you — this deck is the map so you can read them, judge them and fix them.
← / → to navigate · EN/VI switch below
Every page you've used — shop, dashboard, bank app — splits its work into these three files.
The words, images, buttons and their order. The skeleton, and the stuff inside it.
Colours, spacing, where things sit. Turns a plain stack of boxes into a designed page.
What happens when you click, type or scroll. The page stops being a picture and starts reacting.
The frontend is the client side. The server's job ends the moment it hands over three kinds of files — everything after happens in your browser.
Sends the page as plain files: HTML + CSS + JS.
Parses the HTML into a tree, applies the CSS, runs the JS.
See painted pixels — and click them. The frontend is everything between the server and your eyes.
server ──(html · css · js)──▶ browser ──(pixels)──▶ your eyes
Four beats. After beat 3 the page is alive in your browser — and beat 4 repeats on every click.
Your browser asks a server: 'give me this page'.
It sends HTML + CSS + JS. The whole answer is just text files.
HTML becomes a tree of elements, CSS paints and places them, JS starts running.
JS reacts on the spot; for new data it can ask the server again (an API call) — no reload.
This deck is that map — recognize, judge, fix.
Working files: HTML with real structure, CSS that looks professional, JS that reacts. You'll rarely type these by hand.
Read the structure, judge the design, test the behavior. You need the map — not a driver's license.
The next four dividers are the map itself: HTML, CSS, Bootstrap, JS.
Tags with meaning, nested into a tree. The only file that decides what a page IS.
Two halves: <head> is metadata and links (invisible), <body> is everything you can see.
<!doctype html>
<html>
<head> ← invisible: title, CSS links
<title>My page</title>
<link rel="stylesheet" href="style.css">
</head>
<body> ← everything the user sees
<h1>Hello, world</h1>
<p>This is the first paragraph.</p>
<p id="status"></p>
</body>
</html>
This is the first paragraph.
click me — I can be edited
Right side is the live render of this exact page — click the status line, it becomes editable. Tags come in pairs: <h1>…</h1>.
HTML has almost no syntax. Keep these three and you can read any page.
<div> opens, </div> closes. Whatever sits between is the tag's content.
Tags inside tags, always closed properly. The nesting order IS the page's structure.
Extra info inside the opening tag: <a href="/shop">. href says where the link goes.
You don't need to memorize HTML — you need to recognize it when you (or your AI) read it.
| Tag | What it does |
|---|---|
| h1 – h6 | Headings, big → small. One h1 per page, the rest is structure. |
| p · a · img | Paragraph · link · image — the three content workhorses. |
| div · span | Generic containers — meaning-less boxes you wrap things in. |
| ul / li | A bulleted list and its items. |
| button · input · form | The interactive trio: press it · type in it · send it. |
| nav · header · main · footer | The standard zones of a page: menu · top · content · bottom. |
A logo, a list of links, a button. No CSS yet — just structure.
<nav>
<a class="logo">MyShop</a>
<ul>
<li><a href="/shop">Shop</a></li>
<li><a href="/about">About</a></li>
</ul>
<button>Log in</button>
</nav>
Right side is live — click Shop, About or Log in and the click is reported inline. Every menu bar you've seen is some version of this <nav>.
Almost every web app you've used has this exact shape: bar on top, rail on the left, content in the middle.
<body>
├── <nav> menu bar — logo, links, 'login'
├── <div class="layout">
│ ├── <aside> sidebar — filters, sub-menu
│ └── <main> content — what the page is about
└── <footer> links, copyright
# every element above is just a <div> or its friends —
# the layout you see is CSS placed on this tree
Read this tree top to bottom and you can name almost any admin dashboard's structure.
Recognize the pattern, don't master the syntax. Most 'fancy' UI is plain HTML wearing CSS.
A <button>, or an <a> styled to look like one. One element, one job.
An <input type='checkbox'> styled into a switch — or a button that flips a CSS class.
An input plus a JS popup. The calendar itself is just a grid of tiny buttons.
A row of <img> in a flexbox; JS decides which slide you're looking at.
<img> + <div> + a border. 'Card' is a nickname for a styled box.
Inputs inside a <form>, plus one <button type='submit'>. The submit button is the whole point.
The most common UI on the web: a field to type in, and a button to send it.
<form>
<label>Email</label>
<input type="email" placeholder="you@example.com">
<label>Password</label>
<input type="password">
<button type="submit">Log in</button>
</form>
Right side is live — hit Log in and the form reports back (JS would intercept it here). Each <label> describes the <input> next to it.
#status addresses exactly one element on the page. Like a house number: one per address.
class="card" can sit on 50 elements at once. Like a team badge. Bootstrap works entirely with these.
CSS and JS use these same names to find elements — the first place all three files meet.
<div> means nothing. <nav>, <main>, <article> say what a region IS.
Screen-reader users navigate by landmarks: they jump straight to <main> and skip the menus.
Search engines read meaning to index pages. Semantic HTML is free SEO.
AI reads semantic markup far better than a soup of <div>s — and so will future-you, three months later.
Every card on the web — product, profile, post — is this shape: image on top, title, text, action.
<div>
<img src="product.jpg" alt="Espresso machine">
<h3>Espresso Machine</h3>
<p>Pulls a proper shot at home.</p>
<button>Add to cart</button>
</div>
Pulls a proper shot at home.
Right side is the live card — click Add to cart. This exact markup becomes a Bootstrap card next, just by adding class names.
The bugs AI (and humans) still ship. You don't have to find every one — just recognize these four.
<div>…</span> — the tree breaks, and the layout goes haywire below it.
Two elements with id="status" — JS grabs the first, so updates land in the wrong place.
Screen readers hear nothing. Every <img> needs an alt="…" describing it.
A button with no click handler, a link to "" — the page looks finished and does nothing.
CSS answers two questions: what does it look like, and where does it sit?
Every element is a rectangle with four concentric rings. This one mental model explains most of CSS.
The text or image itself — the innermost ring.
Space between the content and the border. Breathing room, still inside the box.
The line around it. Also where border-radius (rounded corners) lives.
Space outside the border — the gap to the neighbouring boxes.
Every CSS rule = an address + a list of properties. Three address types cover 95% of CSS.
button { … } styles every button on the page. Coarse, but instant.
.card { … } hits only elements with class="card"; #status hits exactly one element.
The same addresses JS uses to find elements — the first handshake between the three files.
A CSS rule = pick an element, then assign it properties. That is the entire syntax.
button {
padding: 10px 18px; /* room inside */
background: #34d399; /* colour */
border: none;
border-radius: 8px; /* rounded corners */
color: white;
font-size: 15px;
}
Right side is live — that's the exact CSS from the box applied to the button. Hover it. AI writes this; knowing padding / border / radius is how you review it.
Layout = deciding where things sit. CSS has exactly three big tools for that.
Line things up in a row or a column, and space them evenly. Menu bars, card rows.
Rows AND columns at once. Dashboards, photo walls, whole-page layouts.
'If the screen is narrower than 768px, do this.' One codebase, phone and desktop both work.
display: flex puts items in a line. Two axes do all the work — every other flex property is just a knob on one of these.
justify-content moves items along the MAIN axis · align-items moves them along the CROSS axis
The boxes are real: each row is an actual flex container with that exact property. This is what the browser computes.
space-between: items hug the edges, gaps only between · space-around: equal room for every item, half at the edges
Three more properties cover most real flex layouts. All three boxes below are live flex containers.
items too big → new row
B eats the leftover space
main axis turns vertical
wrap: overflow starts a new row · grow: one item fills what's left · column: the line becomes a column
Left: nine boxes in a 3×3. Right: boxes that span. Same idea, far less code than stacking flexboxes.
Bootstrap's col-3 is this same grid with 12 columns: col-3 = 3 of 12 = a quarter of the row
The media query is a switch: same elements, different arrangement per screen size.
/* desktop: sidebar next to content */
.layout { display: flex; }
/* phone: sidebar stacks on top */
@media (max-width: 768px) {
.layout { flex-direction: column; }
}
Right side is a live window — drag the slider past 768px and the sidebar stacks. Below 768px the same page rearranges itself; that is 'responsive'.
A CSS framework: thousands of ready-made styles you switch on with class names — and the next three slides run it live.
That's the whole pitch: consistency for free, so you spend time on the page — not on pixels.
You decide every pixel: padding, colour, border, hover state, size on mobile. Then do it again for every button on every page.
Add a class: class="btn btn-primary". It arrives styled, with hover and mobile behaviour included. Same for cards, nav bars, modals.
Identical HTML, same photo. The right side only adds class names — and it suddenly looks designed.
Pulls a proper shot at home.
Pulls a proper shot at home.
card · card-img-top · card-body · card-title · card-text · btn btn-primary
Everything below is real Bootstrap 5, rendered in this slide. Each piece is one or two class names.
navbar · card · alert · badge · btn — the vocabulary of a thousand dashboards
The same inputs you saw in the HTML part — now with form-* classes. Focus states and spacing included.
form-label · form-control · form-select · form-check form-switch · btn
No <style> block at all — the page's whole look comes from the class names.
<link href="bootstrap.min.css" rel="stylesheet">
<nav class="navbar">…logo + links…</nav>
<div class="container">
<div class="row">
<div class="col-3">sidebar</div>
<div class="col-9">main content</div>
</div>
<button class="btn btn-primary">Click me</button>
</div>
col-3 / col-9 is the grid: 12 columns, 3+9 fills the row. Change the numbers — the layout changes.
You won't memorize Bootstrap — but these six show up in almost every page built with it.
| Class | What you get |
|---|---|
| btn btn-primary | A styled button — hover and focus states included |
| container · row · col-* | The 12-column grid: col-3 + col-9 fills one row |
| navbar | A responsive menu bar — collapses by itself on phones |
| card | A bordered box for image + text + button |
| modal | A popup window — open/close handled for you |
| form-control | A styled input field |
Every Bootstrap layout is rows of 12 equal slots. You pick how many each block takes.
<div class="row"> is a shelf split into 12 equal parts. col-3 takes 3, col-9 takes 9 — they fill the shelf.
col-6 + col-6 = halves, col-4 × 3 = thirds. Change the numbers and the page re-flows — no other CSS needed.
col-md-6 means '6 columns on medium screens and up, stack below.' The same row is a phone layout and a desktop layout.
HTML is the picture, CSS is its outfit — JS is the reflexes.
The pattern behind every interaction on the web: listen for an event, then change the page.
const btn = document.querySelector("#save");
btn.addEventListener("click", () => {
document.querySelector("#status")
.textContent = "Saved ✓";
});
Right side is live — click Save and the virtual console logs the whole event. The button now DOES something: click fires, JS edits the page.
JS reads and edits the DOM — the page's live tree of elements — so everything below is 'just' editing that tree.
Tabs, dropdown menus, toasts — the same element, switched on and off.
Check the email looks right before it's sent. Show an error, block the submit.
Ask the server for JSON — a list of orders, prices, posts — then paint it into the page.
Find, read, change, add to the page's elements. The page is data you can manipulate.
You won't memorize these — but when AI writes them, you should recognize what each line is doing.
| Method | What it does |
|---|---|
| document.querySelector("#x") | Find the first element by its CSS address — the same #x / .x names CSS uses. |
| document.querySelectorAll(".x") | Find every matching element — a list you can loop over. |
| el.textContent = "…" | Set plain text — safe, no HTML parsing. |
| el.innerHTML = "…" | Set HTML markup — the browser parses and renders it. |
| el.addEventListener("click", fn) | Run fn when the event fires. One element can listen to many. |
| el.classList.toggle("open") | Add the class if missing, remove it if present — a switch. |
| input.value | Read what the user typed — or set the default. |
| createElement + append | Build a new element from scratch and attach it to the page. |
The pattern is always the same: listener → handler → edit the page. Only the event name changes.
The classic — buttons, links, tabs. Fires when the user activates the element.
Fires on every key pressed. The key behind live search boxes and autocomplete — no 'Search' button needed.
Fires when a form is sent. Call e.preventDefault() in the handler to stop the page reload — then JS takes over.
Two buttons, one number. The JS variable is the truth; the page only mirrors it.
let count = 0;
const out = document.querySelector("#count");
document.querySelector("#plus")
.addEventListener("click", () => {
count += 1;
out.textContent = count;
});
document.querySelector("#reset")
.addEventListener("click", () => {
count = 0;
out.textContent = count;
});
Right side is live — click +1 / Reset; the console mirrors the data-then-paint order. That order is the idea behind every app framework.
Show/hide is almost always just flipping a CSS class. JS never touches 'display' directly.
const tabs = document.querySelectorAll(".tab");
tabs.forEach(tab => tab.addEventListener("click", () => {
tabs.forEach(t => t.classList.remove("active"));
tab.classList.add("active");
// CSS: .tab.active shows its panel, the rest hide
}));
Right side is live — click a tab; the console shows the .active class moving. The same trick powers dropdowns, toasts and accordions.
Catch the submit, check the value, show or clear the error — without a page reload.
const form = document.querySelector("#signup");
form.addEventListener("submit", (e) => {
e.preventDefault(); // don't reload
const email = document.querySelector("#email").value;
const err = document.querySelector("#err");
if (!email.includes("@")) {
err.textContent = "That email looks wrong.";
} else {
err.textContent = ""; // clear it
form.reset();
}
});
Right side is live — submit a bad email, then a good one; the console logs each branch. This is the skeleton of every valid-email message.
input → filter the data → re-render the list. This one pattern is behind every filter UI you've ever used.
const foods = ["Pizza", "Pho", "Sushi", "Banh mi", "Tacos"];
const input = document.querySelector("#search");
const list = document.querySelector("#list");
input.addEventListener("input", () => {
const hits = foods.filter(f =>
f.toLowerCase().includes(input.value.toLowerCase()));
list.innerHTML = hits
.map(f => "<li>" + f + "</li>")
.join("");
});
Right side is live — type in the box and the console logs each filter. filter() picks matches, map() makes rows, join() glues them.
Networks fail: the server is down, the wifi drops, the route doesn't exist. The catch is part of the feature.
async function loadOrders() {
const list = document.querySelector("#orders");
try {
const res = await fetch("/api/orders");
const orders = await res.json();
list.innerHTML = orders
.map(o => "<li>" + o.name + "</li>")
.join("");
} catch (err) {
list.textContent = "Couldn't load orders — retry?";
}
}
Right side simulates the network (no real endpoint) — Load orders, or tick Simulate failure to hit the catch. Judge any AI fetch by its catch.
This is why web apps feel alive while loading — and why async bugs feel spooky.
fetch fires and JS moves on immediately. The page keeps scrolling and animating while the server thinks.
Callbacks queue up: 'when the data arrives, do this.' Order matters — you can't paint data that hasn't landed.
The UI stays alive during the wait. A spinner is the interface admitting 'I'm still loading'.
'Where did my data go?' is usually 'I used it before it arrived.' The await/then discipline exists for exactly that.
Two pieces: one source of truth, and a function that repaints the page from it.
// one source of truth
let state = { open: 3, cart: [] };
// the page is a picture of the state
function render() {
document.querySelector("#open").textContent = state.open;
document.querySelector("#cart").textContent =
"Cart: " + state.cart.length;
}
state.open += 1; // 1. change the data
render(); // 2. repaint the page
Right side is live — the two buttons change state, render() repaints, the console logs it. This is exactly what React & Vue automate.
HTML, CSS and JS all talk about the same element — each does its own job.
<p id="status"></p> — the element exists, still empty.
#status { color: red; } — whenever it has text, the text is red.
document.querySelector("#status") gets filled the moment you click the button.
The server's job ends at the send. Everything you see and click is HTML, CSS and JS running in your browser.
You'll now let AI generate these files — your job is to read them, judge them and fix them.