πŸ’» Computer Science · Undergraduate · CS 130

Web Development: HTML, CSS & JavaScript

A hands-on first course in building websites. Starting from how the web actually works, you will write real HTML for structure, CSS for design and layout, and JavaScript to make pages interactive, then combine all three into a small project you build yourself. Every lesson teaches the code on the page, so you can learn the whole craft for free at your own pace with nothing but a text editor and…

Start the interactive course (quizzes, progress, videos) →

Free forever. No sign-up, no ads. 15 lessons. The full lesson text is below so you can read it right here.

Module 1: How the Web Works

The client/server model, HTTP, URLs, and the three languages of the front end. You will build the mental model that every later lesson quietly depends on: the browser asks for text, receives text, and paints it into pages.

Clients, Servers, and HTTP

  • Describe the client/server model of the web.
  • Trace what happens between typing a URL and seeing a page.
  • Identify the roles of HTTP requests and responses.

The big picture

Every time you open a web page, two computers hold a short conversation: your browser asks for a page, and a distant computer sends it back. Learning who is talking, what they say, and how a web address is read turns page loading from a mystery into a simple, traceable process. Everything else in this course sits on top of this one exchange.

Key idea: the browser asks a server for text, receives that text, and paints it into the page you see.

Clients and servers

A client is the program that asks for and displays pages, which for you is the web browser. A server is a computer whose job is to store web pages and hand them out when asked. Think of a restaurant: you (the client) order from a waiter, and the kitchen (the server) prepares and returns your dish.

The word "server" describes a role, not a size. It might be a giant data center or a spare laptop under a desk; what makes it a server is simply that it waits for requests and answers them. This whole arrangement is the client/server model, and it underlies almost everything on the internet.

Key idea: a client requests, a server responds, and that split is the backbone of the web.

What happens when you visit a page

Suppose you type an address and press Enter. Here is the sequence, simplified but honest:

  1. The browser reads the URL (the web address) and works out which server to contact.
  2. The name in the URL, like example.com, is turned into a numeric IP address by a lookup system called DNS (the Domain Name System). DNS works like a phone book for the internet: you know a name, and it gives back the number.
  3. The browser opens a connection to that server and sends an HTTP request, a short message that says, in effect, "please send me this page."
  4. The server prepares the page and sends back an HTTP response holding the content, usually HTML text.
  5. The browser reads that HTML, fetches any extra files it names (styles, images, scripts), and draws the finished page. Drawing the page is called rendering.

Notice that a single page is rarely one download. The HTML arrives first and acts like a shopping list that names a stylesheet, some images, and a script; the browser then makes a fresh request for each. A typical news article can trigger dozens of requests before it looks finished, which is why pages often appear in stages: text first, then images popping in, then interactivity waking up.

Key idea: loading a page is one request for the HTML followed by many small requests for the files it mentions.

Anatomy of a URL

A URL packs several pieces of information into one line. Take this example and read it part by part:

https://shop.example.com/products/shoes?color=red
PartValue hereMeaning
SchemehttpsWhich protocol to use
Hostshop.example.comWhich server to contact
Path/products/shoesWhich resource on that server
Query stringcolor=redExtra parameters after the ?

Reading a URL as these parts, rather than as one long blur, makes the whole system feel far less mysterious. The query string is everything after the ?; it is a set of extra values the server can use to tailor the response, such as which color of shoe to show.

Key idea: a URL names a protocol, a server, a resource, and optional parameters, all in one line.

HTTP: the request-and-reply postcard

HTTP stands for HyperText Transfer Protocol. A protocol is just an agreed set of rules for how two computers talk. It helps to picture HTTP as a postcard exchange: you mail a request card that names what you want, and a reply card comes back with the answer. An HTTP request names a method (the kind of action wanted) and a path (which resource). The two you meet first are GET, which asks to read something, and POST, which sends data to the server, such as a filled-in form. A request also carries headers, short labeled lines of extra information, like which languages the browser prefers.

Every response comes with a numeric status code that reports how it went. You have almost certainly seen 404, which means the page was not found. A few worth knowing, grouped by their leading digit:

CodeFamilyMeaning
2002xx successOK, the request succeeded
301 / 3023xx redirectThe page lives at a different address
4034xx client errorForbidden, you are not allowed in
4044xx client errorNot Found, no such resource
5005xx server errorSomething broke on the server

The pattern is worth memorizing: 2xx means success, 3xx means "look elsewhere," 4xx means the client asked for something wrong, and 5xx means the server itself failed. Knowing the family tells you where to look when something breaks: a 4xx is usually your mistake, a 5xx is the server's.

Key idea: a request carries a method and a path, and the response carries a status code whose first digit tells you the outcome.

The secure version

Modern sites use HTTPS, the same protocol with encryption added, so data traveling between your browser and the server cannot be read by anyone in between. That is the padlock icon near the address bar. Encryption does two jobs here: it keeps the conversation private, and it verifies that the server really is who it claims to be, using a certificate from a trusted authority. As a developer you get HTTPS mostly for free; the important point is that it protects the conversation without changing how you write pages.

Key idea: HTTPS is HTTP wrapped in encryption that keeps the exchange private and proves the server's identity.

A note on memory: cookies

HTTP is stateless, meaning each request stands alone and the server remembers nothing about the previous one by default. So how does a shopping cart remember your items? The answer is the cookie: on its first reply, a server can ask the browser to store a small piece of text, and the browser sends it back on every later request, letting the server recognize a returning visitor. The protocol stays stateless while the feeling of memory is layered on top.

Key idea: the web forgets between requests, and cookies are how sites appear to remember you anyway.

HTTP on the wire: a real request, line by line

The postcard analogy becomes concrete when you look at the actual text the browser sends. HTTP messages are plain, human-readable text. Here is the request your browser would transmit for the shoe page above:

GET /products/shoes?color=red HTTP/1.1
Host: shop.example.com
User-Agent: Mozilla/5.0 (a short description of the browser)
Accept: text/html
Accept-Language: en-US

Walk through it line by line. Line 1 is the request line: the method (GET, "please send me this"), the path plus query string (which resource, with parameters), and the protocol version. Line 2, the Host header, names the site, which matters because one physical server often hosts many sites and needs to know which one you mean. The remaining lines are more headers: User-Agent describes the browser, Accept says what kind of content is wanted, and Accept-Language states the user's preferred language, which is how sites greet you in the right tongue. A blank line ends the headers. A GET request has no body; a POST would carry the form data after that blank line.

The server's reply is text too:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 5120
Cache-Control: max-age=600

<!DOCTYPE html>
<html> ...the page itself follows... </html>

Line 1 is the status line: protocol, code (200), and a short reason phrase (OK). Content-Type tells the browser how to interpret the body, here HTML text encoded as UTF-8; the same response mechanism delivers images and scripts with different types. Content-Length counts the body's bytes so the browser knows when the message ends, and Cache-Control permits the browser to reuse this copy for 600 seconds instead of asking again. Then a blank line, then the body, the actual HTML you will spend the rest of this course writing.

You can watch this exchange yourself today: every major browser's developer tools have a Network tab that lists each request, its method, its status code, and every header. Opening it on any page turns this lesson from theory into observation, and it is the first tool professionals reach for when a page misloads. When you later meet forms (which choose between GET and POST), caching, and APIs, they will all be manipulations of exactly these few lines of text.

Common misconceptions

  • "The internet and the web are the same thing." The internet is the global network of connected computers and cables. The web is just one service running on top of it, the one that uses HTTP to move pages. Email, video calls, and online games also ride the internet without being part of the web.
  • "A website lives inside my browser." The browser only holds a temporary copy it downloaded. The real files live on a server, so editing your local copy never changes what others see until you upload it.
  • "HTTPS makes my whole site safe." HTTPS encrypts data in transit and proves the server's identity, but it does not fix insecure code, weak passwords, or server bugs. It protects the road, not the house.
  • "Loading a page is a single download." The HTML is only the first file; it triggers many more requests for styles, images, and scripts.

Recap

  • The client (your browser) requests pages; the server stores and sends them.
  • DNS turns a domain name into an IP address so the browser knows which machine to contact.
  • A URL names a scheme, host, path, and optional query string.
  • An HTTP request has a method (GET reads, POST sends data); the response has a status code.
  • Status families: 2xx success, 3xx redirect, 4xx client error, 5xx server error.
  • HTTPS adds encryption; cookies add the illusion of memory to a stateless protocol.

Sources

  1. MDN Web Docs. (n.d.). Overview of HTTP. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). How the web works. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). What is a URL?. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). HTTP response status codes. Mozilla. developer.mozilla.org
  5. MDN Web Docs. (n.d.). HTTP request methods. Mozilla. developer.mozilla.org
  6. MDN Web Docs. (n.d.). Using HTTP cookies. Mozilla. developer.mozilla.org
  7. Google. (n.d.). How browsers work. web.dev. web.dev
  8. Khan Academy. (n.d.). The Internet [Computing unit]. Khan Academy. khanacademy.org β†—
Key terms
Client
The program, usually a browser, that requests and displays web pages.
Server
A computer that stores web resources and sends them when requested.
HTTP
The protocol of rules browsers and servers use to exchange requests and responses.
URL
A web address that tells the browser what resource to fetch and from where.
DNS
The Domain Name System that translates a domain name into a numeric IP address.
Status code
A number in an HTTP response reporting the outcome, such as 200 or 404.
Header
A labeled line of extra information carried by an HTTP request or response.
HTTPS
HTTP with encryption that keeps the conversation private and verifies the server's identity.

HTML, CSS, and JavaScript: The Three Languages

  • Distinguish the roles of HTML, CSS, and JavaScript.
  • Set up a folder and files for a website.
  • Open an HTML file in a browser.

The big picture

A modern web page is built from three separate languages, each with one clear job. Keeping those jobs apart is one of the most valuable habits a web developer can build, and it pays off on your very first project and on the largest sites in the world alike. This lesson names the three languages, explains why they stay separate, and gets your first project folder ready.

Key idea: HTML gives structure, CSS gives style, and JavaScript gives behavior, and each stays in its own place.

The three roles

Here is the whole trio in one picture. Think of a human body: HTML is the skeleton, CSS is the clothing, and JavaScript is the muscles that make it move.

  • HTML (HyperText Markup Language) provides the structure and content: the headings, paragraphs, images, and links. It is the bones and words of the page.
  • CSS (Cascading Style Sheets) provides the presentation: colors, fonts, spacing, and layout. It is the clothing and paint.
  • JavaScript provides the behavior: what happens when the user clicks, types, or scrolls. It is the muscles that make the page respond.

Another useful comparison is a house. HTML is the framing and walls, CSS is the paint and furniture, and JavaScript is the electricity that makes the lights turn on when you flip a switch. You could live in a house with framing but no paint and no wiring; it would be plain but usable. That mirrors the web exactly: a page with only HTML still works, just without style or interaction.

Key idea: a page can stand on HTML alone, then CSS dresses it and JavaScript brings it to life.

Why separate the three?

This split is called separation of concerns, and it is not mere tidiness. When structure, presentation, and behavior live in different places, you can restyle an entire site by editing one stylesheet without touching a single word of content. You can hand the HTML to a writer and the CSS to a designer and they will not collide. And when something breaks, you know where to look:

SymptomWhere the problem lives
A missing heading or wrong textHTML
A wrong color, font, or spacingCSS
A button that does nothing when clickedJavaScript

Mixing the three, by contrast, produces pages that are painful to change and to debug, because a single tangle of code does all three jobs at once.

Key idea: separating structure, style, and behavior makes a site far easier to change and to debug.

The tools you need

You need only two free things you already have or can install: a text editor to write code and a web browser to view it. A plain editor works, though a code editor with syntax highlighting is nicer because it colors your tags and warns you about typos. You do not need to install a server for this course; a browser can open HTML files directly from your disk. That low barrier is one of the web's great gifts: the same technology that runs billion-dollar sites is fully available to a beginner with a laptop and no budget.

Key idea: a text editor and a browser are the entire toolkit for this course.

Setting up your first project

Create a folder for your site. Inside it, make three files with a common, sensible naming scheme:

my-site/
    index.html     the page structure (HTML)
    styles.css     the design (CSS)
    script.js      the behavior (JavaScript)

The name index.html is special: when a browser or server is pointed at a folder, it looks for index.html as the default page. This is why you can visit a site's home page by typing just the domain with no filename; the server quietly serves the index. To view your work, open index.html in a browser by double-clicking it or dragging it onto a browser window.

Key idea: index.html is the default page a browser loads when it is pointed at a folder.

The edit-refresh loop

Every time you change a file and save it, refresh the browser to see the update. That short loop, edit then refresh, is the heartbeat of front-end development. You will run it thousands of times: make a small change, save, glance at the result, and repeat. Keeping each change small is a genuine skill, because when something looks wrong you will know exactly which edit caused it.

Key idea: change one small thing, save, refresh, and repeat, so every mistake is easy to trace.

The same page, three layers deep

Watch the division of labor on an actual page. Stage one is pure HTML, a complete and legal page:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Quote of the Day</title>
  </head>
  <body>
    <h1>Quote of the Day</h1>
    <p id="quote">The best way out is always through.</p>
    <button id="next">Another</button>
  </body>
</html>

Open this in a browser and it works: black serif text on white, a plain gray button that does nothing yet. That is the skeleton standing on its own. Stage two adds one CSS file, connected by a single line in the head, <link rel="stylesheet" href="styles.css">:

body { font-family: Georgia, serif; background: #f4efe8; }
#quote { font-size: 24px; font-style: italic; }
button { padding: 8px 16px; }

Refresh, and the same content is suddenly designed: a warm background, a large italic quote, a comfortable button. Not a word of the HTML changed, which is the entire promise of separation of concerns made visible. Stage three adds one JavaScript file, connected by <script src="script.js"></script> placed just before </body>:

const quote = document.querySelector("#quote");
const next = document.querySelector("#next");
next.addEventListener("click", function () {
  quote.textContent = "Well begun is half done.";
});

Refresh again and the button now does something: clicking it swaps the quote. The words in that code will make full sense in Module 7; for now, notice its shape: it finds the two elements, then describes what should happen when one is clicked. Structure, then style, then behavior, each in its own file, each added without touching the others.

What the browser does with the three files

The order of events inside the browser explains several rules you will meet later. The browser reads the HTML top to bottom and builds its internal model of the structure (the DOM you will meet in Module 7). When it hits the <link> in the head, it fetches the stylesheet and combines it with the structure to decide how everything should look. When it hits a <script> tag, it stops and runs the code at that spot before continuing. That pause is precisely why the script tag goes at the end of the body: run it in the head and it executes before the paragraphs and buttons below it exist, so code that looks for them finds nothing. The stylesheet, by contrast, is linked in the head on purpose, so the page never appears unstyled and then jumps into shape. One sequence, two placement rules, no mystery.

A practical habit completes the picture: keep the three files honest about their roles. When you want to change how something looks, resist the urge to reach for JavaScript; when you want to change what a page says, do not bury the words in a script. Ask "is this structure, style, or behavior?" before you open a file, and the file to open answers itself. Professionals reviewing each other's code ask exactly this question constantly, and the habit costs nothing to build from the first project.

How each language fails, and why that helps you

The three languages do not just have different jobs; they fail in different styles, and knowing the styles tells you where to look when something goes wrong. HTML fails silently and forgivingly: misspell a tag name and the browser treats it as an unknown element and renders its text anyway; forget a closing tag and the browser guesses. The page rarely breaks outright, it just drifts from what you meant. CSS fails silently and precisely: a misspelled property or an illegal value causes the browser to discard exactly that one declaration and carry on, so a single typo in colr: navy; costs you the color and nothing else, with no message anywhere on the page. JavaScript fails loudly but invisibly: a typo usually throws an error that stops the script entirely, and the report appears only in the developer console, which the ordinary page never shows. The practical consequences: when content looks wrong, reread your HTML and validate it; when one style is missing but the page is otherwise fine, suspect a typo or a losing cascade battle in that one CSS rule; when a whole interaction is dead, open the console first, because the explanation is almost certainly printed there in red. Three languages, three failure signatures, three first moves. Debugging stops being guesswork the moment you can name which layer's signature you are seeing.

Common misconceptions

  • "You need expensive software to build a website." A plain text editor and any browser are enough to build and view a complete site. Everything in this course uses free tools you likely already have.
  • "HTML is a programming language." HTML is a markup language; it labels and structures content but has no logic, loops, or decisions. JavaScript is the programming language of the trio. Blurring this leads beginners to expect HTML to do things it simply cannot.
  • "Mixing styling into the HTML saves time." It feels faster at first, but tangled structure and style become very hard to change later. Separating them is a habit worth building from day one.
  • "All three files are equally required to see anything." HTML alone renders a plain page. CSS and JavaScript are enhancements layered on top.

Recap

  • HTML is structure, CSS is presentation, JavaScript is behavior.
  • Separation of concerns keeps each in its own place, so each can change independently.
  • The tools are just a text editor and a browser; no server is needed to learn.
  • A site is a folder, and index.html is the default page.
  • Front-end work runs on the edit-save-refresh loop, one small change at a time.

Sources

  1. MDN Web Docs. (n.d.). Your first website. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). Web standards. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). What is CSS?. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). What is JavaScript?. Mozilla. developer.mozilla.org
  5. Google. (n.d.). Learn HTML. web.dev. web.dev
  6. Google. (n.d.). Learn CSS. web.dev. web.dev
  7. Khan Academy. (n.d.). Intro to HTML/CSS: Making webpages [Online course]. Khan Academy. khanacademy.org β†—
