1. Structure
  2. HTML
  3. Elements, attributes and entities

Lesson 03 · Principles

Elements, attributes and entities

Every HTML document is built from three things: elements, the attributes that configure them, and text, some of it written as character references. The grammar for all three fits on a page, and every browser applies it the same way. This lesson reads that grammar closely, including the corners where the obvious reading is wrong: a slash that closes nothing, a "false" that means true, and an attribute nobody defined that the browser keeps anyway.

Learning objectives

  • Name the parts of an element and tell a tag from an element
  • Predict what the parser does with case, crossed tags, whitespace, and names it does not know
  • Tell the six kinds of element apart and say how the parser reads the content of each
  • Write attributes correctly, and explain how boolean and enumerated attributes are read
  • Choose among the global attributes, and know which ones to avoid and why
  • Escape the characters that must be escaped, and write comments that are safe to ship

Syntax as a contract

Lesson 01 described the bargain at the heart of HTML: authors are asked to write conforming markup, and browsers are required to accept anything. This lesson is about the author's half of that contract, the surface grammar. It is small, and worth learning exactly, because nearly every bug in this lesson is a place where the author and the parser read the same characters differently.

A document is made of elements, character references and runs of plain text. One line holds all three:

<p class="legal"><strong>This is important</strong> &copy; 2026</p>
The anatomy of an elementThe markup a href="/about" class="nav", then About us, then the end tag a. Labels point to the element name, an attribute name, an attribute value, a second attribute, the content and the end tag. A bracket below the start tag labels it, and a bracket above the whole line says that the start tag, content and end tag together are the element.the element: start tag + content + end tag<a href="/about" class="nav">About us</a>nameattribute nameattribute valuea second attributestart tagcontentend tag
Tags are what you type. The element is what they delimit, and it is the element that ends up in the tree.

A tag is the bracketed text: a start tag such as <a href="/about">, or an end tag such as </a>. An element is the unit the tags delimit, and the thing the parser puts in the tree. Some elements have no end tag and some end tags may be left out, so the two words are not interchangeable. An attribute is a name, usually with a value, written in the start tag. The content is whatever sits between the tags, and the browser applies the element's meaning to it; here, the content is a link.

Case, nesting and whitespace

Case

Element and attribute names are case-insensitive. <P ID="intro"> and <p id="intro"> produce the same element, because the parser lowercases names as it reads them. Write them in lowercase anyway: XHTML required it, every style guide expects it, and it is what DevTools shows, so source and tree read the same.

Attribute values are another matter, and the slides' advice holds: treat them as case-sensitive. Some are (an id, a class, a URL path), some are not (most keywords, such as type="EMAIL"), and remembering which is which buys nothing. CSS treats .Card and .card as two classes; a server may treat /About and /about as two pages.

Nesting

An element opened inside another must close inside it, so tags pair up like brackets in an expression:

<em><strong>nested</strong></em> <!-- correct --> <em><strong>crossed</em></strong> <!-- the tags overlap -->

A tree cannot represent overlap, so the parser has to choose. Here it closes the strong at </em> and drops the stray </strong>, which happens to be what the author meant. Other crossings end worse: when formatting elements cross block elements, the recovery rules can split one element into several and copy it into the next block. Lesson 01's malformed.html shows several such repairs next to the tree. The lesson here is simpler: close what you open, in reverse order, and the tree will be the one you wrote.

Whitespace

HTML is whitespace-insensitive beyond a single space, except when it is not. A run of spaces, tabs and newlines in ordinary text is shown as one space. The parser does not do this collapsing; the tree keeps every space, as text nodes. CSS does it, through the white-space property, whose default collapses runs when text is laid out. That is why indentation appears in the tree, as lesson 01 showed, and changes nothing on screen. Three places keep whitespace as written:

  • pre, for preformatted text such as code, styled white-space: pre by default; lesson 06 covers it.
  • textarea, whose content is its default value, kept exactly.
  • Any element you style that way, with white-space: pre, pre-wrap or pre-line.

