Web fundamentals without the cargo cult
HTML, CSS, and vanilla JavaScript — the 30 percent of each you actually need. Build a real, semantic, fast page before you reach for a framework.
Prerequisites
01.4
Stack
a browser with devtoolsVS Codeno frameworkno build stepa static host (GitHub Pages or Cloudflare Pages)
By the end of this module
- Write semantic HTML that a screen reader and a search bot both understand.
- Lay out any modern page with Flexbox, Grid, and custom properties — no float hacks, no class soup.
- Use vanilla JS for DOM, fetch, and modules without reaching for jQuery or React.
- Ship a portfolio page that scores 95 plus on Lighthouse across all four categories, on a free static host.
Most students reach for React in week one and never write a <button> that wasn’t generated by a component library. That’s the cargo cult: copying the artifacts of senior teams without copying the reasoning. The reasoning is that the platform — HTML, CSS, JavaScript — is already extraordinarily good. A framework is a bet you should make consciously, after you can ship without one.
This module is the bet you should be able to skip. Build a real portfolio page in plain HTML, CSS, and JS. Make it semantic. Make it accessible. Make it fast. Deploy it. Then, and only then, the next module is allowed to introduce React.
The opinionated take, said early so you can argue with it: roughly 80 percent of student “frontend” projects on GitHub would be smaller, faster, and more maintainable as a single HTML file with a stylesheet. You will not believe this until you ship one.
Set up
mkdir portfolio && cd portfolio
git init
cat > index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>your name — portfolio</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>your name</h1>
</body>
</html>
EOF
touch style.css app.js
That’s the whole project for now. No npm init. No package.json. No bundler. Open index.html directly in a browser. You should see your name. If you can resist the urge to install something, you have already learned the most important lesson in this module.
For deploy at the end, you’ll need a GitHub account and either GitHub Pages turned on for the repo or a free Cloudflare Pages account. Both serve plain HTML for free with HTTPS.
Read these first
Three references, in order, then stop:
- MDN — HTML elements reference. docs · 30 min skim · the canonical list. Bookmark it forever.
- Josh Comeau — An interactive guide to flexbox. post · 30 min · the clearest mental model for the most-used layout primitive.
- Kevin Powell — Learn CSS Grid the easy way. video · 25 min · grid is half of modern layout. Watch it once.
- Lea Verou — CSS variables: var() is for custom properties. post · 10 min · old, still right.
- web.dev — Lighthouse scoring. docs · 10 min · what you are actually being measured on.
Resist reading anything else until your portfolio is up. The Reddit threads about CSS frameworks will still be there after you ship.
Step 1 — HTML, semantically
The first thing students get wrong is using div for everything. The browser, the screen reader, search engines, and “Reader mode” in Safari all key off semantic elements. Use the right one.
<header>
<nav aria-label="Primary">
<a href="#work">Work</a>
<a href="#writing">Writing</a>
<a href="#contact">Contact</a>
</nav>
</header>
<main>
<section id="intro" aria-labelledby="intro-heading">
<h1 id="intro-heading">your name</h1>
<p>One sentence about what you build.</p>
</section>
<section id="work" aria-labelledby="work-heading">
<h2 id="work-heading">Selected work</h2>
<article>
<h3>Project name</h3>
<p>What it is. What you used. Why it mattered.</p>
<a href="https://...">Live</a> · <a href="https://github.com/...">Code</a>
</article>
</section>
</main>
<footer>
<p>© 2026 your name</p>
</footer>
Five rules to internalize:
| Element | Use it for |
|---|---|
<main> | Exactly one per page. The primary content. |
<section> | A thematic chunk with a heading. |
<article> | A self-contained item — a blog post, a project card, a comment. |
<nav> | Navigation. Give it aria-label if there are multiple. |
<button> vs <a> | Buttons do things on the page. Links go somewhere. Never style a div to look like either. |
Run your page through the WebAIM contrast checker and the browser’s built-in accessibility audit (Chrome devtools, Lighthouse tab). Fix everything red before moving on.
Step 2 — CSS that won’t shame you
The 30 percent of CSS you need is small. Memorize the names of these properties; you can look up syntax forever.
display: flexanddisplay: grid— every layout you’ll writegap— spacing between flex/grid children. Stop using margins for this.clamp(min, preferred, max)— fluid type and spacing without media queriesaspect-ratio— for images, video, cards- Custom properties (
--color-text: ...) — your design tokens :has()— parent selector, supported everywhere now- Container queries — components that respond to their container, not the viewport
A starting style.css you can grow into:
:root {
color-scheme: light dark;
--bg: oklch(98% 0 0);
--fg: oklch(18% 0 0);
--muted: oklch(55% 0 0);
--accent: oklch(62% 0.18 250);
--text-base: clamp(1rem, 0.92rem + 0.4vw, 1.125rem);
--text-h1: clamp(2.5rem, 1.5rem + 4vw, 5rem);
--text-h2: clamp(1.75rem, 1.2rem + 2vw, 2.75rem);
--space-section: clamp(3rem, 2rem + 4vw, 8rem);
--measure: 65ch;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: oklch(14% 0 0);
--fg: oklch(96% 0 0);
--muted: oklch(70% 0 0);
}
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font: var(--text-base) / 1.6 ui-sans-serif, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
main {
max-width: var(--measure);
margin-inline: auto;
padding: var(--space-section) 1.25rem;
}
h1 { font-size: var(--text-h1); line-height: 1.05; letter-spacing: -0.02em; margin: 0 0 1rem; }
h2 { font-size: var(--text-h2); margin-top: var(--space-section); }
a { color: var(--accent); text-underline-offset: 0.2em; }
a:hover { color: var(--fg); }
section + section { margin-top: var(--space-section); }
Things deliberately not on this page: BEM. Tailwind. A reset library. A framework. You do not need them yet, and importing them now will cost you the intuition this module exists to build.
Skip floats entirely. They are CSS’s appendix — vestigial, occasionally inflamed, never load-bearing in 2026.
Step 3 — JavaScript essentials, no library
Modern vanilla JS is so much better than the 2014 version that most “you need jQuery” tutorials are flat wrong. The four primitives you need:
// app.js
// 1. Selecting elements
const navLinks = document.querySelectorAll('nav a');
// 2. Event listeners
navLinks.forEach((a) => {
a.addEventListener('click', (e) => {
// smooth scroll, etc.
});
});
// 3. Fetch
async function loadProjects() {
const res = await fetch('/projects.json');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
// 4. Rendering — innerHTML when you trust the data, textContent when you don't
function render(projects) {
const list = document.querySelector('#projects');
list.replaceChildren(
...projects.map((p) => {
const li = document.createElement('li');
li.textContent = p.title; // safe against XSS
return li;
})
);
}
Use ES modules so you can split files without a bundler:
<script type="module" src="/app.js"></script>
The ban list for this module: var, ==, document.write, jQuery, anything that “polyfills” something the browser already supports.
Step 4 — Responsive without media-query hell
Most students write five breakpoints and a thousand media queries. The 2026 toolkit makes that obsolete:
| Need | Tool | Example |
|---|---|---|
| Fluid font size | clamp() | font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem) |
| Auto-wrapping grid | grid-template-columns: repeat(auto-fit, minmax(...)) | classic card grid, zero queries |
| Component responds to container, not viewport | @container | a sidebar card that adapts when the sidebar shrinks |
| Stack on small, side-by-side on big | flex-wrap: wrap plus flex: 1 1 20rem | usually beats a media query |
Keep media queries as the last resort. A rule of thumb: if you have more than one media query per stylesheet, you probably should have used clamp or a grid minmax.
Step 5 — Ship it
Lighthouse all four categories. Targets:
| Category | Target |
|---|---|
| Performance | 95 plus |
| Accessibility | 100 |
| Best Practices | 100 |
| SEO | 100 |
The performance score is the one that punishes laziness. Things that cost it:
- An unsized image causing layout shift. Always set
widthandheight. - A 4MB hero JPG. Convert to WebP or AVIF, target under 200KB.
- A web font that blocks render. Self-host, use
font-display: swap, preload only the critical weight. - A render-blocking stylesheet on a slow network. Inline critical CSS for an above-the-fold page if needed.
Deploy:
git add -A && git commit -m "feat: portfolio v1"
gh repo create your-name-portfolio --public --source=. --push
# In GitHub: Settings → Pages → Deploy from branch → main → /
Custom domain, if you want one: add a CNAME file with your domain, point DNS at GitHub Pages or Cloudflare Pages. HTTPS is automatic on both.
Run Lighthouse against the live URL, not localhost. Anything under 95 means you’re shipping bytes you don’t need.
Going deeper
When you have specific questions, in this order:
- web.dev — Learn HTML / CSS / Performance / Accessibility · the official Google curriculum. Newer than half the books on the topic.
- Every Layout by Heydon Pickering and Andy Bell · how to think in layout primitives, not pages.
- MDN — JavaScript guide · the canon. Read the sections you don’t know cold.
- Smashing Magazine — modern CSS articles · for new properties as they ship.
- Steve Krug — Don’t Make Me Think. book · still the best 2-hour read on usability.
Skip the “Top 10 CSS Frameworks 2026” listicles. They are SEO content and will not help you write a single button.
Checkpoints
If any wobbles, reread the corresponding section.
- Why does using a
divinstead of a<button>for a clickable thing actively break accessibility? Name two specific failure modes. - Explain
clamp()in plain English, and give one example where it replaces three media queries. - What is the difference between
gap,margin, andpadding, and which one is correct for spacing items inside a flex container? - Why is
textContentsafer thaninnerHTMLwhen rendering data from a network request? - List the four Lighthouse categories. For the Performance score, name two specific things that almost always cost you points and how to fix each.
Ship the live URL before moving on. Next: 03.2 Modern frontend — React and Next.js, where you’ll learn when a framework actually earns its weight.