Key terms
HTML
The markup language that defines the structure and content of a web page.
CSS
The language that controls the visual presentation of HTML: color, spacing, and layout.
JavaScript
The programming language that adds interactive behavior to a web page.
Text editor
A program for writing and editing plain-text code files.
index.html
The conventional default filename a browser loads when pointed at a folder.
Separation of concerns
The practice of keeping structure, presentation, and behavior in distinct places so each can change independently.

Module 2: HTML Structure and Semantics

Elements, tags, attributes, the document skeleton, and meaningful semantic markup. You will learn not just how to write valid HTML but how to write HTML that communicates meaning to browsers, search engines, and assistive technology.

Elements, Tags, and the Document Skeleton

  • Write valid HTML elements with opening and closing tags.
  • Build the standard HTML5 document skeleton.
  • Add attributes to elements.

The big picture

HTML describes a page as a set of building blocks called elements, each wrapped in tags. Once you can read a tag, nest elements tidily, add attributes, and type the standard page skeleton, you can write valid HTML for any page. This lesson gives you that foundation, the shape you will start every file from.

Key idea: an HTML page is a tree of elements written with tags, and every page begins from the same skeleton.

Elements and tags

An element is a piece of content wrapped in tags. Most elements have an opening tag and a closing tag, and the content sits between them. A closing tag looks just like the opening tag with a slash added.

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

Here <p> is the opening tag, </p> is the closing tag, and the text between them is the content. Think of tags as a labeled box: the opening tag is the lid going on, the closing tag is the lid coming off, and whatever sits inside is the content. The letters p are the tag name, which tells the browser what kind of element this is. Tag names are not case sensitive, but the universal convention is lowercase, which keeps your files consistent and easy to read.

Key idea: an element is an opening tag, some content, and a matching closing tag.

Nesting

Elements can contain other elements. This is called nesting, and it must be tidy: an element that opens inside another must also close inside it, like properly matched brackets.

<p>Reading is <strong>very</strong> rewarding.</p>

The <strong> element opens and closes entirely inside the paragraph, which is correct. Overlapping tags, where one element closes after another one opens, is invalid and confuses the browser. Picture nesting as a tree: the paragraph is a branch, and the <strong> is a twig growing from it. Every element has exactly one parent and can have many children, and this parent-child tree is the same structure JavaScript will later walk through as the DOM.

Key idea: nested elements must open and close in order, forming a clean parent-child tree.

Attributes

An attribute adds extra information to an element. Attributes go inside the opening tag as name-value pairs, with the value in quotes. Think of them as settings written on the label of the box. For example, the lang attribute states the page's language:

<html lang="en">

An element can carry several attributes at once, separated by spaces, and their order does not matter. Two you will use on almost every project are id, which gives one element a unique name, and class, which tags one or many elements with a shared label. You will lean on both heavily when you reach CSS and JavaScript, because they are how you point at specific elements to style or change them.

Key idea: attributes are name-value settings inside the opening tag, with id naming one element and class labeling many.

Empty elements

A few elements are empty (also called void): they hold no content and therefore have no closing tag. The line break <br> and the image <img> are common examples. Because there is nothing to wrap, writing <br></br> would be meaningless; the single tag is complete on its own.

Key idea: empty elements like the line break and the image are a single self-contained tag with no closing partner.

The document skeleton

Every real HTML page starts from the same skeleton. Learn it by heart; you will type it at the top of every file you write.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My First Page</title>
  </head>
  <body>
    <h1>Hello, web!</h1>
    <p>This is my first page.</p>
  </body>
</html>

Reading it line by line: <!DOCTYPE html> declares the page as modern HTML5 and must be the very first line so the browser uses its standard, predictable rendering rules.

The <html> element wraps the whole document and is called the root. The <head> holds information about the page that is not shown in the main window: <meta charset="UTF-8"> declares the character encoding so accented letters and symbols display correctly, the viewport line sets up mobile behavior, and <title> appears on the browser tab and in search results. The <body> holds everything the visitor actually sees.

Key idea: the head holds information about the page, and the body holds what the visitor sees.

Why the skeleton matters

It is tempting to skip parts of the skeleton because a browser will often display your content even if you leave some out. Resist that temptation. A missing DOCTYPE can throw the browser into a legacy "quirks mode" that renders your careful CSS differently, and a missing charset can turn curly quotes into garbled symbols. In fact the whole point of that short first line is to flip the browser into standards mode: earlier doctypes were long, intimidating strings, and HTML5 shrank it to the shortest text that reliably does the job. Typing the full skeleton every time is a small discipline that prevents a whole category of baffling bugs.

Key idea: typing the complete skeleton, doctype first, keeps the browser in standards mode and your pages predictable.

Attributes in combination

Real elements often carry several attributes cooperating. Read this one:

<p id="intro" class="lead highlight" lang="en">Welcome!</p>

The id="intro" names this one paragraph uniquely, so a stylesheet or script can address exactly it. The class="lead highlight" attaches two classes at once, separated by a space: an element can carry any number of classes, and each class can be shared with any number of elements. That many-to-many relationship is why classes are the workhorse of styling: lead might set a larger font used by a dozen opening paragraphs, while highlight adds a yellow background used by anything needing attention. The lang attribute here overrides the page language for this element, useful when a French quotation sits inside an English page so screen readers pronounce it correctly. One element, three attributes, three different jobs, all in the opening tag.

Comments: notes to your future self

HTML lets you write notes the browser ignores completely, wrapped in a special marker:

<!-- Navigation starts here. Edit links in one place. -->

Everything between <!-- and --> is invisible on the page. Use comments to label the sections of a long file, to explain a non-obvious choice, or to temporarily disable a chunk of markup while testing (wrap it in a comment rather than deleting it). One caution: comments are only hidden from the rendered page, not from readers of the source. Anyone can view a page's source code, so never leave anything private in a comment.

Writing about angle brackets: character references

A puzzle you will hit the moment you write a page about HTML itself: if <p> starts a paragraph, how do you ever display the text "<p>" on screen? Typing it raw makes the browser create a paragraph rather than show the characters. The answer is character references, spellings that stand for a character without triggering its special meaning: &lt; renders as <, &gt; renders as >, and &amp; renders as the ampersand itself. So the visible text "<p>" is typed as &lt;p&gt; in the source. References also cover characters that are hard to type, such as &copy; for the copyright sign. Every code example on this very page uses this mechanism under the hood, which is a nicely self-referential proof that it works.

Check yourself: the validator

Because browsers silently repair broken markup, your best defense is a tool that refuses to be polite. The W3C's free markup validator at validator.w3.org accepts a page (by upload or paste) and lists every violation of the HTML standard with a line number: unclosed elements, stray closing tags, duplicate ids, misplaced content. A worked example of why this matters: leave a <li> unclosed before another <li> and the browser quietly closes it for you and the page looks fine; leave a <div> unclosed and the browser swallows the entire rest of the page inside it, and your footer mysteriously inherits styles meant for the sidebar. The validator catches both instantly, and the page might look identical either way, which is exactly the danger. Validating is a thirty-second habit that pays for itself the first time it finds the unclosed tag you spent an hour hunting. Make it part of finishing any page: write, validate, fix, then style.

Two small rules that prevent weird bugs

Always put attribute values in double quotes. HTML tolerates unquoted values in narrow cases, but the tolerance ends the moment a value contains a space, and class=big card silently becomes a class of "big" plus a mystery attribute named "card". Quoting every value costs two keystrokes and removes the entire category of bug. Second, some attributes are boolean: their mere presence switches something on, and they take no value at all. You will meet required, disabled, and checked on form controls later; each is written bare, as in <input required>. Writing required="false" is a classic trap, because the attribute being present at all means yes, whatever value it carries. Finally, give every page a real <title>: it names the browser tab, the bookmark, and the search result, and it is the first thing a screen reader announces on arrival, so "Untitled Document" is a small unkindness to every visitor at once.

Common misconceptions

  • "If I forget a closing tag, the browser shows an error." Browsers are extremely forgiving and will try to guess what you meant, often nesting elements in ways you did not intend. The page may look almost right while behaving strangely, which is harder to debug than a clean error.
  • "The head is optional decoration." The head carries the charset, the viewport rule, the title, and links to your CSS. Leaving it out causes garbled text, broken mobile layouts, and unstyled pages.
  • "Indentation changes how HTML works." Whitespace is for humans; the browser collapses runs of spaces and ignores your line breaks between tags. Indent for readability, but never rely on it for visible spacing, which is CSS's job.
  • "Every element needs a closing tag." Empty elements such as the line break and the image are complete on their own and take no closing tag.

Recap

  • An element is an opening tag, content, and a closing tag; the tag name says what it is.
  • Nesting must be tidy, forming a parent-child tree.
  • Attributes are name-value pairs in the opening tag; id is unique, class is shared.
  • Empty elements have no closing tag.
  • Every page starts from the doctype, html, head, and body skeleton.
  • The doctype triggers standards mode, and the charset prevents garbled text.

Sources

  1. MDN Web Docs. (n.d.). Basic HTML syntax. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). What is in the head? Web page metadata. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). Void element. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). Global attributes. Mozilla. developer.mozilla.org
  5. WHATWG. (n.d.). HTML living standard: The HTML syntax. html.spec.whatwg.org
  6. Google. (n.d.). Learn HTML: Overview of HTML. web.dev. web.dev
  7. W3C. (n.d.). The W3C markup validation service. World Wide Web Consortium. find source β†—
Key terms
Element
A unit of a web page, typically an opening tag, content, and a closing tag.
Tag
The markup like <p> or </p> that marks the start or end of an element.
Attribute
A name-value pair inside an opening tag that gives extra information about the element.
Nesting
Placing elements inside other elements with properly matched open and close tags.
Empty element
An element with no content and no closing tag, such as <br> or <img>.
head
The document section holding information about the page, like its title and character set.
DOCTYPE
The first line declaring the page as HTML5 and triggering the browser's standards mode.
id / class
Attributes that name a single element uniquely (id) or tag one or many elements with a shared label (class).

Semantic HTML and Text Elements

  • Choose text elements by meaning, not by appearance.
  • Use headings to build a clear document outline.
  • Lay out a page with semantic sectioning elements.

The big picture

Good HTML is chosen for what content means, not for how it happens to look. When you pick tags by meaning, search engines understand your page and screen readers can guide people who cannot see the screen. This lesson shows how to choose headings, emphasis, and page regions semantically, so your markup is genuinely organized rather than just appearing so.

Key idea: semantic HTML labels content by meaning, which serves both machines and people using assistive technology.

What semantic means

Semantic HTML means each tag is chosen for the meaning of its content. A heading is marked as a heading because it is a heading, not merely because you want big text. Think of it like labeling boxes when you move house: a box marked "kitchen" helps everyone, even in the dark, whereas an unlabeled box tells you nothing until you open it. Semantic tags are those labels for your content.

Key idea: the tag you choose is a label that tells software what a piece of content actually is.

Headings and paragraphs

HTML offers six heading levels, from <h1> (most important) down to <h6> (least). Use exactly one <h1> per page for its main title, then nest lower levels to show structure, much like an outline. Body text goes in paragraphs with <p>.

<h1>Chocolate Chip Cookies</h1>
<h2>Ingredients</h2>
<p>You will need flour, butter, sugar, and chocolate.</p>
<h2>Steps</h2>
<p>Mix the dry ingredients, then add the wet ones.</p>

The crucial rule is to keep heading levels in order and not skip them for visual effect. Do not jump from an <h2> straight to an <h4> just because you want smaller text; that breaks the outline that screen readers and search engines rely on. If the size is wrong, fix it with CSS, not by choosing the wrong level.

Key idea: one h1 per page, then ordered headings beneath it, with size fixed in CSS rather than by picking the wrong level.

The document outline

Think of your headings as a table of contents that exists even when no one draws it. Assistive software lets a person who is blind pull up a list of a page's headings and jump straight to the section they want, exactly the way a sighted reader scans for a bold subtitle. A clean, correctly nested heading structure is therefore not decoration; it is a navigation system. A page whose headings read h1, h2, h2, h3, h2 tells a clear story, while one that scatters levels at random is disorienting to anyone who cannot see the layout.

Key idea: headings form an invisible outline that people navigate with, so their order carries real meaning.

Emphasis done right

To stress a word, use <strong> for strong importance (shown bold) and <em> for emphasis (shown italic). These carry meaning that assistive technology can announce, sometimes by changing the tone of the synthesized voice. There are also older look-alike tags, <b> and <i>, that produce the same bold and italic appearance but carry no meaning. Prefer <strong> and <em> when you mean importance or emphasis, and remember that if you only want a visual effect, that is CSS's job.

Key idea: strong and em carry meaning a screen reader can voice, while b and i are only visual look-alikes.

Sectioning elements

Older sites wrapped everything in generic <div> boxes. Modern HTML gives us named regions that describe the page's anatomy:

  • <header> introductory content, often a logo and site title.
  • <nav> a set of navigation links.
  • <main> the primary content, unique to this page. Use one per page.
  • <article> a self-contained piece, like one blog post, that would make sense on its own.
  • <section> a thematic grouping of related content, usually with its own heading.
  • <footer> closing content, such as copyright or contact links.
<body>
  <header>
    <h1>My Blog</h1>
    <nav>
      <a href="index.html">Home</a>
      <a href="about.html">About</a>
    </nav>
  </header>
  <main>
    <article>
      <h2>My First Post</h2>
      <p>Welcome to my blog.</p>
    </article>
  </main>
  <footer>
    <p>Copyright 2026</p>
  </footer>
</body>

Key idea: named regions like header, nav, main, and footer describe the page's anatomy so machines understand its parts.

When to reach for div and span

The <div> and its inline cousin <span> still exist, and they are not forbidden; they are simply meaningless containers, useful for grouping when no semantic element fits, usually so you can attach styling or scripting to the group. The right instinct is to reach for the meaningful tag first and fall back to <div> only when nothing describes your content. This connects to a larger field called web accessibility: when developers rebuild buttons and headings out of bare divs, screen readers see only anonymous boxes, so the strong, correct choice is almost always to pick the right element in the first place.

Key idea: use div and span only when no meaningful element fits, because they carry no meaning for assistive technology.

Div-soup, and the cure: a before and after

Here is the anti-pattern the industry calls div-soup, a page built entirely from anonymous boxes:

<div class="top">
  <div class="big-text">My Blog</div>
  <div class="links">
    <div class="link">Home</div>
    <div class="link">About</div>
  </div>
</div>
<div class="stuff">
  <div class="big-text-2">My First Post</div>
  <div>Welcome to my blog.</div>
</div>

Styled with enough CSS, this can be made to look identical to a proper page. But ask what a machine sees: no title, no navigation, no headings, no articles, just nested anonymous boxes with private class names. A screen reader can announce nothing useful; a search engine must guess what matters; the "links" are divs, so they are not even focusable by keyboard. Now the same content written semantically:

<header>
  <h1>My Blog</h1>
  <nav>
    <a href="index.html">Home</a>
    <a href="about.html">About</a>
  </nav>
</header>
<main>
  <article>
    <h2>My First Post</h2>
    <p>Welcome to my blog.</p>
  </article>
</main>

Same words, same possible appearance, but now every part announces itself: a banner with the site title, a navigation landmark a screen reader user can jump to directly, a heading outline, an article that a search engine can treat as the main content. The two versions cost the same number of lines; only one of them is a web page in the full sense. When you inherit div-soup (and one day you will), the repair is exactly this translation, box by box, asking "what is this content, really?"

When semantics run out: a first, honest look at ARIA