The parser makes one adjustment of its own: a single newline right after the start tag of a pre or textarea is dropped, so the content can start on its own line. The slides also flag the opposite habit, forcing space with &nbsp;. One between "10" and "km", to keep them on the same line, is what it is for. A row of them to indent, or a paragraph holding nothing but one to make a gap, is a markup smell; spacing is a job for CSS.

Unknown elements and attributes

It is a common belief that the browser throws away names it does not know. It does not. The parser keeps every element and attribute it reads, and the details are worth getting exactly right.

An unknown element stays in the tree. <foo>bar</foo> becomes an element named foo, and "bar" is shown like any other text. Its DOM object is an HTMLUnknownElement, a plain element with no behavior, rendered inline like a span, and you can style and script it. Some elements removed from the language, such as <blink>, end up the same way.

A name with a hyphen is different. <my-widget> is a valid custom element name, so the parser creates an ordinary HTMLElement. It waits there until a script defines my-widget, and is then upgraded to the class the script registered. No standard element will ever have a hyphen in its name, so yours cannot collide with the language; the slides' "if you must do it, use a dash" is now a rule. Lesson 10 and the Components section build on it.

An unknown attribute is kept and ignored. getAttribute() returns it, CSS attribute selectors match it, and DevTools shows it. It gets no DOM property and no behavior.

<p id="demo" wat meme="true">Prof Powell demo</p> <!-- demo.getAttribute('meme') → "true" demo.meme → undefined document.querySelectorAll('[wat]').length → 1 -->

So invented attributes "work", in that scripts and styles can find them. They are still invalid, and a name you invent today may be standardized tomorrow with a meaning of its own. The language reserves a set of names for exactly this need: write data-meme="true", not meme="true".

The six kinds of element

The specification sorts elements into six kinds by syntax, not meaning: what each may contain, whether it has an end tag, and how the parser reads the characters inside it. The slides draw it as a map, and it is worth learning in that form.

The six kinds of element, by how their content is parsedSix boxes in two rows of three. Void elements: area, base, br, col, embed, hr, img, input, link, meta, source, track, wbr; no content and no end tag. Raw text elements: script and style; content is text, and nothing in it is decoded. Escapable raw text elements: textarea and title; content is text, character references are decoded. The template element: content is parsed into a separate document fragment and stays inert. Foreign elements: SVG and MathML; a trailing slash closes the element. Normal elements: all the others; text, character references, elements and comments.voidarea base br col embedhr img input link metasource track wbrno content, no end tagraw textscript styletext up to the end tag;no tags, no referencesescapable raw texttextarea titletext up to the end tag;references are decodedtemplatetemplateparsed into a fragment,kept inert until clonedforeignsvg math, and childrena trailing slashcloses the elementnormalp ul li a em section …text, references,elements and comments
Six kinds of element, sorted by how the parser reads what is inside them. Most of what you write is normal; the other five are where syntax surprises live.

Void elements

A void element can never have content, so it has no end tag. There are thirteen: area, base, br, col, embed, hr, img, input, link, meta, source, track and wbr. (The slides also list param, which has since been removed from the standard along with the plugins it configured.) An end tag on a void element is ignored, with one odd exception kept for old pages: a stray </br> becomes a <br>.

A void element's start tag may end with a slash, as in <br/>. The slash is allowed and ignored. On any other HTML element it is not allowed, and it still does nothing: <div/> opens a div that stays open, and what follows lands inside it. The slides once recommended the XML style "for a potential strictness upgrade". That upgrade never came, so it is now a matter of taste; pick one style per project and know that the slash closes nothing.

Raw text elements

script and style hold another language, and the HTML parser keeps out of it. It reads plain text until the matching end tag; nothing in between is a tag or a character reference. That is why if (a < b) works in a script, and &amp; stays five characters long. The one thing raw text cannot contain is its own end tag: a string in a script that contains </script> ends the script mid-string. That is a real injection risk when a server writes data into an inline script; the usual guard is to write <\/script>, which JavaScript reads the same way.

