Learning objectives
- Name the parts of a minimal HTML document and say what each one is for
- Predict the tree the parser builds when
html,headandbodytags are left out, including where the head ends - Explain what the doctype still does, and tell the three rendering modes apart by what you can see and by
document.compatMode - Explain how a browser decides a page's character encoding, and diagnose mojibake from the characters it produces
- Explain why the MIME type the server sends, not the file's extension, decides how the markup is parsed
- Set
langanddiron the root element, and know what depends on them
The skeleton
Lesson 01 showed that a browser will build a page from a file with no tags at all. That tells you what the parser tolerates, not what you should write. Here is the document this course asks you to start from:
The slides for this course label the parts as a stack: the doctype, which "traditionally indicates the version of HTML used"; the root element; the head, which holds information about the document, where you "always set the character set first" and where the "document title is mandatory"; and the body, which holds the content.
html, split into a head that configures and a body that renders.The doctype is not an element. It is a preamble that the parser reads before anything else, and the tree records it as a separate node, document.doctype, outside the root. Its one remaining job is covered below.
The html element is the root: every other element in the document is its descendant. It does little work itself, but its lang attribute states the language of everything inside, and it is the only place to state the language of the whole document.
The head is the document's metadata: its title, its encoding, its style sheets and scripts, its relationships to other documents. Nothing in it renders as content. Search engines, social previews, browser tabs, bookmarks and screen readers announcing a newly loaded page all read it. <title> is the head's only required child. Lesson 09 covers the head element by element.
The body holds the content, which is the subject of most of this series. A document has exactly one head and one body, always, whether or not you typed them, which is the next section's point.
Tags you may leave out
Lesson 01's five-line example produced a full html/head/body tree from a file that typed none of them. That was not error recovery. Leaving out these tags is valid HTML. The specification lists, for each one, exactly when its start or end tag may be omitted, and its first sentence on the subject is the important one: omitting a tag "does not mean the element is not present; it is implied, but it is still there."
| Tag | May be left out when |
|---|---|
<html> | the first thing inside the html element is not a comment |
</html> | the element is not immediately followed by a comment |
<head> | the head is empty, or the first thing inside it is an element |
</head> | the head is not immediately followed by whitespace or a comment |
<body> | the body is empty, or the first thing inside it is not whitespace or a comment, and is not a meta, noscript, link, script, style or template element |
</body> | the body is not immediately followed by a comment |
The conditions look fussy, and all of them say the same thing: you may leave a tag out wherever the parser would put the element back in the same place. The exceptions are the cases where leaving it out would move something. Omit <html> before a comment and the comment ends up outside the root. Omit <body> before a <script> and the script lands in the head instead, because a script is allowed there.
Where the head ends
The parser decides where the head ends by watching for the first thing that cannot go in a head. Elements such as meta, title, link, style, script and base keep the head open. Whitespace and comments keep it open too. The first non-whitespace text, or the first element that belongs in a body, closes the head, opens an implied body, and is placed there. Here is a head with one stray word in it:
The word closed the head, so the link and the description ended up in the body. The style sheet still applies, since browsers honour a style sheet link wherever it appears, but the description is now a meta element in the body, where it is invalid and where a crawler looking in the head will not find it. An unknown element, such as a misspelled <meat> or a custom element, has the same effect as the stray word. The source looks like a head; the tree says otherwise. As lesson 01 put it, the tree is what everything reads.
The demo below takes the rule to its limit. Its source has a doctype and an <html lang="en"> start tag, then no head or body tags at all, and a script at the end prints the tree the parser built from it:
So should you write the tags? Some style guides, Google's among them, recommend leaving optional tags out to save bytes. This course writes them. The <html> start tag is the only place to put lang, so in practice it is never optional, and writing <head> and <body> makes the boundary visible, so a stray word in the head is easy to spot. Compression removes most of the saving anyway.
The doctype
The modern doctype is fifteen characters, <!doctype html>, case-insensitive. To see why it exists at all, go back to where HTML came from.
From DTDs to fifteen characters
Lesson 01 described SGML, a language for defining languages, and the Document Type Definition, the DTD, that declares a language's elements and how they may nest. An SGML document announces which DTD it follows in its first line, the document type declaration, which is where the word "doctype" comes from. In the slides' terms, the doctype identifies "the markup dialect used in a document by referencing an external DTD." HTML 4.01, the last SGML version of HTML, came in three dialects, and each had its own doctype:
Each has two identifiers. The quoted string after PUBLIC is a formal public identifier: a name, in a fixed format, that says who owns the DTD (W3C), what it is (a DTD), which one (HTML 4.01 Transitional), and its language (EN, the language of the DTD itself, not of your page). The second string is a system identifier, a URL where the DTD file can be fetched. Strict left out the presentational elements and attributes such as <font> and <center>; Transitional kept them for pages moving to CSS; Frameset was for pages built from frames. XHTML 1.0, in 2000, reformulated the same three as XML, with the same three doctypes in XML form:
Here is the surprise: no browser has ever read those DTDs. Browsers were never validating SGML parsers and never fetched the URL. The DTD was for validators, which used it to check a document against its declared dialect.
The doctype switch
By the late 1990s millions of pages relied on the bugs of Netscape 4 and Internet Explorer 5, so a browser that fixed the bugs to follow the CSS specification would break them. Internet Explorer 5 for the Macintosh, released in 2000, found a way out. Old pages rarely had a doctype; pages written by authors who knew the standards usually did. So the browser looked at the doctype and chose: a known, modern doctype got the standards-following behavior, and a missing or old one got the old, bug-compatible behavior, which came to be called quirks mode. Mozilla adopted the idea, Internet Explorer 6 for Windows followed in 2001, and every browser has done it since. The slides call this "the doctype switch."
That switch is the doctype's only job in a browser today. HTML5 dropped the DTD, because HTML is no longer defined in SGML, and kept the doctype only because a page without one is rendered in quirks mode. The specification says so directly: "DOCTYPEs are required for legacy reasons." <!doctype html> has no version and no URL; it is short, easy to remember, and was already enough to switch every browser of the time into standards mode. There is no version number to update, because there is only one HTML. For the rare tool that cannot write the short form, the specification allows <!DOCTYPE html SYSTEM "about:legacy-compat">, which means the same thing.
Three rendering modes
The specification now defines the switch exactly, as the first thing the parser does. There are three modes.
- No-quirks mode (once called "standards mode"): HTML and CSS behave as their specifications say.
<!doctype html>selects it, and so do the HTML 4.01 Strict and XHTML 1.0 Strict doctypes. - Limited-quirks mode (once called "almost standards mode"): no-quirks mode with one difference, how the height of a line is calculated around images, which keeps old image-sliced layouts from opening gaps. The XHTML 1.0 Transitional and Frameset doctypes select it, as do the HTML 4.01 Transitional and Frameset doctypes when they include the DTD's URL.
- Quirks mode: a list of deliberate bugs, emulating the browsers of the late 1990s. A missing doctype selects it, and so does any of about sixty old doctypes on the specification's list, including HTML 4.01 Transitional without its URL, and text or an element before the doctype.
document.compatMode reports; it cannot tell limited-quirks from no-quirks.Two details are worth knowing. A comment before the doctype is harmless; text before it is not. A file that begins with a stray word, such as a debugging message printed by a server script before the template, is in quirks mode, because the doctype was not the first thing the parser saw. And you can always ask a page which mode it is in: document.compatMode returns "BackCompat" in quirks mode and "CSS1Compat" otherwise. Type it into the console of any page. Firefox also prints a console warning when a page is in quirks or limited-quirks mode.
What quirks mode changes
The quirks are written down, in the WHATWG's Quirks Mode Standard and the HTML specification, so that every browser emulates the same old bugs in the same way. The demo below loads two files that are identical except for line 1, which is <!doctype html> in one and missing from the other. Each frame prints its own document.compatMode.
Five differences show, each one a quirk you could meet in old code:
- Tables forget the font. In quirks mode a table does not inherit the font size, weight or style of its surroundings; the browser's style sheet resets them at every
<table>. Old sites therefore repeated<font>tags inside every table cell. In no-quirks mode the table inherits like any other element. - CSS mistakes are forgiven.
width: 200has no unit andbackground-color: fd8has no#. Both are invalid, and no-quirks mode drops them. Quirks mode reads them as pixels and hexadecimal, but only for a fixed list of older properties:widthgets the help,inline-sizedoes not. - Percentage heights work. In no-quirks mode,
height: 12%of a parent whose height comes from its content means nothing, so it is ignored. Quirks mode keeps looking up the tree for something with a height and ends at the window; it also stretchesbodyto fill the window. Hence the old advice to sethtml, body { height: 100% }first. - Inputs use the other box model. In quirks mode, text inputs and text areas measure their width from border edge to border edge, as if they had
box-sizing: border-box; in no-quirks mode they measure the content alone and the padding is added on. The demo measures both. - No gap under images. An image sits on a text baseline, so in no-quirks mode there is room under it for letters' descenders, a gap of a few pixels inside the box. Quirks and limited-quirks modes drop the gap. That is the one quirk limited-quirks mode keeps.
The fourth item corrects a claim you will still find in older tutorials: that quirks mode switches the whole page to "the IE box model," where a div's width includes its padding and border. Internet Explorer 5 did size every box that way, and Internet Explorer's own quirks mode kept doing so for years. Current browsers keep that behavior only for text inputs, text areas and the heights of table cells; a div or a paragraph is sized the same in all three modes. In the demo the paragraph is the same width in both frames, and only the input changes. Today you choose the box model yourself with box-sizing, as CSS layout explains, and it works the same in every mode.
None of these is useful in new work. Quirks mode exists so that pages from 1998 keep working, and it applies to any page that forgets its doctype.
Character encoding
Lesson 01 described the parser as a pipeline that starts with bytes, and the first stage is the one most authors never think about: turning bytes into characters. A file on disk or a response on the network is a sequence of bytes, numbers from 0 to 255. Text is a sequence of characters. A character encoding is the rule that maps one to the other, and the same bytes mean different text under different rules. The parsing module of How Browsers Work calls this "the first place a page can be silently wrong before a single tag has been recognized."
One encoding: UTF-8
ASCII, from the 1960s, defines 128 characters: enough for English, not for "café". Dozens of regional encodings extended it, such as windows-1252 for Western Europe, windows-1251 for Cyrillic and Shift_JIS for Japanese, each giving the same byte values different meanings, so a file made sense only if you knew which one it used.
Unicode replaced the patchwork with one numbered list of characters, over 150,000 of them, covering every modern script and emoji. UTF-8, designed by Ken Thompson and Rob Pike in 1992, is the way to write those numbers as bytes. It uses one byte for each ASCII character, the same byte ASCII uses, so plain English text is identical in both, and two to four bytes for everything else: é is C3 A9, € is E2 82 AC, and the grinning-face emoji is F0 9F 98 80. According to W3Techs, in October 2026, 99.1% of websites whose encoding it knows use UTF-8. The HTML specification goes further: a document "must" be encoded in UTF-8, and authoring tools must default to it. There is no decision to make. Save your files as UTF-8, and say so.
Declaring it
Saying so is the job of <meta charset="utf-8">. The declaration has a chicken-and-egg problem: it is written in the bytes it describes. HTML gets away with it because the declaration is pure ASCII, which reads the same in nearly every encoding, and because browsers look for it early. Before parsing starts, the browser prescans the start of the file for a meta charset. The specification requires the declaration to be complete within the first 1024 bytes, and that is why the slides say to "always set the character set first": make it the first element in the head, before the title, which may itself contain non-ASCII characters. A declaration found later may cause the browser to throw away what it has parsed and start again with the new encoding, or it may be missed entirely; testing in Chrome for this lesson, one after a long comment in the head was honoured, and one after some body text was ignored.
The browser takes the first answer it finds, in this order:
- A byte order mark. A file may begin with the bytes
EF BB BF, an invisible marker that means "UTF-8". If it is there, it wins over everything else, including ametathat says otherwise. - The HTTP header.
Content-Type: text/html; charset=utf-8on the response. Many servers send it; many, including the one serving this course's demos, sendtext/htmlwith no charset, which leaves the decision to the page. - The
metadeclaration, found by the prescan. - A guess. A frame may inherit its parent page's encoding; the browser may remember the page from an earlier visit, or analyse the bytes. Failing all that, it uses a default that depends on the browser's locale. For English, and most other locales, the default is windows-1252.
The last step matters. A page with no declaration, served without a charset, is decoded as windows-1252 on an English-language computer and as something else on a Japanese or Russian one. The specification requires a declaration even for a page that is entirely ASCII, because the encoding also governs what happens to text people type into its forms and to URLs a script builds from it (the URLs section covers percent-encoding).
Mojibake
Get the declaration wrong and you get mojibake, from the Japanese for "character transformation": text decoded with the wrong rule. It has a recognizable look, and once you can read it you can diagnose it. Take "café" saved as UTF-8 and decoded as windows-1252, which is a single-byte encoding. Every byte becomes one character, so the two bytes of é become two characters:
C3 A9 as one character. windows-1252 reads it as two.The pattern generalizes. UTF-8 read as windows-1252 turns each accented Latin letter into two characters, usually starting with Ã. Curly quotes, apostrophes and dashes are three bytes in UTF-8 and come out as three characters starting with â€: "it’s" becomes "it’s", the most common mojibake on the web. Japanese and emoji turn into longer runs of accented letters and symbols. The opposite mistake, a windows-1252 file read as UTF-8, looks different: its accented bytes are not valid UTF-8, so each one becomes the replacement character, �, and "café" becomes "caf�". Seeing à and †means UTF-8 was read as something else; seeing � means something else was read as UTF-8.
The demo's bytes are UTF-8, written deliberately by a script, and its meta declares windows-1252. The explanations on it are ASCII, so they survive; the samples do not.
Mojibake often comes from outside the page, wherever text crosses a boundary that assumes another encoding: a database connection, a spreadsheet export, a copy and paste. The fix is the same everywhere: UTF-8 at every step, declared wherever it can be.
The MIME type
Before the parser can decode anything, the browser has to know which parser to use, and it does not decide by looking at the file name. It reads the Content-Type header of the HTTP response, which carries a MIME type (also called a media type): text/html for an HTML page, text/css, image/png, application/json, and so on. The server chooses the header, usually from a table that maps extensions to types; the extension matters only because the server's configuration says so.
The slides for this course call this "not a detail" and show why. An HTML file served as text/plain is displayed as text, tags and all, and that is what current browsers do. Old versions of Internet Explorer instead sniffed the content, decided it looked like HTML, and rendered it. The slides name this "Thomas' Law of Unintended Consequences": people don't know MIME, and trouble follows when software "fixes" things for them. Sniffing also turned out to be a security problem, since an uploaded "image" that a browser decided was HTML could run script, and today servers can forbid it with X-Content-Type-Options: nosniff.
Same markup, two parsers
The MIME type matters most for one pair. Served as text/html, a document goes to the HTML parser, the forgiving one lesson 01 described. Served as application/xhtml+xml, the same bytes go to an XML parser, which is strict: the first well-formedness error stops it. The file below has one unclosed <em> and one self-closed <div/>:
As application/xhtml+xml, Chrome shows a pink box reading "This page contains the following errors: … Opening and ending tag mismatch: em line 5 and p", followed by "a rendering of the page up to the first error", which is the word "One". As text/html, the page renders in full. The HTML parser closes the em at the end of the paragraph and then reopens it, so the rest of the page is in italics. It ignores the slash in <div/>, so the div stays open and swallows the paragraph after it. Neither result is what the author meant, but only one of them tells you.
Other differences follow from the parser. XML is case-sensitive, so in XHTML <P> is not a paragraph but an unknown element; in HTML it is a paragraph. XHTML documents are always rendered in no-quirks mode, doctype or not. And in XHTML, <div/> really is an empty div.
This is why lesson 01 said that most XHTML on the web never was XHTML. Authors wrote XML-style markup and served it as text/html, partly because Internet Explorer before version 9 could not display application/xhtml+xml at all, so the HTML parser read every page and checked nothing. As the slides put it, "HTML5 doesn't mean XHTML is dead": its strictness is available to anyone who serves it with the right type. The doctype never decided which language a page was in; the MIME type did. For this course, everything is text/html, and the trailing slashes are optional decoration that the parser throws away.
Language and direction
The lang attribute on the root element states the primary language of the document, as a language tag: en, en-GB, es, ja, zh-Hant. It is one of the most useful attributes in HTML and one of the most often forgotten, because a page without it looks exactly the same to its author. What depends on it:
- Pronunciation. Screen readers choose a voice and pronunciation rules from it. Without it they fall back to the language of the user's system, so an English reader's screen reader reads a Spanish page with English rules. This is why WCAG makes a programmatically determined page language a Level A requirement (success criterion 3.1.1), the lowest bar there is.
- Hyphenation and line breaking. CSS
hyphens: autoonly hyphenates text whose language the browser knows; on a page with nolang, or an unknown one, nothing is hyphenated. It also decides the glyphs used for Chinese and Japanese characters, which share code points but differ in form. - Translation. Translation tools use it to know what to translate from.
- Spelling and input. The language of an editable field tells the browser which language the user is expected to type, which spell checkers can use.
- Styling. The
:lang()selector lets CSS choose per language, for example the quotation marksqelements use: “ ” in English, « » in French. - Not search, at least not Google. Google's documentation says it determines a page's language from the visible text and does not use code-level language information "such as
langattributes". Writelangfor people and their tools, not for ranking.
Set it on html and it covers everything, including the title, which is announced when the page loads. Set it again on any element whose content is in a different language, such as a quotation or a name: <span lang="fr">. Use the right tag, not a guess; a page in Spanish marked lang="en" is worse than one with no lang, because it is confidently wrong. The film The Web Speaks Every Language. Does Your Page? shows the cost of getting language wrong.
The dir attribute sets the base direction of the text: ltr, the default, or rtl, for Arabic, Hebrew, Persian, Urdu and other right-to-left scripts. Put dir="rtl" on html for a right-to-left page and the whole layout mirrors, including CSS logical properties such as margin-inline-start, which is one reason this course writes them instead of margin-left. For text whose direction you do not know in advance, such as a user's name, dir="auto" lets the browser decide from the first strong character. Direction is not implied by lang; an Arabic page needs both.
The viewport, in outline
The last line of the skeleton is for phones:
Without it, mobile browsers assume the page was designed for a desktop screen, lay it out at a width of about 980 CSS pixels, and shrink the result to fit, which gives tiny text that people have to zoom. With it, the page is laid out at the width of the device. Never add user-scalable=no, which stops people from zooming. Lesson 09 covers the viewport in detail.
A minimal correct document
Here is the skeleton again, with each line's reason, as a template to start every page from:
Three things go with it that are not in the file: save it as UTF-8, serve it as text/html (with ; charset=utf-8 if you control the server), and check it in the Nu HTML Checker. Style sheets, scripts, descriptions and icons are added to this skeleton, not instead of it.
Try it: read the first lines
Four exercises, each about ten minutes:
- Open render-modes.html. For each of the five tests, write one sentence saying what differs and why. Then open any page of a site you use and run
document.compatModein the console. If you find one that saysBackCompat, View Source and find out why. - Save a small page containing "café", "it’s" and an emoji as UTF-8 with
<meta charset="windows-1252">, open it, and predict each piece of mojibake before you look. Then change the declaration toutf-8. Then remove it entirely and open the file again; note whatdocument.characterSetreports, and whether the browser guessed right. - Write a document with no
<head>or<body>tags, with atitle, then a word of text, then a<meta name="description">. Predict the tree, then check it in the Elements panel. Move the word below themetaand check again. - Pick three sites in three languages. In the console, check
document.documentElement.langanddocument.diron each. Note any that are missing or wrong, and what a screen reader would do with them.
Summary
| Concept | Key points |
|---|---|
| The skeleton | Doctype, html lang, head with meta charset first, the viewport and a title, then body. The title is the head's only required child. |
| Optional tags | html, head and body tags may be omitted where the parser would put the element back in the same place; the elements are always in the tree. Keep <html> for lang. |
| Where the head ends | At the first non-whitespace text or body element. Anything after it lands in the body, even meta and link. |
| The doctype | Once named an SGML DTD that browsers never read. Its only job now is choosing the rendering mode. Write <!doctype html>, first. |
| Rendering modes | No-quirks (<!doctype html>, Strict doctypes), limited-quirks (Transitional with a URL, XHTML Transitional: only the line-height quirk), quirks (no doctype, old doctypes, text before the doctype). document.compatMode: BackCompat or CSS1Compat. |
| Quirks | Tables reset fonts; unitless lengths and hashless colours accepted; percentage heights resolve against the window; inputs use border-box; no gap under images. Not "the IE box model" for every element. |
| Encoding | Bytes become characters by a rule. Use UTF-8. Order: byte order mark, HTTP charset, meta in the first 1024 bytes, then a guess (windows-1252 for English). |
| Mojibake | é and ’: UTF-8 read as windows-1252. �: something else read as UTF-8. The declaration labels bytes; it does not convert them. |
| MIME type | The server's Content-Type picks the parser, not the extension. text/html: forgiving HTML parser. application/xhtml+xml: XML parser, stops at the first error. |
lang and dir | lang drives pronunciation, hyphenation, translation, spell checking and :lang(); WCAG Level A. Google ignores it. dir="rtl" mirrors the layout; dir="auto" for unknown text. |
| Viewport | width=device-width, initial-scale=1; never disable zoom. |
Next steps
The document is now in place: the right mode, the right encoding, the right parser and a language. Lesson 03, Elements, attributes and entities, looks inside it at the grammar of the markup itself: how tags, attributes and character references are written, which elements are void or hold raw text, what the parser does with whitespace, comments and names it does not know, and the global attributes, lang and dir among them, that every element accepts.