Native HTML covers most meanings, but not all. Consider a hamburger button that opens and closes a mobile menu. The element choice is easy and semantic: a real <button>, never a div, because a button is focusable and clickable by keyboard for free. But HTML has no attribute that says "the thing this button controls is currently open." That state exists only in your design, and a blind user cannot see the menu appear. This is precisely the gap ARIA attributes fill:

<button aria-expanded="false" aria-controls="menu">
  Menu
</button>
<nav id="menu">...</nav>

Now a screen reader announces "Menu, button, collapsed." When a script opens the menu, it also flips the attribute to aria-expanded="true", and the announcement becomes "expanded." Compare the wrong path: building the button as a <div class="menu-btn"> and then bolting on role="button", a keyboard handler, and focus styling by hand, badly reinventing what <button> already does perfectly. That contrast is the famous first rule of ARIA: do not use ARIA when a native element already carries the meaning. Reach for ARIA to express what HTML genuinely cannot, state and relationships like expanded, selected, or controls, and let real elements do everything else. Semantic HTML first, ARIA for the remainder, is the entire professional playbook in one sentence.

How a screen reader user actually moves

It helps to picture the navigation this markup enables. Screen readers offer a summoned menu (VoiceOver calls it the rotor) that lists a page three ways: all landmarks, all headings, or all links. A practiced user arriving at your blog does not listen from the top; they pull up landmarks and hear "banner, navigation, main, contentinfo," jump straight to main, then pull up headings and hear "heading level one, My Blog; heading level two, My First Post," and dive directly to the post. Total time: seconds, but only because the <header>, <nav>, <main>, and heading levels existed to be listed. On the div-soup version, all three menus come up empty, and the same user must arrow through every line of the page in order. Run this audit on anything you build: imagine the three lists your markup would generate, landmarks, headings, links, and ask whether each list alone tells the page's story. If the headings list reads like a sensible table of contents and the links list makes sense out of context, your semantics are doing their job for everyone, including search engines, which build their picture of your page from very nearly the same signals.

Common misconceptions

  • "Heading tags are just preset text sizes." Their sizes are a convenience, but their real purpose is to define the document's outline. Choose the level by importance, then use CSS if you want a different size.
  • "Semantic tags like article and section change the appearance." By default most of them look identical to a plain div. Their value is meaning, not visuals; you still use CSS to make a region look distinct.
  • "Using b for bold is wrong and outdated." The b and i tags are still valid; they just carry no importance or emphasis. Use strong and em when you mean importance or emphasis, and reserve b and i for stylistic bolding, such as a book title.
  • "Screen readers do not care which tags I use." They rely almost entirely on your tags; a button announced as a button or a heading announced as a heading is only possible because you chose the semantic element.

Recap

  • Semantic HTML chooses tags by meaning, helping search engines and screen readers.
  • Use one h1, keep heading levels in order, and adjust size with CSS.
  • Headings form a navigable outline that assistive technology uses.
  • strong and em carry meaning; b and i are visual only.
  • header, nav, main, article, section, and footer name the page's regions.
  • Fall back to div and span only when no semantic element fits.

Sources

  1. MDN Web Docs. (n.d.). Headings and paragraphs. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). Structuring documents. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). <main>: The main element. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). HTML: A good basis for accessibility. Mozilla. developer.mozilla.org
  5. WebAIM. (n.d.). Semantic structure: Regions, headings, and lists. webaim.org
  6. Google. (n.d.). Learn HTML: Semantic HTML. web.dev. web.dev
  7. W3C Web Accessibility Initiative. (n.d.). ARIA authoring practices guide: Read me first. World Wide Web Consortium. find source β†—
Key terms
Semantic HTML
Markup chosen for the meaning of content rather than its appearance.
Heading
An element from <h1> to <h6> that titles a section and defines the document outline.
strong / em
Elements marking strong importance and emphasis, carrying meaning as well as bold or italic styling.
main
The semantic element wrapping a page's primary, unique content; used once per page.
nav
A semantic element grouping a set of navigation links.
div
A generic block container used to group content when no semantic element fits.
Document outline
The hierarchical structure of a page implied by its headings, used for navigation by assistive technology.
Accessibility
The practice of building pages usable by people with disabilities, supported heavily by semantic markup.

Module 3: Links, Images, Lists, and Tables

Connecting pages, showing images accessibly, and organizing content into lists and tables. These are the elements that turned a set of documents into a genuine, navigable web.

Lists and Tables

  • Build unordered and ordered lists.
  • Structure tabular data with rows, headers, and cells.
  • Choose lists versus tables appropriately.

The big picture

Much of the content on the web is naturally a list or a grid, and HTML has purpose-built elements for each. Choosing the right one is another small act of semantics: it tells machines whether your content is a sequence of items or a matrix of related values. This lesson covers ordered and unordered lists and genuine data tables, and when to use which.

Key idea: lists hold sequences of items, and tables hold rows and columns of related data, and picking correctly keeps your markup honest.

Unordered and ordered lists

An unordered list, <ul>, is for items whose order does not matter; it shows bullet points. An ordered list, <ol>, is for sequences where order carries meaning; it shows numbers. In both, each item is a list item, <li>. A simple way to remember: a shopping list is unordered because you can buy items in any order, while cooking steps are ordered because the sequence matters.

<ul>
  <li>Bread</li>
  <li>Cheese</li>
  <li>Apples</li>
</ul>

<ol>
  <li>Preheat the oven</li>
  <li>Mix the batter</li>
  <li>Bake for 20 minutes</li>
</ol>

The choice between them is not about the bullet versus the number; it is about meaning. Baking steps must happen in order, so they are an <ol>; a grocery list can be bought in any order, so it is a <ul>. If you later dislike how the markers look, CSS can change or remove them, so never pick the wrong list type just for appearance.

Key idea: choose ol when order matters and ul when it does not, and fix the marker's look with CSS.

Nesting lists

Lists can be nested: place a whole <ul> or <ol> inside an <li> to make sub-items, which is how you build outlines and multi-level menus. Navigation menus are very often an unordered list of links, styled with CSS to look like a horizontal bar; underneath the styling, they are semantically just a list of destinations, which is exactly what a menu is.

<ul>
  <li>Fruit
    <ul>
      <li>Apples</li>
      <li>Bananas</li>
    </ul>
  </li>
  <li>Vegetables</li>
</ul>

Key idea: a list nested inside a list item builds outlines and menus, and a nav menu is really just a styled list of links.

Tables for real tabular data

A table displays data in rows and columns. Use one only for genuine tabular data, like a schedule or price list, never just to arrange a page (layout is CSS's job). The core elements are:

  • <table> wraps the whole table.
  • <tr> is a table row.
  • <th> is a header cell (bold and centered by default).
  • <td> is a normal data cell.
<table>
  <tr>
    <th>Fruit</th>
    <th>Price</th>
  </tr>
  <tr>
    <td>Apple</td>
    <td>$1.00</td>
  </tr>
  <tr>
    <td>Banana</td>
    <td>$0.50</td>
  </tr>
</table>

Read the structure carefully: a table is a stack of rows, and each row is a series of cells. The browser lines the cells up into columns automatically because every row has the same number of cells in the same order. The first row here uses <th> so the browser and screen readers know those are column labels, not data.

Key idea: a table is rows of cells, and header cells label the columns so software knows a number is, say, a price.

Structuring larger tables

For larger tables you can group rows with <thead> for the header row and <tbody> for the body, and add a <caption> as the table's title. These groupings help both styling and accessibility: a screen reader can announce the caption and repeat column headers as a user moves across a wide table, so a person who is blind does not lose track of which column a number belongs to. Spanning cells across columns or rows is possible with the colspan and rowspan attributes, but the four core elements are enough to build almost any table you need.

Key idea: thead, tbody, and caption make big tables clearer for both styling and screen readers.

Lists or tables?

A quick rule of thumb: if your content is a shopping list, use a list; if it is a spreadsheet, use a table. More precisely, use a table only when each item has the same set of attributes that you want to compare across items, like a price and a quantity for each product. If your items are just a sequence of single things, a list is simpler and more appropriate.

Key idea: reach for a table only when each item shares the same columns you want to compare, otherwise use a list.

A complete, accessible table, assembled

Here is the full pattern for a real data table, with every structural element in place. Read it top to bottom:

<table>
  <caption>Farmers market prices, week of May 5</caption>
  <thead>
    <tr>
      <th scope="col">Item</th>
      <th scope="col">Unit</th>
      <th scope="col">Price</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">Apples</th>
      <td>per pound</td>
      <td>$1.80</td>
    </tr>
    <tr>
      <th scope="row">Honey</th>
      <td>8 oz jar</td>
      <td>$6.50</td>
    </tr>
  </tbody>
</table>

The <caption> is the table's title, announced first by screen readers and useful to everyone scanning a page of several tables. The header row lives in <thead>; the data rows in <tbody>. Each header cell declares its direction with scope: the column headers say scope="col", and the first cell of each data row is itself a header for that row, scope="row", because "Apples" labels everything beside it. With this wiring, a screen reader moving to the price cell announces "Apples, Price, $1.80," reconstructing for the ear exactly what the eye does by glancing up and left. The visual table looks identical without any of it, which is why unwired tables are so common and so quietly hostile.

Spanning cells: the arithmetic of colspan

Sometimes one cell must cover several columns, such as a "Sold out" notice across a row. The colspan attribute does it: <td colspan="2">Sold out</td> makes one cell as wide as two. The bookkeeping rule: every row must still add up to the same number of columns. In a three-column table, a row with a colspan="2" cell holds only one more ordinary cell, because 2 + 1 = 3. Its cousin rowspan stretches a cell downward across rows with the same arithmetic running vertically. Miscounting a span is the classic cause of a table whose cells visibly stagger, and the fix is always to count each row's total spans back to the column count.

The third list: description lists

Beyond <ul> and <ol> there is a third list family for term-and-definition pairs, the description list:

<dl>
  <dt>HTML</dt>
  <dd>The language of page structure and content.</dd>
  <dt>CSS</dt>
  <dd>The language of presentation and layout.</dd>
</dl>

The <dl> wraps the list, each <dt> is a term, and each <dd> is its description. Glossaries, FAQ pages (question as term, answer as description), and metadata displays (Author: ..., Published: ...) are all natural description lists, and choosing <dl> tells machines the items are pairs, not a flat sequence. It completes the pattern of this lesson: sequences get <ol>, loose collections get <ul>, pairs get <dl>, and grids of comparable values get <table>. Content shaped like data should be marked up as the shape it is.

Hearing the table: why the wiring matters

Replay the farmers market table through a screen reader to see what the structural elements actually buy. A user navigating cell by cell with table commands hears, on entering the price column of the honey row: "Honey, Price, six dollars fifty." Three pieces of information arrived together: the row header (from the scope="row" on Honey), the column header (from scope="col" on Price), and the cell's own content. Strip the scopes and headers away, mark everything as plain <td>, and the same keystroke yields just "six dollars fifty": a number floating free, in a grid the listener must hold in memory. Multiply that by a timetable with nine columns and the difference between usable and hopeless is total. The caption earns its place the same way: a user skimming a page of several tables hears each caption announced as they land, "Farmers market prices, week of May 5," and can decide whether to enter at all. None of this wiring changes the visual rendering by a pixel, which is exactly why it is so often omitted and why remembering it marks you as someone who builds for the whole audience.

Styling hooks you will use in the CSS modules

Browsers render tables and lists plainly, and the coming CSS modules will dress them. Two previews worth noting now, because they explain some markup choices. Tables are usually given border-collapse: collapse; so adjacent cell borders merge into single clean lines, and alternating row shading (zebra striping) is applied with a pseudo-class selecting every second row, which works because each row is a real <tr> element that CSS can count. Navigation lists are usually flattened with list-style: none; and laid out horizontally with flexbox, which works because each destination is a real <li> the layout system can space. In both cases, the semantic structure you wrote for meaning turns out to be exactly the set of hooks styling needs, one more instance of the recurring bargain: honest markup now, effortless styling and scripting later.

Common misconceptions

  • "Tables are an easy way to lay out a page in columns." Using tables for visual layout is now considered wrong. It confuses screen readers, which announce the page as data, and it is far less flexible than CSS. Use flexbox or grid for layout.
  • "Use ol whenever I want numbers and ul whenever I want bullets." The choice should reflect whether order matters, not the marker you prefer. CSS can change or hide markers.
  • "A header cell is just a bold data cell." A th is announced to screen readers as a header and tied to its column or row, which is how a blind user knows a number is a price rather than a quantity. Bolding a td with CSS looks the same but carries none of that meaning.
  • "Columns are elements I create directly." You build rows of cells; the browser forms the columns automatically from cells in matching positions.

Recap

  • ul is for unordered items, ol is for meaningful sequences, and li is each item.
  • Lists nest to build outlines and menus.
  • Tables are for real tabular data, not page layout.
  • table wraps rows (tr), which contain header cells (th) and data cells (td).
  • thead, tbody, and caption add structure and accessibility to larger tables.
  • Use a table only when items share the same columns to compare.

Sources

  1. MDN Web Docs. (n.d.). Lists. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). HTML table basics. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). HTML table accessibility. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). <table>: The table element. Mozilla. developer.mozilla.org
  5. MDN Web Docs. (n.d.). <dl>: The description list element. Mozilla. developer.mozilla.org
  6. Google. (n.d.). Learn HTML: Tables. web.dev. web.dev
  7. W3C Web Accessibility Initiative. (n.d.). Tables tutorial. World Wide Web Consortium. find source β†—
Key terms
Unordered list (<ul>)
A bulleted list for items whose order does not matter.
Ordered list (<ol>)
A numbered list for items in a meaningful sequence.
List item (<li>)
A single entry inside a <ul> or <ol>.
Table (<table>)
An element that arranges genuine tabular data into rows and columns.
Table row (<tr>)
A horizontal row of cells within a table.
th / td
A header cell and a normal data cell inside a table row.
thead / tbody
Elements that group a table's header rows and body rows for styling and accessibility.

Module 4: Forms and User Input

Collecting information from visitors with inputs, labels, and controls. Forms are where a static page becomes a two-way conversation, powering searches, logins, checkouts, and surveys.

Building Forms

  • Assemble a form with labeled inputs.
  • Choose the right input type for each field.
  • Use select menus, checkboxes, and radio buttons.

The big picture

A form is how a page collects information from a visitor and turns a one-way display into a two-way conversation. This lesson shows how to build a form from labeled inputs, choose the right input type for each field, and add menus, checkboxes, and radio buttons. These are the controls behind every search box, login, and checkout on the web.

Key idea: a form groups labeled controls that collect data and send it somewhere when submitted.

The form element

Everything goes inside the <form> element, which groups the controls and defines where the data should go when submitted.

<form action="/subscribe" method="post">
  ...form controls go here...
</form>

The action attribute names the server address to send the data to, and method is usually post for data that changes something. Recall from Module 1 that GET requests append data to the URL while POST tucks it into the request body; that is exactly the distinction here. For learning, the exact destination does not matter; focus on the controls inside, because those are what you assemble and label.

Key idea: the form element wraps the controls and its action and method say where and how the data is sent.

Inputs and labels

The workhorse control is <input>, an empty element whose type attribute decides what it collects. Always pair each input with a <label>. Connecting them, by matching the label's for attribute to the input's id, means clicking the label focuses the field and screen readers announce the field's purpose correctly. Think of the label as the caption on a form blank; without it, the box is unlabeled and confusing.

<label for="email">Email address</label>
<input type="email" id="email" name="email">

The name attribute labels the data when it is sent to the server; without it, the value would arrive nameless and useless. So a fully wired input often carries three attributes at once: type for what it collects, id to pair with a label, and name to identify its value on submission. Common input types include:

typeCollects
textA single line of plain text
emailAn email address, with basic checking
passwordText hidden as dots
numberA numeric value, often with up/down arrows
dateA calendar date, often with a date picker
checkboxAn on/off choice
radioOne choice from a group

Key idea: pair every input with a label via for and id, and give it a name so its value is identified when submitted.

Why input types matter

Choosing the right type does far more than validation. On a phone, type="email" summons a keyboard with an @ key ready, type="number" shows a numeric keypad, and type="date" offers a friendly calendar rather than making the user type a date by hand. The right type also gives the browser a chance to catch obvious mistakes before the form is sent, and it improves accessibility by telling assistive technology what kind of data each field wants.

Key idea: the most specific input type gives the right keyboard, catches mistakes early, and helps assistive technology.

Bigger text, menus, and grouped choices