Escapable raw text elements

textarea and title are read almost the same way: a <b> inside a textarea is three characters of text. The difference is that character references are decoded, so &amp; in a title shows as & in the tab. Hence the slides' tip that a textarea is a quick way to show a code listing, though a pre is better for code that is only to be read.

The template element

template is a kind of its own. Its content is parsed as HTML, but the nodes go into a separate document fragment, its content property, where they stay inert: nothing renders, images are not fetched, scripts do not run. A script clones the fragment when it needs a copy. The slides call this the way "to keep HTML strings out of JavaScript": markup for a repeated view lives in the page as markup, checked by the parser and the validator.

Foreign elements

SVG and MathML can be written straight into HTML. Inside <svg> or <math> the parser follows rules for foreign content, and two things change. A trailing slash means what it means in XML, so <circle r="5"/> is complete and the next shape is its sibling. And case is restored where SVG needs it: type viewbox and the tree holds viewBox. Foreign content also ends sooner than you might expect: an HTML element such as span inside an svg closes the svg on the spot. HTML inside SVG belongs in a foreignObject.

The slides say "foreign elements are XML, so be strict". Be strict, but for a slightly different reason: inside HTML they are still read by the forgiving HTML parser. The same SVG saved as a .svg file is XML, though, and there one missing quote breaks the image.

Normal elements

Everything else, about ninety elements, is normal: text, character references, other elements and comments, with an end tag that is sometimes optional. Which elements may go inside which is the subject of lesson 04. The demo below shows each kind as markup and as the tree. The trees are not drawn by hand: a script parses each string with DOMParser and prints the nodes. The last panel parses whatever you type.

Attributes

Syntax

An attribute is a name, optionally followed by = and a value, which can take three forms:

<p title="double quoted"> <!-- the course style --> <p title='single quoted'> <p title=unquoted> <!-- one word, no spaces or quotes --> <input required> <!-- no value: a boolean attribute -->

An unquoted value may not be empty or contain whitespace or any of " ' = < > `. Write title=Hello world and the element gets the title "Hello" plus a nonsense attribute named world, which is reason enough to quote every value. The slides' convention is double quotes in markup and CSS and single quotes in JavaScript, so markup built in a script string rarely needs escaping. An attribute may appear once per tag; repeat one and the parser keeps the first.

Every attribute has two faces. The content attribute is the text in the markup, which getAttribute() returns. The DOM property is what a script reads as element.disabled, derived from the content attribute by rules that depend on its type. For the two types below they often disagree, which explains some of the most persistent bugs in HTML. The DOM lesson on attributes takes the scripting side further.

Boolean attributes

A boolean attribute is true when present and false when absent. Its value is never read, so all of these disable the button:

<button disabled> <button disabled=""> <button disabled="disabled"> <button disabled="true"> <button disabled="false"> <!-- still disabled -->

The last line is the trap. The only way to say false is to leave the attribute out, or, from a script, to set button.disabled = false, which removes it. The specification allows only two values, the empty string and the attribute's own name, so a validator flags disabled="false", but the browser just obeys it. The slides show the history: HTML allowed the bare form, <hr noshade>, XHTML required noshade="noshade", and both are valid today. Others you will meet often: required, checked, hidden, inert, open, defer.

Enumerated attributes

An enumerated attribute takes one keyword from a fixed set, in any case: type="email", dir="rtl". It is HTML's version of a union type, and the interesting part is what happens with a value that is not a keyword. The specification sets two defaults per attribute: a missing value default for when it is absent, and an invalid value default for when its value is not a keyword. A misspelled type="emial" falls to the invalid default, text: the field works, with no email keyboard and no email check, and nothing warns you. getAttribute('type') still says "emial"; the type property says "text". Some attributes simply ignore a bad value: dir="sideways" gives a dir property of "". The validator catches every such misspelling, and the browser hides every one.