For multi-line text use <textarea>. For a drop-down menu use <select> with <option> children. Radio buttons that share the same name become a group where only one can be chosen, which is exactly why the shared name matters: it is what ties them into a single choice.

<label for="msg">Message</label>
<textarea id="msg" name="msg"></textarea>

<label for="size">Size</label>
<select id="size" name="size">
  <option>Small</option>
  <option>Medium</option>
  <option>Large</option>
</select>

<p>Choose one:</p>
<label><input type="radio" name="plan" value="free"> Free</label>
<label><input type="radio" name="plan" value="pro"> Pro</label>

Note the difference between checkboxes and radios: checkboxes are independent on/off switches where any number can be ticked, while radios sharing a name are mutually exclusive, so choosing one clears the others. Reach for radios when exactly one answer is allowed and checkboxes when several are.

Key idea: radios sharing a name allow exactly one choice, while checkboxes are independent switches allowing any number.

The submit button and basic validation

Finally, a button with type="submit" sends the form; you can write <button type="submit">Send</button>. Add the required attribute to any input the browser should refuse to submit while empty, and it will show a helpful message pointing at the missing field. Other built-in checks include minlength and maxlength for text and min and max for numbers. Remember, though, that these client-side checks improve the experience but can be bypassed, so real validation must also happen on the server. With these pieces you can build accessible logins, contact forms, and surveys before adding a single line of JavaScript.

Key idea: a submit button sends the form and attributes like required add built-in checks, but the server must validate too.

What the server actually receives

Demystify submission by looking at the data itself. Suppose a form holds a name field and the Pro radio button checked, and the user typed Ada. On submit, the browser assembles name-value pairs from each control's name and current value, producing the encoded string name=Ada&plan=pro. With method="get" that string rides visibly in the URL after a question mark, /subscribe?name=Ada&plan=pro, which is exactly the query-string anatomy from Module 1. With method="post" the same string travels in the request body instead. That difference drives the choice: GET is right for requests that only read, like a search, because the resulting URL can be bookmarked and shared ("page 2 of results for cats"); POST is right for anything that changes or submits something, and mandatory for anything sensitive, since URLs are logged in history and server logs. One rule of thumb covers it: search forms GET, everything else POSTs.

Built-in validation, worked

The browser can reject bad input before it ever leaves the page, driven entirely by attributes:

<label for="user">Username (3 to 12 characters)</label>
<input type="text" id="user" name="user"
       required minlength="3" maxlength="12">

<label for="zip">ZIP code</label>
<input type="text" id="zip" name="zip"
       required pattern="[0-9]{5}"
       title="Five digits, like 30301">

<label for="guests">Guests (1 to 8)</label>
<input type="number" id="guests" name="guests" min="1" max="8">

Trace what each attribute buys. required blocks submission while the field is empty, and the browser focuses the field and shows a message. minlength="3" refuses a two-letter username the moment the user submits. The pattern attribute is the powerful one: it holds a regular expression, a compact matching language, and [0-9]{5} reads as "exactly five characters, each a digit 0 through 9." A user typing 3030 or 30A01 is stopped, and the title text is shown as the hint explaining what was expected, so always write a human-friendly title alongside a pattern. On the number field, min and max bound the value, and the spinner arrows respect the bounds. All of this behavior arrives with zero JavaScript, works with the keyboard, and is announced correctly to screen readers, which is why native validation should be your first tool rather than your afterthought. The server still re-checks everything, as always, but most honest mistakes never reach it.

Grouping related choices: fieldset and legend

When several controls form one question, wrap them in a <fieldset> whose <legend> is the question:

<fieldset>
  <legend>Shipping speed</legend>
  <label><input type="radio" name="ship" value="standard" checked> Standard (5 days)</label>
  <label><input type="radio" name="ship" value="express"> Express (2 days)</label>
  <label><input type="radio" name="ship" value="overnight"> Overnight</label>
</fieldset>

Sighted users see a box with a title; a screen reader user hears "Shipping speed, Standard, radio button, selected," the group name arriving with each option, so the choices make sense even when reached mid-form. Two details in the example are worth stealing: the checked attribute pre-selects a sensible default, and each radio carries an explicit value, because the value, not the visible label text, is what travels to the server (ship=standard). The same is true of <option value="m">Medium</option> in a select menu: label for humans, value for machines. Forms are the one place in HTML where you are constantly writing for both audiences in the same line, and the discipline of naming both well is what makes the submitted data clean.

The other way to wire a label, and why targets matter

Besides matching for to id, HTML allows an implicit label: wrap the control inside the label element itself, as the radio examples above do, and no attributes are needed because containment declares the pairing. Both wirings are valid; the explicit for/id form is preferred when the label and field sit in different places in the layout, while wrapping is convenient for compact choices like radios and checkboxes. Either way, the payoff extends beyond screen readers: a wired label makes the entire label text a click target for its control. On a phone, the difference between tapping a 16-pixel checkbox and tapping the whole phrase "Subscribe to the newsletter" is the difference between fumbling and flow, and it costs one attribute. Forms are full of these tiny mechanical kindnesses, and users feel their sum without ever knowing why one site is pleasant and another exhausting.

Common misconceptions

  • "Labels are just visible text; I can skip the for/id wiring." Without the connection, clicking the text does not focus the field and screen readers cannot reliably announce what the input is for. The for attribute matching the input's id is what makes a label functional.
  • "The required attribute means my data is safe." Client-side checks can be bypassed by disabling them or sending data directly. Real validation must also happen on the server; never trust input just because the browser checked it.
  • "type=password makes the data secure." It only hides the characters on screen. The value is still sent as ordinary text; it is HTTPS encryption, not the input type, that protects it in transit.
  • "Radios and checkboxes are interchangeable." Radios sharing a name allow one answer; checkboxes allow any number. Picking the wrong one misrepresents what answers are possible.

Recap

  • The form element groups controls; action and method define where and how data is sent.
  • Pair each input with a label (for matches id) and give it a name.
  • Specific input types improve the keyboard, validation, and accessibility.
  • textarea is for long text, select and option build menus.
  • Radios sharing a name are exclusive; checkboxes are independent.
  • required and similar attributes add checks, but the server must validate too.

Sources

  1. MDN Web Docs. (n.d.). Your first form. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). Basic native form controls. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). The HTML5 input types. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). Client-side form validation. Mozilla. developer.mozilla.org
  5. MDN Web Docs. (n.d.). <input>: The HTML input element. Mozilla. developer.mozilla.org
  6. Google. (n.d.). Learn Forms. web.dev. web.dev
  7. W3C Web Accessibility Initiative. (n.d.). Forms tutorial: Labeling controls. World Wide Web Consortium. find source β†—
Key terms
Form
The <form> element that groups input controls and defines where their data is sent.
input
An empty element whose type attribute determines what kind of data it collects.
label
Text tied to an input via for and id, improving usability and accessibility.
name attribute
The identifier attached to a control's value when the form is submitted.
select / option
Elements that build a drop-down menu of choices.
required
An attribute that stops a form from submitting while the field is empty.
radio vs checkbox
Radios sharing a name allow exactly one choice; checkboxes are independent switches allowing any number.

Module 5: CSS Fundamentals

Selectors, the cascade, the box model, color, and typography. This module turns bare, unstyled markup into designed pages and teaches the rules that decide which style wins when several compete.

Selectors and the Cascade

  • Write CSS rules with selectors and declarations.
  • Target elements by tag, class, and id.
  • Attach CSS to an HTML page.

The big picture

CSS is how you style HTML, and it works by writing rules that pick elements and describe how they should look. The trickiest part for beginners is not writing a rule but understanding which rule wins when several target the same element. This lesson teaches selectors, the cascade that resolves conflicts, and how to attach CSS to a page.

Key idea: a CSS rule selects elements and styles them, and the cascade decides the winner when rules collide.

Rules, selectors, and declarations

A CSS rule has two parts: a selector that chooses which elements to style, and a block of declarations that say how to style them. Each declaration is a property and a value separated by a colon and ended with a semicolon.

p {
  color: navy;
  font-size: 18px;
}

Read it as: "for every <p> element, set the text color to navy and the font size to 18 pixels." The curly braces hold the declarations; the selector p sits in front. Forgetting the semicolon at the end of a declaration is one of the most common beginner bugs, because it can silently break the declaration after it, so build the habit of always closing each line with one.

Key idea: a rule is a selector plus declarations, and each declaration is a property, a colon, a value, and a semicolon.

Three ways to select

The three selectors you will use constantly are:

  • A type selector targets all elements of a tag, like p or h1. Use it to set broad defaults.
  • A class selector targets any element carrying a given class attribute; write a dot before the name, like .warning. Classes are reusable across many elements and are the workhorse of real stylesheets.
  • An id selector targets the single element with a matching id; write a hash before the name, like #main-title. An id must be unique on the page.
.button {
  background: teal;
  color: white;
}
#logo {
  width: 120px;
}

In the HTML you attach these with attributes: <p class="warning"> or <h1 id="main-title">. You can also combine selectors. A descendant selector like nav a means "an <a> that is anywhere inside a <nav>," which lets you style navigation links without touching links elsewhere. You can group selectors with commas, so h1, h2, h3 { color: navy; } styles all three at once.

Key idea: type selectors set broad defaults, classes are reusable, and ids target a single unique element.

The cascade and specificity

The C in CSS is Cascading, which refers to how the browser resolves conflicts when several rules target the same element. It decides the winner by specificity: an id beats a class, and a class beats a type selector. If two rules have equal specificity, the one written later in the file wins. Think of specificity like ranks in an organization: a manager's instruction (id) overrides a team lead's (class), which overrides a general policy (type). This ordering lets a broad rule set a sensible default while a more specific rule overrides it for special cases.

Key idea: id beats class beats type, and when specificity ties, the later rule wins.

A worked example of the cascade

Imagine this HTML and CSS together:

<p class="note" id="intro">Hello</p>

p       { color: black; }
.note   { color: green; }
#intro  { color: red; }

What color is the text? All three rules match the same paragraph, so specificity decides. The type selector p is the weakest, the class .note beats it, and the id #intro beats them both. The paragraph renders red. Now remove the id rule: the class wins and the text is green. Remove the class too, and only the type rule remains, so it is black. Walking down this ladder is exactly how you reason about any conflict you meet in practice.

Key idea: when rules conflict, climb the specificity ladder from type to class to id to find the winner.

Attaching CSS

The best way to add CSS is an external file linked from the head, so one stylesheet can control every page of a site at once:

<head>
  <link rel="stylesheet" href="styles.css">
</head>

You could also write CSS inside a <style> block in the head, or inline on a single element with a style attribute, but the external file is cleanest and keeps structure and presentation separate. Inline styles are also the most specific of all and the hardest to override, which is another reason to avoid them. There are escape hatches like the !important flag that override normal specificity, but they start an arms race and are best avoided; a calm, low-specificity stylesheet built mostly from classes is far easier to maintain.

Key idea: link one external stylesheet from the head, and avoid inline styles and !important because they are hard to override.

Specificity as arithmetic: the three-count

The ladder "id beats class beats type" is actually a number the browser computes. Every selector scores a count of three values, written here as (ids, classes, types): how many id selectors it contains, how many class selectors (plus attribute selectors and pseudo-classes like :hover), and how many type selectors (plus pseudo-elements). Counts are compared left to right, and the first difference decides; a single id outranks any number of classes, and a single class outranks any number of types.

Battle one. Which rule colors this link: nav a.active { color: orange; } or .menu .active { color: purple; }?

  1. Score nav a.active: no ids, one class (.active), two types (nav, a): (0, 1, 2).
  2. Score .menu .active: no ids, two classes: (0, 2, 0).
  3. Compare left to right: ids tie at 0; classes 2 beats 1, and the comparison stops there. The type count never matters.

The link is purple, even though the orange rule "mentions more things." Battle two: #intro scores (1, 0, 0) and .card .note .highlight p scores (0, 3, 1). The id column is compared first: 1 beats 0, so the id rule wins against three classes and a type combined. Battle three, the tie: h1.title and .card h1 both score (0, 1, 1). Identical counts mean the cascade falls through to source order, and whichever rule appears later in the stylesheet wins. When a style refuses to apply, doing this ten-second arithmetic beats guessing every time, and the browser's developer tools will even show you the winning and crossed-out rules per element.

Inheritance: styles that flow downhill

Not every style needs a selector to reach an element. Text properties, color, font-family, font-size, line-height, inherit: set them on an ancestor and descendants receive them automatically, which is why one body { font-family: Georgia, serif; } rule types the entire page. Box properties, padding, margin, border, background, deliberately do not inherit, because a border around every nested element would be chaos. This split explains a common beginner surprise: setting color on the body changes everything, but setting border on the body draws exactly one border. When a value seems to come from nowhere, look up the tree: it probably flowed down from an ancestor rule, and the developer tools' "inherited from" section names the culprit.

Styling states: pseudo-classes

A pseudo-class selects an element only while it is in a particular state, written with a colon:

a:hover { text-decoration: underline; }
a:focus { outline: 2px solid #1b4d8f; }
button:disabled { opacity: 0.5; }

The first rule applies only while the mouse is over the link; the second while the link holds keyboard focus, which is how a visitor tabbing through the page sees where they are, so never remove focus outlines without replacing them with something visible; the third greys a button that cannot currently be pressed. Each pseudo-class counts as one class in the specificity arithmetic, so a:hover scores (0, 1, 1). States are also your first taste of dynamic styling without JavaScript: the browser applies and removes these rules live as the user acts.

Watching the cascade in the developer tools

You never have to reason about specificity blind. Right-click any element on any page and choose Inspect: the developer tools open with that element selected and a Styles panel listing every rule that targets it, ordered from most to least authoritative. Losing declarations are shown struck through, so the paragraph from our worked example would display the #intro rule on top, then the .note rule with its color crossed out, then the p rule likewise. The panel is live: click a value and edit it, toggle a declaration off with its checkbox, even add trial declarations, and the page updates instantly, with nothing saved to your files. This is the standard professional loop for styling mysteries: inspect the element, read which rule actually won, experiment in the panel until it looks right, then copy the winning declarations back into your stylesheet. A companion tab, Computed, answers the related question "what is the final value of every property on this element, and which rule supplied it," which is the fastest way to trace an inherited value back to the ancestor that set it. Ten minutes of exploring these two panels repays itself for the rest of your career.

Common misconceptions

  • "If a style is not applying, the property must be spelled wrong." Just as often, a more specific rule is overriding it, or a later rule of equal specificity wins. Check the cascade before doubting your spelling.
  • "An id is just a fancier class." An id must be unique and carries much higher specificity, which makes it harder to override later. For reusable styling, classes are almost always better.
  • "Inline styles are convenient and harmless." They scatter presentation through your HTML, cannot be reused, and outrank almost every stylesheet rule, making later changes painful.
  • "Forgetting a semicolon is fine." A missing semicolon can silently break the next declaration, producing bugs that look like typos elsewhere.

Recap

  • A rule is a selector plus declarations; each declaration ends in a semicolon.
  • Type selectors set defaults, classes are reusable (dot), ids are unique (hash).
  • Descendant selectors and comma grouping combine selectors.
  • The cascade resolves conflicts by specificity: id beats class beats type.
  • Equal specificity is broken by source order, later wins.
  • Link one external stylesheet; avoid inline styles and !important.

Sources

  1. MDN Web Docs. (n.d.). Basic CSS selectors. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). Handling conflicts. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). Specificity. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). !important. Mozilla. developer.mozilla.org
  5. Google. (n.d.). Learn CSS: The cascade. web.dev. web.dev
  6. Google. (n.d.). Learn CSS: Specificity. web.dev. web.dev
  7. W3C. (n.d.). CSS cascading and inheritance level 5 [Specification]. World Wide Web Consortium. find source β†—
Key terms
Rule
A CSS selector plus the block of declarations applied to the matched elements.
Selector
The part of a CSS rule that chooses which elements to style.
Declaration
A single property-and-value pair, such as color: navy;
Class selector
A reusable selector written with a dot, matching elements with a given class attribute.
id selector
A selector written with a hash, matching the one element with that unique id.
Specificity
The ranking that decides which conflicting CSS rule wins, with id beating class beating type.
Descendant selector
A selector like 'nav a' that matches an element nested inside another.

The Box Model, Color, and Typography

  • Explain the four layers of the CSS box model.
  • Set colors with names, hex, and rgb.
  • Control fonts, size, and spacing of text.

The big picture

Once you can select elements, the next thing to master is that every element on a page is a rectangular box. Understanding the four layers of that box explains nearly every spacing and sizing question you will ever have. This lesson covers the box model, the ways to name colors, and the properties that control text.

Key idea: every element is a box of content, padding, border, and margin, and CSS sizes, colors, and types that box.

The four layers

Every element is a box with four concentric layers, from the inside out:

  • Content the text or image itself, with a width and height.
  • Padding clear space inside the box, between the content and the border.
  • Border a line drawn around the padding.
  • Margin clear space outside the border, pushing other elements away.
The CSS box model: margin surrounds border surrounds padding surrounds content margin border padding content

A memorable way to remember the order: content is the picture, padding is the mat around it, the border is the frame, and the margin is the empty wall space between frames. Padding shares the element's background color, while margin is always transparent, which is a quick way to tell which one you are looking at when you inspect a page.

.card {
  width: 300px;
  padding: 16px;
  border: 2px solid gray;
  margin: 24px;
}

Key idea: padding is space inside the border and margin is space outside it, and only margin is always transparent.

A worked example: the box-sizing trap

By default the width applies only to the content, so padding and border are added on top of it. Take the card above: its content width is 300px, but it has 16px of padding on each side and a 2px border on each side. The total visible width is therefore 300 + 16 + 16 + 2 + 2 = 336 pixels, not 300. This surprises nearly every beginner and wrecks layouts where two boxes you sized at 50% overflow because their padding pushed them past the space available.

The widely used fix is one declaration: box-sizing: border-box;. It changes the meaning of width so that it includes the padding and border. With it applied, a box you size at 300px stays exactly 300px wide, and the padding eats inward instead of pushing outward. Many developers apply it to everything at the top of their stylesheet with * { box-sizing: border-box; }, and from then on sizing behaves the way intuition expects.

Key idea: by default width is content-only, and box-sizing: border-box makes width include padding and border.

Color

CSS gives you several ways to name a color. Three common ones:

  • Keywords like red, navy, or tomato, convenient for quick work.
  • Hex codes like #1b4d8f, six digits giving the red, green, and blue amounts in base 16, from 00 to ff each.
  • rgb like rgb(27, 77, 143), the same three channels written as decimal numbers from 0 to 255.

Hex and rgb describe the exact same colors in different notation; #1b4d8f and rgb(27, 77, 143) are identical, because hex 1b is decimal 27, hex 4d is 77, and hex 8f is 143. A related form, rgba(...), adds a fourth value for opacity from 0 (invisible) to 1 (solid), which is how you make semi-transparent overlays.

body {
  background: #f4f4f4;
  color: rgb(30, 30, 30);
}

Key idea: keywords, hex, and rgb are three notations for color, and hex and rgb are just two spellings of the same red, green, and blue amounts.

Typography

Text is styled with a small family of properties: font-family chooses the typeface (list fallbacks, ending in a generic family like sans-serif, so that if the first font is unavailable the browser tries the next); font-size sets the size; font-weight sets boldness; line-height sets the vertical space between lines; and text-align aligns text left, right, center, or justified.

body {
  font-family: Georgia, "Times New Roman", serif;
  font-size: 18px;
  line-height: 1.6;
}
h1 {
  font-weight: bold;
  text-align: center;
}

The comma-separated list in font-family is called a font stack, and the generic family at the end is a guaranteed fallback so text always renders in something reasonable. Comfortable body text is usually 16 to 18 pixels with a line-height around 1.5 to 1.6, which gives the eye enough room to move from one line to the next without losing its place. A unitless line-height like 1.6 is best because it scales with the font size.

Key idea: a font stack lists fallbacks ending in a generic family, and comfortable body text is about 16 to 18px with a unitless line-height near 1.6.

The overflow bug, computed to the pixel

Here is the classic layout failure that box-sizing prevents, with real numbers. A 600px-wide container holds two boxes meant to sit side by side, each declared width: 50%; padding: 20px; border: 1px solid gray;. Do they fit?

  1. Each box's content width: 50% of 600 = 300px. (Percentages resolve against the parent.)
  2. Each box's visible width under default sizing: 300 + 20 + 20 (padding) + 1 + 1 (border) = 342px.
  3. Two boxes: 342 × 2 = 684px, but the container offers 600. They cannot fit; the second box wraps below or overflows, and the layout "mysteriously" breaks.

Now apply box-sizing: border-box. Each box's total width becomes exactly 300px, with the padding and border eating inward (content shrinks to 300 − 42 = 258px). Two boxes: exactly 600. They fit to the pixel. This one computation is the whole argument for the near-universal * { box-sizing: border-box; } reset, and doing the arithmetic once by hand makes the property permanently un-mysterious.

Margin collapsing: the gap that is smaller than you ordered

Vertical margins between stacked blocks do not add; they collapse to the larger of the two. If a heading has margin-bottom: 20px and the paragraph below has margin-top: 30px, the gap between them is 30px, not 50. The design logic: each element is stating the minimum breathing room it wants, and 30px of space already satisfies a request for 20. Three boundary facts complete the rule: horizontal margins never collapse, padding never collapses (two paddings always add), and any border or padding between the margins blocks the collapse. When a vertical gap measures smaller than your two margins combined, this is what happened, and the fix is usually to set the gap you actually want on one side only. A tidy convention many stylesheets adopt: space paragraphs with margin-bottom alone, leaving margin-top at zero, so no collapse arithmetic is ever needed.

Reading the shorthands

The padding and margin properties accept one to four values, and the expansion rules are worth learning once:

DeclarationMeaning
padding: 16px;All four sides 16px
padding: 8px 24px;Top and bottom 8px, left and right 24px
padding: 8px 24px 32px;Top 8, sides 24, bottom 32
padding: 8px 24px 32px 4px;Clockwise from the top: top 8, right 24, bottom 32, left 4

The four-value order is clockwise starting at the top, remembered by the mnemonic TRBL ("trouble"): top, right, bottom, left. The two-value form is the everyday workhorse, since designs usually want different vertical and horizontal breathing room, as in a button's padding: 10px 16px;.

Audit one box, end to end

Put it all together on .banner { width: 400px; padding: 12px 24px; border: 3px solid navy; margin: 16px; } with default box-sizing. Visible width: 400 + 24 + 24 + 3 + 3 = 454px. Horizontal space consumed on the page: 454 + 16 + 16 = 486px. Height works identically with the vertical values (12px paddings). Being able to run this audit on any element, and knowing that switching to border-box would pin the visible width at the declared 400, is exactly the fluency the browser's box-model inspector diagram assumes you have.

Color contrast: the readability arithmetic

Color choices carry an accessibility obligation with actual numbers attached. The Web Content Accessibility Guidelines define a contrast ratio between text and its background, running from 1:1 (identical) to 21:1 (black on white), and require at least 4.5:1 for normal body text. The fashionable light gray #999999 on white measures about 2.8:1, a clear failure that many low-vision readers simply cannot read, while the slightly darker #767676 on white squeaks past at roughly 4.5:1. You are not expected to compute the ratio by hand; free contrast checkers (WebAIM's is the standard) take two colors and report the ratio and pass/fail instantly, and browser developer tools flag failing text. The habit to build is checking any gray-on-gray or color-on-color text combination before shipping it, because contrast failures are invisible to the designer with good eyesight on a bright monitor and glaring to everyone else. A related shorthand completes this lesson's toolkit: border: 2px solid navy; bundles width, style, and color in one declaration, and the style keyword (solid, dashed, dotted) is the one piece beginners forget, without which no border draws at all.

Common misconceptions

  • "Setting width: 300px makes the whole box 300px wide." By default width sets only the content area; padding and border are added on top. Apply box-sizing: border-box to make width include them, which is what most people want.
  • "Padding and margin are basically the same spacing." Padding is inside the border and takes the background color; margin is outside the border and is always transparent, separating the element from its neighbors.
  • "line-height should be set in pixels to match the font size." A unitless value like 1.6 is usually better because it scales with the font size, so larger text automatically gets proportionally more spacing.
  • "Hex and rgb are different colors." They are two notations for the same red, green, and blue amounts; #1b4d8f equals rgb(27, 77, 143).

Recap

  • Every element is a box: content, padding, border, margin.
  • Padding is inside the border and margin is outside; only margin is transparent.
  • By default width is content-only; box-sizing: border-box includes padding and border.
  • Colors can be keywords, hex, or rgb, with rgba adding opacity.
  • Typography uses font-family, font-size, font-weight, line-height, and text-align.
  • A font stack ends in a generic family, and body text reads well near 16 to 18px with line-height about 1.6.

Sources

  1. MDN Web Docs. (n.d.). The box model. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). box-sizing. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). Mastering margin collapsing. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). Fundamental text and font styling. Mozilla. developer.mozilla.org
  5. MDN Web Docs. (n.d.). <color>: The CSS color value. Mozilla. developer.mozilla.org
  6. Google. (n.d.). Learn CSS: Box model. web.dev. web.dev
  7. Google. (n.d.). Learn CSS: Text and typography. web.dev. web.dev
Key terms
Box model
The layout model where every element is a box of content, padding, border, and margin.
Padding
Space inside an element, between its content and its border.
Margin
Space outside an element's border that separates it from other elements.
box-sizing: border-box
A setting that makes an element's width include its padding and border.
Hex code
A color written as a # followed by six base-16 digits for red, green, and blue.
line-height
The vertical spacing between lines of text, best set as a unitless multiplier.
Font stack
A comma-separated list of fonts ending in a generic family, providing fallbacks.

Module 6: Layout and Responsive Design

Arranging boxes with flexbox and grid, then adapting to every screen size. These modern tools replaced years of layout hacks and make it straightforward to build pages that look right on any device.

Flexbox: Laying Out in One Dimension

  • Turn an element into a flex container.
  • Distribute and align items along a row.
  • Wrap and space flex items.

The big picture

For years, arranging boxes side by side in CSS was awkward. Flexbox replaced those old tricks with a clean system for placing items in a single direction and controlling how they line up and share space. This lesson shows how to make a flex container, align items along two axes, wrap them, and let them grow to fit.

Key idea: flexbox lays items out in one direction, a row or a column, and gives you simple controls for spacing and alignment.

The container and its items

You switch on flexbox by setting display: flex; on a parent element. That parent becomes the flex container, and its direct children become flex items that lay out in a row by default. Picture a shelf and the books on it: the shelf (container) decides how the books (items) are spaced and aligned, while each book can still be given a little individual treatment.

.menu {
  display: flex;
}

With just that one line, the children of .menu sit next to each other instead of stacking vertically the way block elements normally do. Two container properties then do most of the work of positioning them.

Key idea: display: flex makes a parent a flex container whose direct children become items laid out in a row.

The two axes: justify-content and align-items

Flexbox thinks in terms of two axes. The main axis runs in the direction items flow (across, for a row), and the cross axis runs perpendicular to it (down, for a row). justify-content controls spacing along the main axis, and align-items controls alignment across the cross axis. Keeping straight which property works on which axis is the single most useful thing to internalize about flexbox. Useful values for justify-content:

ValueEffect
flex-startItems packed to the start (the default)
centerItems centered as a group
space-betweenEqual gaps between items, none at the ends
space-aroundEqual space around each item
space-evenlyEqual space between items and at both ends
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

This is the classic navigation bar: logo on the left, links on the right, everything vertically centered. Read it as a sentence: spread the children apart along the row, and center them top to bottom. Centering something both horizontally and vertically, long the most infamous nuisance in CSS, is now just justify-content: center; paired with align-items: center;.

Key idea: justify-content spaces items along the main axis, and align-items aligns them across the cross axis.

Direction, wrapping, and gaps

Change the direction with flex-direction: column; to stack items vertically instead of horizontally; note that this swaps the axes, so now justify-content works vertically and align-items horizontally. Allow items to flow onto new lines when they run out of room with flex-wrap: wrap;, which prevents a row of cards from squashing or overflowing on a narrow screen. Add even spacing between items with the gap property, a clean modern replacement for fiddly margins.

.cards {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}

Key idea: flex-direction sets row or column (and swaps the axes), flex-wrap lets items move to new lines, and gap spaces them cleanly.

Growing and shrinking items

Each child can also grow or shrink to fit the available space. Setting flex: 1 on the items tells them to share the leftover space equally, which is how you make three cards fill a row in equal thirds even as the window resizes. You can give different weights, so flex: 2 on one item and flex: 1 on another makes the first twice as wide. Under the hood, flex bundles three settings: how eagerly an item grows, how readily it shrinks, and its ideal starting size, and flex: 1 tells items to ignore their natural size and split the space evenly.

This flexibility, items that respond to the space they are given rather than demanding a fixed size, is where the name flexbox comes from. Flexbox is the right tool whenever your content is essentially a line of things; for two-dimensional grids, the next lesson introduces CSS grid.

Key idea: flex on an item lets it grow or shrink to share space, and equal weights split the row into equal parts.

Building the navbar property by property

Watch each declaration earn its place. The HTML is a brand name and a group of links:

<nav class="bar">
  <span class="logo">Atlas</span>
  <div class="links">
    <a href="#">Courses</a>
    <a href="#">About</a>
    <a href="#">Contact</a>
  </div>
</nav>

Step 1: .bar { display: flex; }. The logo and the links-div, previously stacked, now sit on one line, packed to the left, and that is all. Step 2: add justify-content: space-between;. The two children fly to opposite ends: logo left, links right, with all spare space between them. This works because the container has exactly two children; the three anchors are grandchildren and stay bundled inside their div. Step 3: add align-items: center;. The logo (taller, bigger font) and the links now share a vertical centerline instead of hanging from the top. Step 4: the links inside are their own one-dimensional row, so make the inner div a flex container too: .links { display: flex; gap: 16px; }, spacing the three anchors evenly. The finished CSS:

.bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 24px;
}
.links {
  display: flex;
  gap: 16px;
}

Nested flex containers like this are completely normal: the outer one solves "two groups at opposite ends," the inner one solves "three items in a spaced row." Each property was added for one visible reason, which is exactly how to build any flexbox layout: one declaration, one glance at the result, repeat.

flex: 1 versus flex: auto, with numbers

The difference between the two common shorthands shows up the moment items have different content. Put three items with natural widths of 100px, 150px, and 200px (total 450) into a 600px container, leaving 150px of spare space.

  • flex: auto (grow from natural size): the 150px of spare space is shared, 50px each. Final widths: 150, 200, 250. Items keep their proportions; bigger content stays bigger.
  • flex: 1 (grow from a basis of zero): natural sizes are discarded and the full 600px is divided equally. Final widths: 200, 200, 200. Perfect uniformity regardless of content.

Neither is "correct"; they answer different design questions. Equal-width cards in a row want flex: 1; a search bar that should swallow all spare space next to fixed buttons wants flex: 1 on the input only; a row of buttons that should stay content-sized but absorb a little slack wants flex: auto. When a flex layout sizes items in a way that surprises you, ask which basis, zero or natural, the items are growing from.

Two per-item dials: align-self and order

Container properties set policy for everyone, but two properties let a single item dissent. align-self overrides align-items for one item, so a "New!" badge can ride at the top (align-self: flex-start;) of a row whose other items are centered. And order changes visual position without touching the HTML: items default to order: 0 and are drawn in ascending order, so order: -1 pulls one item leftward and order: 1 pushes one to the end. Use order sparingly and never to fix broken source order: screen readers and keyboard tabbing follow the HTML, not the visual order, so a page that only makes sense after reordering is a page that confuses non-visual users.

A wrapping card row, predicted to the pixel

Combine wrapping with a basis and you can compute exactly how many cards fit per row. Take .cards { display: flex; flex-wrap: wrap; gap: 20px; } with each card set to flex: 1 1 250px;, meaning grow 1, shrink 1, basis 250px: each card wants 250px but may flex.

  1. Container at 900px: can three cards fit? 3 × 250 + 2 × 20 (two gaps) = 790, which fits in 900; four would need 4 × 250 + 3 × 20 = 1060, too wide. So each row holds three cards, and because grow is 1, the trio stretches to share the spare space: (900 − 40) / 3 = about 287px each.
  2. Container at 520px: two cards need 2 × 250 + 20 = 520, an exact fit, so rows hold two cards of 250px; a third wraps to the next line.
  3. Container at 320px (phone): even one 250px card plus nothing fits, so each row holds a single card stretched to the full 320px.

Three layouts, one rule, no media queries: the wrap arithmetic is doing responsive design by itself. This basis-plus-wrap pattern is flexbox's answer to the auto-fitting grid from the next lesson, and knowing the fit calculation, how many bases plus gaps go into the width, lets you predict rather than discover what any screen will show.