How the browser reads a boolean and an enumerated attributeTwo flows. Boolean: is the attribute present? Yes gives true, whatever the value, including the string false; no gives false. Enumerated: is the attribute present? No gives the missing value default. Yes leads to a second question, is the value one of the keywords, ignoring case? Yes gives that keyword's state; no gives the invalid value default, for example type emial becomes text.booleanattribute present?yestrue, whatever the valuedisabled="false" → truenofalseenumeratedattribute present?yesone of the keywords?(any case)yesthat keyword's statenoinvalid default: "emial" → textnomissing value default
A boolean attribute asks one question; an enumerated attribute asks two, and each "no" has its own default. None of the defaults produces a warning.

The demo puts both kinds next to the DOM's view of them; each line updates as you change things.

Attribute types

Boolean and enumerated are two of about a dozen value types, and together they form a small type system: each attribute's type fixes the grammar of its value, how the browser parses it, and what a validator checks.

TypeExamplesThe value is
Booleandisabled, checkedPresence only; any value is ignored
Enumeratedtype, dirOne keyword from a fixed set, any case
Numbertabindex, colspanAn integer or decimal in a defined syntax
Texttitle, altAny text, for people to read
URLhref, srcA URL, resolved against the document's base
Date and timedatetimeA date or time in a fixed format
ID referencefor, popovertargetThe id of another element
ID reference listaria-labelledbySeveral ids, separated by spaces
Token setclass, relWords separated by spaces
Comma listsrcset, sizesItems separated by commas
Language taglangA BCP 47 tag such as pt-BR

Two rows deserve a second look. URL attributes are where HTML meets the network, which is why the URLs and Linking sections exist. ID references are where markup quietly breaks: a label whose for names a misspelled id labels nothing, and the page looks fine.

The global attributes

A few dozen attributes are global, allowed on every element. HTML 4 had the "core" four, id, class, style and title, plus lang, dir and the event handlers. The list has grown, and the newer entries do real work.

Naming: id and class

id names an element uniquely in the document, and uniqueness is a rule: with two elements sharing one, getElementById(), a label and a fragment link all find the first, and the second silently loses. An id has three jobs. It is a fragment target: #summary scrolls to id="summary", which is why every h2 here has one. It is the target of ID references such as for. And it is a script hook. Choose names, as the slides suggest, as if they will appear in a URL, CSS and JavaScript: lowercase words joined by hyphens.

It can also be a CSS hook, and this course avoids that. An id selector outranks any number of classes, so a rule written with one is hard to override; the Selectors lesson explains why. class holds a space-separated list of names and is the general-purpose hook for CSS. The slides warn against "class-itis", classes doing the work of elements; lesson 05 takes that up. In short: the element says what a thing is, classes name variations, attributes carry state.

Advisory text: title

title holds advisory text that desktop browsers show as a tooltip when a pointer rests on the element. That is the problem: touch users never see it, keyboard users usually do not, and screen readers announce it inconsistently. The specification itself tells authors not to rely on it. Use it for a genuine extra, such as expanding an abbr, never as the only label of a control or the only home of a piece of information.

Language: lang, dir and translate

lang names the language of an element's content with a tag such as en or fr-CA. Lesson 02 put it on the root; on any other element it marks a passage in another language. Screen readers switch pronunciation by it, browsers choose fonts, hyphenation and quotation marks by it, and CSS selects it with :lang(). dir sets text direction: ltr, rtl, or auto, which decides from the text itself and suits content you did not write, such as a user's name. Direction affects layout too, which is why this course writes CSS with logical properties such as margin-inline-start that flip with the text.

translate="no" asks translation tools, including the browser's own, to leave an element's text alone: brand names, code, anything that would be wrong in another language. The film The Web Speaks Every Language follows these attributes through a real page load.

Presence: hidden and inert

hidden says an element is not relevant right now; the browser does not render it and assistive technology does not report it. It is fully supported, whatever the old slide said. Two cautions: it acts like a boolean, so any value other than until-found, even hidden="false", hides; and browsers implement it with a display: none style rule, so your own CSS that sets display: flex on the element shows it again.