Common misconceptions

  • "justify-content and align-items are interchangeable ways to center." They act on different axes. In a row, justify-content moves items horizontally and align-items moves them vertically. Forgetting that flex-direction: column flips which is which is the most common flexbox confusion.
  • "Flexbox can lay out my whole two-dimensional page grid." Flexbox is fundamentally one-dimensional; it excels at a single row or column. For layouts where rows and columns must align together, CSS grid is the proper tool.
  • "I need margins between flex items." The gap property spaces items cleanly without the edge-case headaches of margins. Reach for gap first.
  • "flex: 1 keeps each item its natural size." flex: 1 discards the natural size and splits the space equally, so items with different content still end up equal width.

Recap

  • display: flex makes a container; its direct children become items.
  • justify-content works along the main axis; align-items works across the cross axis.
  • flex-direction: column swaps which axis is which.
  • flex-wrap lets items move onto new lines; gap spaces them.
  • flex on items lets them grow and shrink; equal weights split the space evenly.
  • Use flexbox for one-dimensional layouts and grid for two dimensions.

Sources

  1. MDN Web Docs. (n.d.). Flexbox. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). Basic concepts of flexbox. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). Aligning items in a flex container. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). flex. Mozilla. developer.mozilla.org
  5. Google. (n.d.). Learn CSS: Flexbox. web.dev. web.dev
  6. CSS-Tricks. (n.d.). A complete guide to CSS flexbox. css-tricks.com
  7. W3C. (n.d.). CSS flexible box layout module level 1 [Specification]. World Wide Web Consortium. find source β†—
Key terms
Flexbox
A CSS layout system that arranges items in a single row or column.
Flex container
An element with display: flex whose direct children become flex items.
justify-content
The property that distributes flex items along the main axis (the row).
align-items
The property that aligns flex items across the container (the cross axis).
flex-wrap
A property that lets flex items flow onto multiple lines when space runs out.
gap
A property that sets even spacing between flex or grid items.
flex shorthand
The property bundling flex-grow, flex-shrink, and flex-basis to control how an item sizes.

CSS Grid: Two-Dimensional Layout

  • Define a grid with rows and columns.
  • Size tracks with fr units and repeat().
  • Place content into a grid.

The big picture

Where flexbox arranges items in one direction, CSS grid arranges them in two at once: rows and columns together. It is the most powerful layout tool in CSS and is ideal for page skeletons and card galleries. This lesson shows how to define a grid, size its tracks flexibly, and place items into it.

Key idea: grid lays items out in rows and columns at the same time, which flexbox cannot do.

Turning on the grid

Set display: grid; on a container, then define its columns with grid-template-columns. Each value you list defines one column's width, and the number of values sets how many columns there are. The container's direct children then become grid items, just as they become flex items under flexbox. Picture a spreadsheet or a chessboard: you draw the grid of cells first, and content drops into the squares.

.gallery {
  display: grid;
  grid-template-columns: 200px 200px 200px;
}

That makes three columns each 200 pixels wide. Items you place inside flow into the cells automatically, filling the first row left to right, then wrapping to the next, so you rarely have to position anything by hand for a simple gallery.

Key idea: display: grid plus grid-template-columns defines the columns, and items flow into the cells automatically.

The fr unit and repeat()

Fixed pixel columns are rigid and do not adapt to screen size. The grid-specific fr unit (short for fraction) divides the available space into shares. Three columns of 1fr 1fr 1fr split the width into equal thirds that stretch and shrink with the screen, which is exactly what you usually want. Because typing the same value repeatedly is tedious, repeat() is a shorthand:

.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}

You can mix units freely within one template, which is where grid shines for page structure. A common page layout is a fixed sidebar next to a flexible main area:

.layout {
  display: grid;
  grid-template-columns: 250px 1fr;
}

Here the sidebar is always 250px and the main content takes all the remaining space, because 1fr means "one share of whatever is left" after the fixed 250px is subtracted. Add grid-template-rows the same way to control row heights, though rows often size themselves to their content automatically.

Key idea: the fr unit shares out leftover space, and repeat() saves typing while mixing fr with fixed sizes builds real page layouts.

Spanning cells and placing items

By default each item occupies one cell, but items can span several. Writing grid-column: span 2; on an item makes it stretch across two columns, which is how you build a featured card wider than its neighbors or a header that runs the full width above a two-column body. More advanced placement lets you name lines and address exact cells, but spanning alone handles a large share of real designs. Grid's ability to make one item cover a rectangle of cells, while others flow around it, is something flexbox simply cannot do.

Key idea: grid-column: span 2 lets an item cover several columns, so featured items and full-width headers are easy.

Intrinsically responsive grids

Grid becomes genuinely powerful when you combine repeat() with two helpers. minmax(min, max) sets a track to flex between a floor and a ceiling, so minmax(200px, 1fr) means "never narrower than 200px, otherwise take an equal share." The keyword auto-fit inside repeat() tells the browser to fit as many columns as will fit rather than a fixed number. Put together:

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 16px;
}

This one line produces a gallery that shows as many columns as the screen allows, each at least 200px wide, reflowing to fewer columns as the window narrows, with no breakpoints at all. On a wide monitor it might render five columns; on a phone, one, and the CSS never mentions a specific device width.

Key idea: repeat(auto-fit, minmax(...)) makes a grid respond to available space on its own, without media queries.

Grid or flexbox?

A simple guide: reach for flexbox when you are laying out along one line, like a navigation bar or a row of buttons, where the items and their content decide the sizing. Reach for grid when you need rows and columns to line up together, like a photo gallery or a full page layout, where you define the structure first and drop content into it. They cooperate happily, and it is extremely common to use grid for the overall page skeleton and flexbox inside individual pieces such as a card's header.

Key idea: flexbox is content-driven and one-dimensional, grid is layout-driven and two-dimensional, and they work well together.

A full page skeleton in twelve lines

Grid's home turf is the overall page: header across the top, sidebar and content in the middle, footer across the bottom, footer pinned to the bottom even when content is short. Here it is, complete:

.page {
  display: grid;
  grid-template-columns: 220px 1fr;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;
  gap: 16px;
}
header, footer {
  grid-column: 1 / 3;
}

Trace every choice. The columns are a fixed 220px sidebar track and a flexible 1fr content track. The rows are auto (header: exactly as tall as its content), 1fr (the middle stretches to absorb all leftover height), and auto (footer hugs its content). min-height: 100vh makes the grid at least as tall as the window, and because the middle row soaks up the excess, the footer sits at the window's bottom on a short page instead of floating mid-screen, a layout that plagued developers for a decade before grid. The grid-column: 1 / 3 on header and footer uses line numbers: a two-column grid has three vertical lines, numbered 1, 2, 3, and "from line 1 to line 3" means spanning both columns. It is the same effect as span 2, written as coordinates. In the HTML, the order is simply header, aside, main, footer: the header fills row one (both columns), the aside and main auto-flow into row two's two cells, and the footer fills row three.

fr arithmetic you can check by hand

The fr unit distributes what is left, so you can predict pixel widths exactly. In a 960px-wide page with columns 220px 1fr and a 16px gap: the fixed track takes 220, the gap takes 16, so the fr track gets 960 − 220 − 16 = 724px. With columns 2fr 1fr in a 916px container and a 16px gap: available space is 900, split into 2 + 1 = 3 shares of 300 each, so the tracks are 600px and 300px. Percentages cannot express either of these cleanly, because they ignore gaps and fixed neighbors; fr exists precisely to make "whatever is left, in these proportions" a first-class idea. When a grid track renders a surprising size, run this subtraction, and the mystery usually dissolves.

Rows get the same controls

Everything columns can do, rows can too: grid-row: span 2 lets a tall card occupy two rows of a gallery, and grid-auto-rows: 180px sets the height of the rows grid creates automatically as items wrap. A practical gallery pattern combines the ideas: grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-auto-rows: 180px; yields uniform tiles at every width, with one featured tile enlarged by grid-column: span 2; grid-row: span 2;. The mental model stays constant: define the tracks, let items flow, then promote the few items that need more room.

auto-fit arithmetic, and its sibling auto-fill

The responsive gallery line can be computed by hand just like fixed tracks. With repeat(auto-fit, minmax(200px, 1fr)) and a 16px gap in a 900px container, the browser fits the largest column count n such that n × 200 + (n − 1) × 16 fits in 900. Test n = 4: 800 + 48 = 848, fits. Test n = 5: 1000 + 64 = 1064, too wide. So four columns, each stretched by the 1fr ceiling to (900 − 48) / 4 = 213px. Narrow the window to 500px and the same test gives two columns of (500 − 16) / 2 = 242px; at 320px, one full-width column. Every "magic" reflow is just this inequality re-solved as the width changes.

The sibling keyword auto-fill differs in exactly one situation: when there are fewer items than columns. Suppose the 900px gallery holds only two cards. auto-fit collapses the two empty tracks to zero width, so the two cards stretch across the full 900px, roughly 442px each. auto-fill keeps all four tracks alive, so the two cards each occupy one 213px track and the right half of the row stands empty. Neither is wrong: stretch-to-fill suits featured content; reserved slots suit a dashboard where items come and go and positions should not jump. The pair is a nice reminder that grid is declarative: you state the sizing policy, and the browser re-derives the layout continuously from it.

Common misconceptions

  • "Grid replaces flexbox; I only need one." They solve different problems. Grid is for two-dimensional layouts where rows and columns align; flexbox is for one-dimensional rows or columns. Skilled developers use both, often together.
  • "The fr unit is just a percentage." fr distributes the space that remains after fixed sizes and gaps, which percentages cannot do cleanly. In 250px 1fr, the 1fr column takes exactly what is left.
  • "You must define grid-template-rows for a grid to work." Rows are created automatically as items wrap and size to their content by default. Many real grids define columns only.
  • "colspan works in CSS grid." colspan is an HTML table attribute. In grid you span columns with grid-column: span 2.

Recap

  • display: grid plus grid-template-columns defines the grid; items flow into cells.
  • The fr unit shares leftover space; repeat() shortens repeated tracks.
  • Mix fr with fixed sizes for layouts like 250px 1fr.
  • grid-column: span 2 spans several columns.
  • repeat(auto-fit, minmax(...)) makes a grid responsive without breakpoints.
  • Use grid for two dimensions, flexbox for one, often together.

Sources

  1. MDN Web Docs. (n.d.). CSS grid layout. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). Basic concepts of grid layout. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). minmax(). Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). repeat(). Mozilla. developer.mozilla.org
  5. Google. (n.d.). Learn CSS: Grid. web.dev. web.dev
  6. CSS-Tricks. (n.d.). A complete guide to CSS grid layout. css-tricks.com
  7. W3C. (n.d.). CSS grid layout module level 1 [Specification]. World Wide Web Consortium. find source β†—
Key terms
CSS grid
A layout system that arranges items into rows and columns simultaneously.
grid-template-columns
The property that defines the number and size of a grid's columns.
fr unit
A grid unit representing a fraction of the available space.
repeat()
A shorthand that repeats a track definition a given number of times.
grid gap
The spacing between grid rows and columns, set with the gap property.
grid item
A direct child of a grid container that occupies one or more cells.
minmax()
A grid function setting a track to flex between a minimum and maximum size.

Responsive Design and Media Queries

  • Explain why responsive design matters.
  • Write media queries that adapt to screen width.
  • Use relative units and the viewport meta tag.

The big picture

People visit websites on phones, tablets, laptops, and large monitors, and on many sites more traffic now comes from phones than desktops. Responsive design is the practice of building one page that reshapes itself to look good on any screen. This lesson covers the viewport tag, relative units, and the media queries that switch styles by screen size.

Key idea: responsive design serves every device from one codebase that adapts, rather than separate sites per device.

The viewport meta tag

Responsive design starts in the HTML head with one line you already saw in the skeleton:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

Without it, phones pretend to be a wide desktop, around 980 pixels, and shrink the whole page to fit, making text tiny and forcing users to pinch and zoom. This tag tells the browser to use the device's real width and not to zoom out, so your responsive styles actually take effect. It is easy to forget, and its absence is a classic reason a "responsive" site still looks broken on a phone.

Key idea: the viewport meta tag makes phones use their real width, without which responsive CSS never takes effect.

Relative units

Fixed pixel sizes do not adapt to different screens or user preferences. Responsive layouts therefore lean on relative units:

  • Percentages size an element relative to its parent, so width: 50% is always half the container, whatever the container's size.
  • rem sizes things relative to the root font size, so setting the root size once scales the whole page together, and it respects a user who has increased their browser's default text size for readability.
  • Viewport units like vw and vh are a percentage of the window's width or height, useful for full-screen sections.

Preferring these units over fixed pixels is a large part of what makes a layout flow rather than break. Pixels still have their place for things like borders, but for widths and text sizes, relative units keep a design flexible.

Key idea: relative units like percent, rem, and viewport units let a layout flow and respect the user's text-size choices.

Media queries

The heart of responsive design is the media query: a block of CSS that applies only when a condition about the screen is true, most often its width. You write @media, a condition in parentheses, then a set of rules in braces that take effect only while the condition holds. Think of it as an "if the screen is at least this wide, then use these styles" switch.

/* base styles: single column for phones */
.cards {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
}

/* on wider screens, switch to three columns */
@media (min-width: 700px) {
  .cards {
    grid-template-columns: repeat(3, 1fr);
  }
}

Read it carefully. The base rule gives one column, which is right for a narrow phone. The media query says: when the screen is at least 700 pixels wide, use three columns instead. Below 700px the query is dormant and the base rule stands; at 700px and above it activates and overrides the column count. The width at which the layout changes is called a breakpoint.

Key idea: a media query is a block of CSS that switches on only when a screen condition, like a minimum width, is true.

A worked example of a breakpoint decision

Suppose a phone is 375px wide, a tablet 800px, and a laptop 1300px, and you have written the media query above. Trace each: the phone at 375px is below 700, so the query does not fire and the visitor sees one column. The tablet at 800px is above 700, so the query fires and the visitor sees three columns. The laptop at 1300px is also above 700, so it too sees three columns. One breakpoint has produced two sensible layouts from a single stylesheet. Add a second query at 1100px to introduce a fourth column for very wide screens, and you tune the design further without ever duplicating the page.

Key idea: tracing each device against the breakpoint tells you exactly which layout it will see.

Mobile-first thinking

Writing the simple phone layout first and adding complexity for larger screens with min-width queries is called the mobile-first approach. It tends to produce cleaner, faster pages because the smallest devices get the leanest styles by default and never download rules meant for large screens they will not use. You can layer several breakpoints, for example one at 700px for tablets and another at 1100px for wide desktops, so a single stylesheet gracefully serves every visitor. Media queries can also test far more than width, such as prefers-color-scheme for dark mode or prefers-reduced-motion for users who ask to minimize animation.

Key idea: mobile-first means writing the lean small-screen layout first, then enhancing it upward with min-width queries.

A responsive navigation bar, traced at two widths

The navbar from the flexbox lesson becomes truly usable once it adapts. Mobile-first, the base styles stack everything; one query converts it to a row when space allows:

.bar {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

@media (min-width: 700px) {
  .bar {
    flex-direction: row;
    justify-content: space-between;
  }
}

Trace a 375px phone: the query is dormant, so the bar is a centered vertical stack, brand above links, each an easy touch target. Trace a 900px laptop: the query activates, the main axis rotates to horizontal, justify-content now spreads brand and links to opposite ends, and the same markup renders as a classic desktop navbar. Notice what did not happen: no HTML changed, no elements were hidden, and both layouts came from the same eleven lines. This is the standard shape of real-world responsive CSS: a lean base, then a handful of queries that each rearrange, never duplicate.

The centered content column

Nearly every site needs a main column that is fluid on small screens and capped on large ones. The modern one-liner:

.container {
  width: min(100%, 1100px);
  margin: 0 auto;
  padding: 0 16px;
}

The min() function takes the smaller of its arguments at any moment: on a phone, 100% (say 375px) is smaller, so the column fills the screen; on a desktop, 1100px is smaller, so the column caps there, and margin: 0 auto centers the capped column by splitting the leftover space into equal left and right margins. The side padding keeps text off the screen edges on phones. Three declarations produce behavior that would otherwise take a media query, illustrating a broader trend: modern CSS pushes toward layouts that flex continuously, with media queries reserved for genuine rearrangements.

Choosing breakpoints like a professional

Beginners often ask for the official list of device widths to target. There is none, and chasing one is the wrong model: there are thousands of devices, new sizes ship monthly, and a design tuned to specific phones is obsolete in a year. The professional method is content-first: start narrow, widen the browser slowly, and add a breakpoint at the width where the design itself starts to fail, where a text line grows uncomfortably long, a nav wraps awkwardly, or a card row could comfortably fit another column. Those failure points depend on your font size and content, not on any device catalog. In practice most sites end up with two or three breakpoints, commonly somewhere near 600 to 800px and again near 1000 to 1200px, but treat those as coincidences of typical content, not targets. Combined with fluid techniques, flexible grids, min(), wrapping flex rows, a good design spends most of its life between breakpoints looking fine without any query firing at all.

Testing responsively without a drawer of phones

Every desktop browser contains a device simulator: in the developer tools, a toggle (the small phone-and-tablet icon) switches the page into a resizable viewport with presets for common phone and tablet sizes. The professional testing pass takes five minutes and three checks. First, drag the width slowly from 320px (the practical floor for modern phones) up to full desktop, watching for the failure points, text lines growing too long, elements colliding or overflowing, layouts snapping awkwardly at your breakpoints; anything that breaks mid-drag will break on some real device. Second, zoom the text: browsers let users enlarge text up to 200 percent, your rem-based sizes should scale gracefully, and any container that clips its grown text needs fixing. Third, flip orientation, because a phone held sideways is a short, wide screen that surprises layouts tuned only for portrait; a query like @media (orientation: landscape) exists for the rare case that needs explicit handling. One technicality prevents a common confusion: media queries measure CSS pixels, not hardware pixels. A phone advertising a 1170-pixel-wide display typically reports 390 CSS pixels, because it maps three hardware pixels to each CSS pixel for sharpness. That is why your 700px breakpoint correctly treats such a phone as narrow, and why you never need to write queries against marketing spec-sheet resolutions.

Common misconceptions

  • "Responsive design means a separate mobile site." The whole point is one page that adapts, using flexible units and media queries. Separate mobile sites are a maintenance burden and a legacy approach.
  • "I can skip the viewport meta tag if my CSS is responsive." Without it, phones render at a fake desktop width and zoom out, so your media queries never see the real screen size and the page looks tiny.
  • "More breakpoints always make a design better." Too many breakpoints are hard to maintain and can produce awkward in-between states. Design with flexible units so the layout flows, adding breakpoints only where the design truly needs to change.
  • "Media queries only test width." They can test orientation, pixel density, color-scheme preference, reduced-motion preference, and more.

Recap

  • Responsive design adapts one page to any screen.
  • The viewport meta tag makes phones use their real width.
  • Relative units (percent, rem, vw, vh) keep layouts flexible.
  • A media query applies CSS only when a screen condition is met.
  • A breakpoint is the width where the layout changes.
  • Mobile-first writes the small-screen layout first, then enhances with min-width queries.

Sources

  1. MDN Web Docs. (n.d.). Responsive web design. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). Using media queries. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). <meta name="viewport">: The viewport meta element. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). CSS values and units. Mozilla. developer.mozilla.org
  5. Google. (n.d.). Learn Responsive Design. web.dev. web.dev
  6. Google. (n.d.). Learn Responsive Design: Media queries. web.dev. web.dev
  7. W3C. (n.d.). Media queries level 5 [Specification]. World Wide Web Consortium. find source β†—
Key terms
Responsive design
Building one page that adapts its layout to any screen size.
Viewport meta tag
The head tag that tells mobile browsers to use the device's real width.
Media query
A CSS block that applies only when a screen condition, such as a minimum width, is met.
Breakpoint
A screen width at which the layout changes.
rem
A unit relative to the root font size, letting a whole page scale together.
Mobile-first
Designing the small-screen layout first, then enhancing it for larger screens.
Container query
A newer query letting an element adapt to the size of its own container rather than the viewport.

Module 7: JavaScript and the DOM

Variables, functions, events, and manipulating the page to make it interactive. This module gives static pages a pulse, letting them respond to clicks, keystrokes, and form input in real time.

JavaScript Basics: Variables and Functions

  • Declare variables and use core data types.
  • Write and call functions.
  • Add JavaScript to a page.

The big picture

JavaScript is the programming language of the browser. Where HTML and CSS describe a page, JavaScript lets it act: respond to clicks, change content, validate a form. This lesson covers the language basics you need before touching the page: variables, data types, functions, decisions, and how to run your code.

Key idea: JavaScript is a full programming language, and this lesson teaches its grammar before the next one points it at the page.

Variables

A variable is a named container for a value, like a labeled jar you can put something into and take it out later. Modern JavaScript declares variables with let (for values that can change) or const (for values that will not). Prefer const unless you know the value must change, because a name that cannot be reassigned is easier to reason about and prevents a class of accidental bugs.

let score = 0;
const name = "Ada";
score = score + 10;

Statements end with a semicolon. Note that const stops you from reassigning the name to a different value; it does not freeze the contents of an object or array the name points to. There is an older keyword, var, which you will see in legacy code but should avoid in new code because its scoping rules are surprising and error-prone.

Key idea: use const by default and let when a value must change, and end statements with a semicolon.

Data types

The core data types you will use are numbers (42, 3.14; JavaScript has just one number type for both whole and decimal values), strings (text in quotes), and booleans (true or false). Text can be joined with the + operator, so "Hello, " + name builds a greeting. Two special values, null and undefined, both represent "no value," with undefined being what you get from a variable that has not been assigned yet.

Key idea: the everyday types are numbers, strings, and booleans, plus null and undefined for the absence of a value.

Functions

A function is a reusable block of code you define once and run whenever you call it by name. Think of it as a recipe: you write the steps once, then follow them any time you need that dish. Functions can take parameters (inputs) and return a result. They are the primary way to organize code and avoid repetition.

function greet(person) {
  return "Hello, " + person + "!";
}

let message = greet("Sam");

Calling greet("Sam") runs the function with person set to "Sam" and hands back "Hello, Sam!", which we store in message. The value handed back by return is the function's result; if a function has no return, it produces undefined. Many modern examples use a shorter arrow function form:

const square = (n) => n * n;
let result = square(5);   // result is 25

Key idea: a function is reusable named code that takes inputs and returns a result, and no return means it evaluates to undefined.

Making decisions

An if statement runs code only when a condition is true, and an optional else handles the other case. Comparisons use === for "equal to", !== for "not equal", and > and < for greater and less than. Prefer the three-character === over the two-character ==, because === compares without surprising type conversions, whereas == can judge the number 0 equal to the string "0".

if (score > 50) {
  message = "You passed!";
} else {
  message = "Try again.";
}

Key idea: if and else choose which code runs, and === compares safely without converting types.

Running your code

Attach a script to your page with a <script> tag just before the closing </body>, so the page has loaded before the code runs and can find the elements it needs:

  <script src="script.js"></script>
</body>

While learning, console.log(...) prints a value to the browser's developer console (opened with the F12 key), which is invaluable for checking what your code is actually doing rather than guessing. When something behaves unexpectedly, logging the values along the way is usually the fastest path to understanding. With variables, functions, and conditions you now have the grammar of the language; next you will point it at the page.

Key idea: put the script just before the closing body tag, and use console.log to see what your code is doing.

Arrays: one name, many values

Pages constantly deal in lists: quiz scores, task items, image URLs. An array holds an ordered collection under one name, written with square brackets:

const scores = [86, 92, 75];
scores.length;      // 3
scores[0];          // 86, the FIRST item
scores[2];          // 75
scores.push(90);    // scores is now [86, 92, 75, 90]

Two conventions matter immediately. Positions count from zero, so scores[0] is the first item and scores[2] the third; off-by-one confusion here is a rite of passage, so internalize "index = how many steps from the start." And notice push modified an array declared with const: legal, because const only locks the name-to-array binding, not the array's contents, exactly as the misconception list warned for objects.

Loops: doing something with every item

A for...of loop runs its body once per item. Here is a complete average calculation, traced honestly:

const scores = [86, 92, 75];
let total = 0;
for (const s of scores) {
  total = total + s;
}
const average = total / scores.length;
console.log(average);   // 84.33...

Trace it like the machine: total starts at 0. Pass one: s is 86, total becomes 86. Pass two: s is 92, total becomes 178. Pass three: s is 75, total becomes 253. The loop ends (no more items), and 253 / 3 gives 84.33. Every list computation you will ever write, summing a cart, counting completed tasks, building a menu from an array, is this pattern: an accumulator declared before the loop, updated once per item, used after. Note the accumulator must be let, since it is reassigned, while the loop variable s is a fresh const each pass.

A type gotcha worth meeting early

The + operator adds numbers but joins strings, and when the two sides disagree, the string wins: "5" + 1 is "51", not 6. This bites in real code because everything read from a form field arrives as a string, so a "total" built from an input can silently become text concatenation: 5 + 1 items showing as 51. The cure is explicit conversion: Number("5") + 1 is 6, and typeof x tells you what you actually have ("string", "number", "boolean"). When arithmetic misbehaves, log the values and their types; nine times out of ten, a number you trusted is wearing quotes.

Where a variable lives: block scope

Variables declared with let or const exist only inside the braces where they are declared, their block. A variable declared inside an if block or a loop body vanishes at the closing brace, and referring to it outside is an error. This is a feature: it keeps temporary names from leaking and colliding across a file. The practical rule you will use next lesson: declare a variable in the smallest block that needs it, and when state must survive across many runs of a function (like a click counter), declare it outside that function. Scope, more than any syntax, is what separates code that merely runs from code you can reason about.

Putting the pieces together: a grading function

Variables, decisions, and functions combine into the everyday shape of real code. Here is a function that converts a numeric score into a letter grade, using the chained form else if to test conditions in order:

function gradeFor(score) {
  if (score >= 90) {
    return "A";
  } else if (score >= 80) {
    return "B";
  } else if (score >= 70) {
    return "C";
  } else {
    return "F";
  }
}

console.log(gradeFor(86));   // "B"
console.log(gradeFor(70));   // "C"

Trace gradeFor(86) the way the machine runs it: is 86 at least 90? No, move on. At least 80? Yes, so return "B" ends the function immediately; the remaining tests never run. That early exit is why the conditions can be simple: by the time the 80 test runs, you already know the score is below 90, so there is no need to write "between 80 and 89." Order is therefore load-bearing: test the highest threshold first, or every score would match the lowest test it reaches. Trace gradeFor(70) to confirm the boundary: 70 fails the 90 and 80 tests but passes >= 70, so exactly 70 earns a C, which is the kind of edge you should always check by hand. Functions like this, a clean input, a chain of ordered decisions, one returned answer, are the bread and butter of application code, and next lesson this exact skill starts deciding what a page displays.

Common misconceptions

  • "=, ==, and === all mean equals." A single = assigns a value to a variable; == compares but converts types first, which causes surprises; === compares value and type without conversion. Use = to assign and === to compare.
  • "const means the value can never change at all." const only prevents reassigning the variable name. If it points to an object or array, that object's contents can still be modified.
  • "A function always gives back a value." Only if it has a return statement. A function without return still runs but evaluates to undefined, so storing its result gives undefined.
  • "Where I place the script tag does not matter." A script in the head can run before the elements exist. Placing it just before the closing body tag ensures the page is ready.

Recap

  • Variables are named containers; prefer const, use let when a value changes.
  • Core types are numbers, strings, and booleans, with null and undefined for no value.
  • Functions are reusable named code that take parameters and return a result.
  • No return means a function evaluates to undefined.
  • if and else make decisions; === compares without type conversion.
  • Place the script before the closing body tag and use console.log to debug.

Sources

  1. MDN Web Docs. (n.d.). Storing the information you need: Variables. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). Functions: Reusable blocks of code. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). Making decisions in your code: Conditionals. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). Arrays. Mozilla. developer.mozilla.org
  5. MDN Web Docs. (n.d.). Strict equality (===). Mozilla. developer.mozilla.org
  6. Kantor, I. (n.d.). Variables. JavaScript.info. javascript.info
  7. Kantor, I. (n.d.). Functions. JavaScript.info. javascript.info
Key terms
Variable
A named container for a value, declared with let or const.
const
A declaration for a variable whose value will not be reassigned.
Function
A reusable, named block of code that can take inputs and return a result.
Parameter
A named input a function receives when it is called.
if statement
A control structure that runs code only when a condition is true.
console.log
A command that prints a value to the browser's developer console for debugging.
=== (strict equality)
An operator that compares value and type without converting, preferred over ==
Arrow function
A concise function syntax written with =>, common for small functions.

The DOM, Events, and Interactivity

  • Select and change page elements with the DOM.
  • Respond to user actions with event listeners.
  • Build a small interactive feature.

The big picture

When the browser loads your HTML, it builds a live model of the page called the DOM that your JavaScript can read and change. This lesson connects the language from the last lesson to the page: selecting elements, changing their content and style, and responding to user actions with event listeners. This is where web development becomes visibly interactive.

Key idea: the DOM is a live version of the page in memory, and changing it updates what the user sees at once.

What the DOM is

The DOM (Document Object Model) represents every element as an object your JavaScript can read and change, arranged in the same parent-child tree as your nested tags. Picture it as a family tree of the page: the html element is the ancestor, and every nested tag is a descendant with exactly one parent. Change a node in that tree, and the page redraws instantly.

Key idea: the DOM is a family tree of page objects, and JavaScript edits that tree to change the page.

Selecting elements

You reach into the page through the global document object, your entry point to everything on the page. The most flexible selector is querySelector, which takes a CSS selector string, the very same selector syntax you learned for styling, and returns the first matching element.

const title = document.querySelector("#main-title");
const firstButton = document.querySelector(".btn");

Because it accepts any CSS selector, one method covers ids, classes, tags, and combinations, so it is worth making your default. Use querySelectorAll to get every match as a list you can loop over with a for...of loop, for example to attach the same behavior to many buttons.

Key idea: document.querySelector takes a CSS selector and returns the first matching element; querySelectorAll returns them all.

Changing content and style

Two properties do most everyday work. textContent reads or sets an element's text, and style lets you set CSS properties, written in camelCase because hyphens are not allowed in property names, so background-color becomes backgroundColor and font-size becomes fontSize.

title.textContent = "Welcome back!";
title.style.color = "teal";

Setting styles directly is fine for one-off changes, but the cleaner habit for anything reusable is to toggle CSS classes with element.classList.add("active"), remove("active"), or toggle("active"). This keeps the actual styling in your stylesheet where it belongs and lets JavaScript simply switch states on and off.

Key idea: textContent changes an element's text, and toggling a class is cleaner than setting many styles directly.

Events

An event is something that happens on the page: a click, a key press, a form submission, the mouse moving. You react to one by registering an event listener with addEventListener. You give it the event name and a function to run when it fires. Rather than checking over and over "has it been clicked yet?", you leave a note that says "when a click happens, run this," and the browser honors it.

const button = document.querySelector("#say-hi");
button.addEventListener("click", function () {
  alert("Hello there!");
});