It has one keyword value, hidden="until-found". The content is hidden, but find in page and fragment links can still reach it; when they do, the browser fires a beforematch event, removes the attribute and scrolls to the match. It suits collapsed sections, such as an FAQ, that should stay findable. Chrome has supported it since 2022 and Firefox since 2025; Safari 26.2 added partial support, so check find in page there. Where it is missing, the content simply stays hidden, so treat it as an enhancement.

inert leaves an element visible but untouchable: no clicks, no focus, no find in page, and assistive technology skips it. Use it for content behind a custom overlay. A modal dialog makes the rest of the page inert by itself; lesson 08 covers that.

Interaction: tabindex, contenteditable and friends

tabindex controls keyboard focus. Links, buttons and form controls are focusable already, and most other elements should not be. tabindex="0" makes an element focusable in its normal place, which a custom widget needs; tabindex="-1" makes it focusable by script but skipped by Tab, which suits an error summary that a script moves focus to. Positive values pull elements ahead of the whole page; never use them, for the reasons Usable Forms gives.

contenteditable makes content editable in place. It is enumerated: true (or empty), false, or plaintext-only, which strips formatting from pastes; any other value inherits from the parent. As the slides say, it "does not determine how the change is retained": saving is up to your script.

Three smaller ones. spellcheck turns spelling checks on or off for editable content; turn them off for codes and usernames. autocapitalize tells a virtual keyboard when to capitalize; Forms uses both in Capitals, corrections and spelling. draggable decides whether the drag-and-drop API can drag an element; links and images can by default. It is enumerated, not boolean, so a bare draggable with no value does nothing.

Three to avoid: accesskey, style and event handlers

accesskey assigns a keyboard shortcut. The slides list its "open questions", and they were never answered: the key combination differs by browser and system, the letters collide with browser and screen reader shortcuts, nothing shows that the shortcut exists, and a letter chosen for English means nothing in translation. Usable Forms takes the same position.

style writes CSS on one element. It beats almost every stylesheet rule, cannot hold media queries or :hover, and puts presentation back in the markup. The one use that holds up is passing a value to CSS, such as style="--progress: 40%".

Event handler attributes such as onclick put JavaScript in the markup, and the slides' verdict stands: like inline style, they should not be used. They have odd scoping, cannot be cached, and are blocked by a content security policy that forbids inline script. Attach listeners with addEventListener(); the first Events lesson shows why.

Extension: data-*

Any attribute whose name starts with data- is valid on any element, and the rest of the name is yours. It is the sanctioned version of the invented attributes above: CSS can select it, a script reads it as element.dataset, and HTML will never give it another meaning. Lesson 10 covers when to use one.

Character references

Some characters cannot simply be typed, because the parser would read them as syntax. A character reference writes a character by name or number instead. The slides call them entities, the old SGML name. There are three forms:

&copy; <!-- named: one of over two thousand names --> &#169; <!-- decimal: the Unicode code point --> &#xA9; <!-- hexadecimal: the same code point in base 16 -->

All three produce ©. The numeric forms reach any Unicode character, emoji included, so the slides' warning that entities cannot write non-Western characters no longer applies. End every reference with a semicolon. For historical reasons some names work without one, so &notit; becomes "¬it;", and in attribute values a name followed by = or a letter is left alone, which is why href="?a=1&copy=2" survives.

What must be escaped, and where

  • In text, escape < as &lt;. A < followed by a letter starts a tag, so if a<b and c>d creates a b element with attributes and and c, which then bolds the rest of the paragraph and the next one too.
  • Escape & as &amp; wherever it could start a reference. "AT&T" happens to be safe; escaping every ampersand is simpler than knowing why.
  • In an attribute value, escape the quote that delimits it, as &quot; or &#39;, and escape ampersands in URLs: href="?a=1&amp;b=2".
  • > never needs escaping, though many people write &gt; for symmetry, as this site's code samples do.

Raw text elements are the exception: in script and style, references are not decoded, so write < and & plainly.