Now every click on that button runs the function. The function you pass is often called the handler or callback. Common event names are click, input (fired repeatedly as the user types in a field), and submit (for forms, where you often call the event's preventDefault() method to stop the page from reloading so you can handle the data in JavaScript).

Key idea: addEventListener registers a handler to run when a named event fires, instead of repeatedly checking for it.

Putting it together: a counter

Here is a complete, correct example. The HTML has a display and a button; the JavaScript makes clicking the button raise a count.

<p id="count">0</p>
<button id="add">Add one</button>

<script>
  let total = 0;
  const display = document.querySelector("#count");
  const addBtn = document.querySelector("#add");

  addBtn.addEventListener("click", function () {
    total = total + 1;
    display.textContent = total;
  });
</script>

Trace it: a variable total starts at 0; we grab the two elements and store them in constants so we do not search for them on every click; and we register a click listener on the button.

Each click adds one to total and writes the new number into the paragraph using textContent. Notice that total lives outside the handler so its value persists between clicks; if it were declared inside, it would reset to 0 every time.

Key idea: keep state in a variable outside the handler so it persists, and write the updated state back into the DOM.

Why this pattern matters

That small loop, an event changing a variable that updates the DOM, is the essence of interactive web pages. A to-do list, a shopping cart, a game score, a live search: all of them are elaborations of exactly this cycle of listen, update state, reflect the state back into the DOM. Frameworks like React and Vue exist largely to manage this same cycle more conveniently as pages grow, but the underlying idea is the one you can now build by hand.

Key idea: listen, update state, reflect it into the DOM is the cycle behind every interactive feature.

A second traced example: the live character counter

The counter used a click; this one reacts to typing, the input event, which fires on every keystroke in a field. A message box allows 200 characters and shows how many remain:

<textarea id="msg"></textarea>
<p><span id="left">200</span> characters left</p>

<script>
  const msg = document.querySelector("#msg");
  const left = document.querySelector("#left");

  msg.addEventListener("input", function () {
    left.textContent = 200 - msg.value.length;
  });
</script>

Follow one keystroke through the machine: the user types "H"; the browser fires an input event on the textarea; the registered handler runs; it reads msg.value (form fields expose their current text as .value, not textContent), measures its length, 1, subtracts from 200, and writes 199 into the span; the page repaints. Event, handler, DOM update: the same three beats as the click counter, at typing speed. Every live interface you have ever used, search suggestions, form hints, character limits, is this loop running fast enough to feel continuous.

Forms and preventDefault: taming the page reload

A form's default action on submit is to send the data and load a new page, which in a JavaScript-driven page throws away everything. To handle submission yourself, listen for submit on the form (not click on the button) and call the event object's preventDefault():

const form = document.querySelector("#signup");
const nameField = document.querySelector("#name");
const error = document.querySelector("#error");

form.addEventListener("submit", function (event) {
  if (nameField.value.trim() === "") {
    event.preventDefault();
    error.textContent = "Please enter your name.";
  }
});

The handler now receives the event object as a parameter, a bundle of information about what happened, and preventDefault() is its brake pedal: it cancels the built-in action while your code decides what to do instead. Here, an empty name blocks submission and shows a message (trim() strips spaces so a name of blanks does not sneak through); a filled name lets the browser submit normally. The classic bug is forgetting the call: the symptom is a page that flashes, reloads, and loses your validation message, and the diagnosis is always "the default action ran." The same method stops a link from navigating, which is how single-page interfaces intercept clicks.

Fetching data: the same request, two syntaxes

Modern pages load data without reloading, using fetch. It is asynchronous: the network takes time, so fetch returns immediately with a promise, an object that will deliver the response later, and you say what to do when it arrives. First, the promise chain form, loading a quote from a JSON file:

fetch("data/quote.json")
  .then(function (response) {
    if (!response.ok) {
      throw new Error("HTTP " + response.status);
    }
    return response.json();
  })
  .then(function (data) {
    quote.textContent = data.text;
  })
  .catch(function (error) {
    quote.textContent = "Could not load the quote.";
  });

Each .then is a "when this is ready" step: the first receives the HTTP response, checks response.ok (a 404 still counts as a response, so you must check the status yourself, a famous gotcha), and asks for the body parsed as JSON, itself a second wait; the next .then receives the parsed data and writes it into the page; .catch handles network failure or the thrown error. Now the exact same request with async/await syntax:

async function loadQuote() {
  try {
    const response = await fetch("data/quote.json");
    if (!response.ok) {
      throw new Error("HTTP " + response.status);
    }
    const data = await response.json();
    quote.textContent = data.text;
  } catch (error) {
    quote.textContent = "Could not load the quote.";
  }
}
loadQuote();

Nothing changed but the shape: await pauses this function (never the whole page) until the promise settles, so the code reads top to bottom like the synchronous code you already know, with try/catch as the error path. The two forms are interchangeable, you will read both in the wild, and both run on the event loop described in the last lesson's deep dive.

One more thing you will meet quickly: browsers enforce the same-origin policy, so a fetch to a different site is blocked unless that server opts in with CORS (Cross-Origin Resource Sharing) headers. The symptom is an error mentioning CORS in the console while the same URL works fine elsewhere; the fix belongs to the server (sending Access-Control-Allow-Origin), not to your JavaScript. Requests to your own site, like the relative URL above, are never affected.

One efficiency habit: touch the DOM once

DOM updates are the expensive part of this loop, so batch them. Building a list the slow way, list.innerHTML += "<li>" + t + "</li>" inside a loop, forces the browser to reparse and rebuild the list on every pass. The fast, equally simple way: accumulate a string, then write once.

let html = "";
for (const t of tasks) {
  html = html + "<li>" + t + "</li>";
}
list.innerHTML = html;

One hundred items, one DOM write instead of one hundred. The principle, compute in variables, then reflect into the DOM once, scales from this loop to entire frameworks, whose core job is precisely to minimize and batch DOM updates for you.

Common misconceptions

  • "Changing the page with JavaScript edits my saved file." DOM changes only affect the live page in the current session. Your source file is untouched, and a refresh rebuilds the DOM from that unchanged file, discarding your script's changes unless the script runs again.
  • "I set style with hyphenated names like element.style.background-color." In JavaScript, CSS property names are camelCase, so it is element.style.backgroundColor. The hyphenated form is a syntax error because the hyphen reads as subtraction.
  • "querySelector returning null is a random bug." If a script in the head runs before the elements exist, querySelector genuinely finds nothing and returns null. Placing the script just before the closing body tag ensures the elements are present.
  • "State can live inside the handler." A variable declared inside the handler resets every time it runs; keep persistent state outside so it survives between events.

Recap

  • The DOM is the browser's live object tree of the page.
  • document.querySelector selects the first match; querySelectorAll selects all.
  • textContent changes text; classList toggles CSS classes cleanly.
  • addEventListener runs a handler when a named event fires.
  • Keep persistent state outside the handler and write it back into the DOM.
  • Listen, update state, reflect it into the DOM is the core interactive cycle.

Sources

  1. MDN Web Docs. (n.d.). DOM scripting introduction. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). Introduction to events. Mozilla. developer.mozilla.org
  3. MDN Web Docs. (n.d.). EventTarget: addEventListener() method. Mozilla. developer.mozilla.org
  4. MDN Web Docs. (n.d.). Using the Fetch API. Mozilla. developer.mozilla.org
  5. MDN Web Docs. (n.d.). Making network requests with JavaScript. Mozilla. developer.mozilla.org
  6. MDN Web Docs. (n.d.). Cross-Origin Resource Sharing (CORS). Mozilla. developer.mozilla.org
  7. Kantor, I. (n.d.). Fetch. JavaScript.info. javascript.info
  8. Kantor, I. (n.d.). Event delegation. JavaScript.info. javascript.info
Key terms
DOM
The Document Object Model, the browser's live object representation of the page.
document
The global object that is your JavaScript entry point into the page.
querySelector
A method that returns the first element matching a CSS selector string.
textContent
A property for reading or setting the text inside an element.
Event
Something that happens on the page, such as a click, keypress, or form submit.
addEventListener
The method that registers a function to run when a given event fires.
classList
An element property with add, remove, and toggle methods for switching CSS classes.
Event delegation
Handling events for many child elements with one listener on a shared parent, using event bubbling.

Module 8: Build a Page - Capstone Project

Combine HTML, CSS, and JavaScript into one small, complete, interactive page. This is where every earlier idea comes together in something you build and can show off.

Project: A Personal Profile Card

  • Plan a small page combining all three languages.
  • Assemble semantic HTML, responsive CSS, and a JavaScript interaction.
  • Test and refine your finished page.

The big picture

Everything so far has been practice for this: building a complete page yourself. Your project is a personal profile card, a small page with your name, a short bio, a list of interests, and one interactive button. It is deliberately modest so you can finish it and see all three languages cooperate, which teaches the real craft in miniature.

Key idea: a small, finished page that combines HTML, CSS, and JavaScript teaches more than a large one you abandon.

Planning before coding

A moment of planning saves a lot of flailing. Decide the three layers up front: the structure is a heading, a bio paragraph, an interests list, a button, and an empty paragraph for the button to fill; the style is a centered white card with comfortable padding on a soft background; and the behavior is a click that reveals a fun fact. Naming your goal in terms of structure, style, and behavior is itself the separation-of-concerns habit at work, and it maps directly onto the three files you will edit.

Key idea: plan the page as structure, style, and behavior, which maps onto your HTML, CSS, and JavaScript files.

Step 1: The HTML structure

Start from the document skeleton and fill the body with semantic elements, choosing tags for what the content means.

<body>
  <main class="card">
    <h1>Jordan Lee</h1>
    <p class="bio">A student learning to build the web.</p>
    <h2>Interests</h2>
    <ul>
      <li>Photography</li>
      <li>Cycling</li>
      <li>Reading</li>
    </ul>
    <button id="fact-btn">Show a fun fact</button>
    <p id="fact"></p>
  </main>
  <script src="script.js"></script>
</body>

Notice the choices: <main> wraps the primary content, a single <h1> names the person, an unordered list holds the interests because their order does not matter, and the empty <p id="fact"> is a target the JavaScript will fill. The script sits just before </body> so the elements exist by the time it runs.

Key idea: semantic HTML gives the card meaning, and the empty paragraph is the target the script will write into.

Step 2: The CSS design

Style the card with the box model, color, and typography, and center it with flexbox. A generous max-width keeps it comfortable on large screens while width: 100% lets it shrink on small ones.

body {
  font-family: Georgia, serif;
  background: #eef2f5;
  display: flex;
  justify-content: center;
  padding: 40px 16px;
}
.card {
  background: white;
  max-width: 400px;
  width: 100%;
  padding: 28px;
  border: 1px solid #ccc;
  border-radius: 10px;
}
h1 {
  color: #1b4d8f;
  margin-top: 0;
}
button {
  background: #1b4d8f;
  color: white;
  border: none;
  padding: 10px 16px;
  border-radius: 6px;
  cursor: pointer;
}

This one stylesheet exercises much of the course: flexbox centers the card, the box model gives it padding and a border, border-radius rounds its corners, the color and font properties set its look, and cursor: pointer signals that the button is clickable.

Pairing max-width: 400px with width: 100% is a small piece of responsive design: the card is at most 400 pixels but shrinks to fit a narrow phone.

Key idea: max-width with width: 100% caps the card yet lets it shrink, a real responsive touch in one pairing.

Step 3: The JavaScript interaction

Make the button reveal a fun fact when clicked, using the DOM and an event listener, exactly the listen-update-reflect cycle from the last module.

const button = document.querySelector("#fact-btn");
const fact = document.querySelector("#fact");

button.addEventListener("click", function () {
  fact.textContent = "I once cycled 100 miles in a single day!";
});

The script selects the button and the empty paragraph, then listens for clicks on the button; each click writes the fact into the paragraph via textContent, and the browser shows it at once. It is only a few lines, but it is the same mechanism behind every interactive feature on the web.

Key idea: a single click listener that writes into the DOM is the whole interaction, and the same mechanism scales to any feature.

Step 4: Test and refine

Open the page, click the button, and confirm the fact appears. Resize the window narrow and wide to check the card adapts, and open the developer console with F12 to make sure there are no errors. Then make it yours: change the name, bio, and interests, adjust the colors, and add a second interaction if you like, perhaps a button that toggles a dark background by adding a class with classList.toggle. Work in the small edit-save-refresh loop the whole way, changing one thing at a time so any mistake is easy to trace.

A page like this is fully static, just files a server hands out unchanged, which is the easiest kind of site to publish: upload the three files to a host, point a domain at them, and the client/server exchange from Module 1 does the rest.

Key idea: test at several widths and in the console, refine in small loops, and remember a static site is the simplest kind to publish.

Extension: a rotating fact button

The single hard-coded fact upgrades naturally with an array, using the loop-and-list skills from Module 7. Replace the script with:

const button = document.querySelector("#fact-btn");
const fact = document.querySelector("#fact");

const facts = [
  "I once cycled 100 miles in a single day!",
  "I develop my own black-and-white film.",
  "I have read every Sherlock Holmes story twice."
];
let index = 0;

button.addEventListener("click", function () {
  fact.textContent = facts[index];
  index = (index + 1) % facts.length;
});

The state is now two pieces: the array of facts and a position index, both living outside the handler so they persist between clicks. Each click shows the current fact, then advances the index. The % (remainder) operator wraps the count: when index + 1 reaches 3, dividing by the array length 3 leaves remainder 0, so the fourth click loops back to the first fact. Click through it mentally: 0, 1, 2, 0, 1... a circular playlist in one line of arithmetic. This tiny upgrade demonstrates the deepest habit in interface code: when behavior gets richer, grow the state, and keep the handler a thin translation from state to screen.

Publishing your card: deployment basics

A finished page deserves a real URL. Because your project is static files, deployment is genuinely simple, and it works the same on any static host (GitHub Pages, Netlify, and similar services all offer free tiers).

  1. Pre-flight check. The home page must be named index.html and sit at the top of the folder you deploy, because servers serve that file for the bare URL, exactly as Module 2 promised. Every link and src must be a relative path; an absolute path from your own disk, like C:/Users/you/Desktop/photo.jpg, works on your machine and breaks for every other human on earth, which is the single most common first-deploy bug.
  2. Upload. Hosts accept the folder by drag-and-drop upload or by connecting a Git repository so every push republishes automatically. Either way, what happens underneath is only what Module 1 taught: your files are copied to a server that answers HTTP requests for them.
  3. Verify live. Visit the URL the host gives you, on your phone as well as your laptop. Modern static hosts serve over HTTPS automatically, so the padlock comes free.
  4. Update. Re-upload (or push) changed files. If an old version lingers, it is usually your browser's cache being helpful; a hard refresh fetches fresh copies.

A custom domain is one optional step more: buy the name, then point its DNS record at your host, and the phone-book lookup from Module 1 starts resolving your name to their server.

When it does not work: a debugging checklist

Something will misbehave eventually, and professionals debug with a ritual, not with staring. Run it in order: (1) Open the console; a red error names the file and line, and the first error is the one to fix, since later ones are often fallout. (2) If the script "does nothing" silently, check that every querySelector string exactly matches an existing id or class, singular and plural, capitals and hyphens; a selector that matches nothing returns null, and the error appears only when you use it. (3) Confirm the script tag's src path is right and that it sits just before </body>; a script that runs before the elements exist finds none of them. (4) Make the failure smaller: comment out half the code, refresh, and see whether the problem persists, then halve again. Ten disciplined minutes of this beats an hour of rereading, and the habit transfers unchanged to every framework and language in your future.

One live-site gotcha: case matters online

Test the deployed site as seriously as the local one, because one difference bites almost everyone once: most web servers run on Linux, where file names are case-sensitive, while Windows and macOS are forgiving. A page that references Photo.jpg while the file is named photo.jpg works perfectly on your laptop and serves a broken image from the live server. The insurance policy is a naming convention: all lowercase, hyphens instead of spaces, for every file you will ever put on the web, adopted now while your site is three files small. Then click through the live page, press the fact button, and open the console on the deployed URL one last time; if anything differs from local behavior, the answer is nearly always a path, a case mismatch, or a stale cached file.

Common misconceptions

  • "A capstone project should be big and impressive." A small project you finish and understand fully teaches more than a large one you abandon. The value is in seeing all three languages cooperate end to end.
  • "If the page looks right, the code is fine." Looking right and being correct differ. Open the console to catch silent JavaScript errors, and test at several widths, since a page perfect on your monitor can break on a phone.
  • "Write all the HTML, then all the CSS, then all the JS, and look only at the end." Building in one big batch makes bugs hard to locate. Small edit-save-refresh cycles are faster and far less frustrating.
  • "These three languages are just a beginner's stepping stone." The largest applications in the world still render down to the same HTML, CSS, and JavaScript; they remain the bedrock of the front end.

Recap

  • The project combines HTML structure, CSS design, and one JavaScript interaction.
  • Plan the page as structure, style, and behavior before coding.
  • Semantic HTML provides the card and a target paragraph for the script.
  • max-width with width: 100% makes the card responsive.
  • A click listener writing into the DOM is the whole interaction.
  • Test at several widths and in the console; a static site is the easiest to publish.

Sources

  1. MDN Web Docs. (n.d.). Publishing your website. Mozilla. developer.mozilla.org
  2. MDN Web Docs. (n.d.). How do you upload your files to a web server?. Mozilla. developer.mozilla.org
  3. GitHub. (n.d.). What is GitHub Pages? GitHub Docs. docs.github.com
  4. MDN Web Docs. (n.d.). DOM scripting introduction. Mozilla. developer.mozilla.org
  5. MDN Web Docs. (n.d.). Responsive web design. Mozilla. developer.mozilla.org
  6. Google. (n.d.). Learn HTML. web.dev. web.dev
  7. Khan Academy. (n.d.). Intro to HTML/CSS: Making webpages [Online course]. Khan Academy. khanacademy.org β†—
Key terms
Capstone project
A culminating exercise that combines the skills from an entire course.
max-width
A CSS property capping how wide an element can grow, useful for responsive cards.
border-radius
A CSS property that rounds the corners of an element's box.
cursor: pointer
A style that shows the hand cursor, signaling an element is clickable.
Integration
Combining HTML, CSS, and JavaScript so structure, style, and behavior work together.
Static site
A site made of files served unchanged, with no server-side program, and the easiest kind to publish.

Open the interactive version with quizzes and progress →