UTF-8 made most of them unnecessary

References were once the only safe way to write é, € or a curly quote, because the page's encoding might not include them. A document in UTF-8, which lesson 02 makes the rule, can contain any character directly: type "café", "€20" and "日本語" as they are. References remain useful for the syntax characters above and for characters that are invisible in an editor, such as the non-breaking space. The slides' "character set issues across platforms" belong to the era before UTF-8 won.

Comments

A comment starts with <!-- and ends with -->. Its text may not start with > or ->, or contain <!--, --> or --!>, or end with <!-. In practice that means comments do not nest: commenting out markup that already contains a comment ends at the inner -->, and the rest shows on the page. (The old ban on a double hyphen inside a comment is gone.) Comments are not discarded, either. Each is a node in the tree, as the demo above shows; they are invisible on screen and nowhere else.

Uses, past and present

  • Masking. Early pages wrapped scripts in comments so browsers that did not know script would not print the code. That habit can go.
  • Inert data. Content parked in a comment does not render, and a script can read it back, awkwardly. A template, or a <script type="application/json"> block, does the job with an element made for it.
  • Conditional comments. Internet Explorer acted on comments such as <!--[if IE 8]>, so authors served it patches other browsers ignored. IE 10 dropped them in 2012; they are history now.

Comments are sent to every visitor

An HTML comment is part of the page, downloaded by everyone and one View Source away. Notes about known bugs, internal hostnames, test accounts or "TODO: remove the admin bypass" are reports written for an attacker. Keep developer notes in source files that the build strips, or in a server template's own comment syntax, which never reaches the browser. (The slides mention one deliberate exception, a "honeytrap" comment with a fake login that raises an alarm when used.) Dead markup left in comments costs bytes on every request. Delete it; version control remembers.

Try it: read the grammar in the tree

Four exercises, each about ten minutes. Predict each answer before you check it.

  1. In element-kinds.html, type <title>Fish &amp; chips</title>, then the same text inside <style>. Explain the difference between the two text nodes.
  2. Write a paragraph containing <note> and <side-note>. In the console, compare document.querySelector('note').constructor.name with the same for side-note. Then style note and confirm the rule applies.
  3. In attributes.html, run document.querySelector('button').disabled = false in the console and watch the attribute in the Elements panel. Then try document.querySelector('button').setAttribute('disabled', 'false') and explain the result.
  4. On a site you use, search View Source for <!--. For each comment, decide whether it should have been shipped to every visitor.

Summary

ConceptKey points
Tags and elementsA tag is the bracketed text; an element is what the tags delimit, and what ends up in the tree.
Case and nestingNames are case-insensitive; write them lowercase. Treat values as case-sensitive. Close elements in reverse order.
WhitespaceKept in the tree, collapsed by CSS. pre, textarea and white-space: pre keep it.
Unknown namesUnknown elements stay as HTMLUnknownElement; hyphenated names are HTMLElements awaiting a definition. Unknown attributes are kept and ignored; use data-*.
Six kinds of elementVoid (the slash is ignored), raw text, escapable raw text, template, foreign (the slash closes) and normal.
Boolean attributesPresence is true, absence is false; disabled="false" is disabled.
Enumerated attributesFixed keywords, any case, with missing and invalid value defaults that apply silently.
Global attributesid for fragments, references and scripts; class for styling; title only as an extra; lang, dir, translate; hidden, inert; tabindex 0 or -1. Avoid accesskey, style and on*.
Character referencesNamed, decimal or hex. Escape < and & in text, and the delimiting quote in attributes. With UTF-8, type everything else.
CommentsDo not nest; are nodes in the tree; reach every visitor, so ship nothing private in them.

Next steps

This lesson covered the grammar a document needs to be well-formed. Lesson 04, Content models and validity, moves up a level, to which element may appear where: content categories as a type system, what the parser does when they are broken, and the validator as the linter for the half of the contract the browser never checks. The HTML topics taught in other sections are listed below.