---
title: 'Tags, Attributes, and Entities'
source: 'https://academia.sh/en/courses/web-fundamentals-and-html/tags-attributes-and-entities'
course: 'Web Fundamentals and HTML'
language: en
updated: '2026-08-17T18:09:31+00:00'
license: 'CC BY-SA 4.0'
---

# Tags, Attributes, and Entities

Tag and attribute writing rules, character reference resolution, the difference between text and attribute context, and recovery behavior in flawed markup.

The previous lesson built the document's outermost shell and used tags without going into
their syntactic detail. This lesson defines that detail: how a tag is written, which forms
attributes accept, how the `<` and `&` characters that appear in text are written, and what
the parser does with writing that breaks the rules.

These rules are not a list to memorize. Each one corresponds to a specific transition in the
parser's state machine, and knowing that transition explains the reason behind unexpected
results.

## Writing a Tag

An opening tag starts with `<`, continues with the element name, continues with optional
attributes, and ends with `>`. A closing tag starts with `</` and carries only the name.

Element names are case-insensitive: `<P>` opens the same element as `<p>`. The parser
lowercases the name and builds it into the tree in lowercase. Not being case-sensitive does not
mean two forms of writing can be used; a single form is used within a document, and lowercase
is the established form.

Every non-void element has a closing tag. As seen in the previous lesson, not writing part of
it gives a defined result, but every unwritten closing tag makes it harder for the reader to
parse the document in their own head.

## Attributes

An attribute is a name–value pair written inside an opening tag that describes the element.

```html
<img src="slope.jpg" alt="Measurement pole" width="960" height="640">
```

Attribute names are case-insensitive too and get lowercased. Their order carries no meaning;
if the same name is written twice on the same element, the first applies, the second is
ignored.

There are three forms of writing a value, and all three are valid:

```html
<td colspan="2">    <!-- double quote -->
<td colspan='2'>    <!-- single quote -->
<td colspan=2>      <!-- unquoted -->
```

The unquoted form cannot contain a space, a quote character, `=`, `<`, `>`, or a backtick.
This constraint produces a trap: in the form `class=measurement table`, `table` is counted
as a separate attribute, it does not become part of the value. For this reason values are
always quoted; tracking where the constraint begins is more expensive than just always
applying the rule.

**Boolean attributes** are a separate class. For these, what carries the meaning is not the
value, it is the attribute's **presence**.

```html
<input type="checkbox" checked>
<input type="checkbox" checked="">
<input type="checkbox" checked="checked">
```

All three forms mean the same thing: checked. Counter to intuition, `checked="false"` means
the same thing too — the attribute is written, so it is checked. The only way to turn it off
is to not write the attribute at all.

## Character References

Inside text, the character `<` is taken as the start of a tag, `&` as the start of a
reference. The way to write these characters as ordinary text is a **character reference**.
It has three forms: named (`&lt;`), decimal numeric (`&#60;`), and hexadecimal numeric
(`&#x3C;`).

The script below applies the resolution rule. Named references are, in reality, looked up
from a table of close to a couple thousand names; a small subset is used here.

```js
// entities.mjs — resolving character references
const NAMED = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", reg: "®" };
const PATTERN = /&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*)(;?)/g;

function resolve(text, isAttribute) {
  return text.replace(PATTERN, (full, body, semi, pos) => {
    if (body.startsWith("#")) {
      const hex = body[1] === "x";
      return String.fromCodePoint(parseInt(body.slice(hex ? 2 : 1), hex ? 16 : 10));
    }
    let name = null;
    for (let n = body.length; n > 0; n -= 1) {
      if (body.slice(0, n) in NAMED) { name = body.slice(0, n); break; }
    }
    if (name === null) return full;
    const exactMatch = name.length === body.length && semi === ";";
    const rest = body.slice(name.length) + semi;
    if (exactMatch) return NAMED[name];
    const next = rest[0] ?? text[pos + full.length] ?? "";
    if (isAttribute && /[=a-zA-Z0-9]/.test(next)) return full;
    return NAMED[name] + rest;
  });
}

const examples = [
  "5 &lt; 8 &amp;&amp; 8 &gt; 5",
  "risen &#8722;2&#xb0;C to 6&#xb0;C",
  "code: &lt;p&gt;",
  "&weather; not in table",
];
for (const example of examples) {
  console.log(JSON.stringify(example), "->", JSON.stringify(resolve(example, false)));
}

console.log("--- same string, two contexts ---");
const address = "/measure?type=humidity&regcount=3";
console.log("as text content    :", resolve(address, false));
console.log("as attribute value :", resolve(address, true));
console.log("escaped writing    :", resolve("/measure?type=humidity&amp;regcount=3", true));
```

```
"5 &lt; 8 &amp;&amp; 8 &gt; 5" -> "5 < 8 && 8 > 5"
"risen &#8722;2&#xb0;C to 6&#xb0;C" -> "risen −2°C to 6°C"
"code: &lt;p&gt;" -> "code: <p>"
"&weather; not in table" -> "&weather; not in table"
--- same string, two contexts ---
as text content    : /measure?type=humidity®count=3
as attribute value : /measure?type=humidity&regcount=3
escaped writing    : /measure?type=humidity&regcount=3
```

The first line resolves three named references. The second mixes the decimal and hexadecimal
forms of numeric references; both refer to the same set of code points — the code points
defined in the Character Encodings lesson of the How Computers Work course. The fourth line
shows that a name not in the table is not resolved and is left as is.

## The Same String, Two Contexts

The second block of the output makes a rule visible that is missed often. The string
`/measure?type=humidity&regcount=3` breaks, producing the character `®`, in text content; it
does not break in an attribute value.

The reason for the difference is a separate rule defined for named references that do not end
in a semicolon: in an attribute value, if the reference is followed by `=` or a letter/digit,
no resolution is done. This rule exists to prevent addresses inside quotes from silently
breaking — but it only applies in attribute context.

The third line shows the correct way to write it. When an address is written into an attribute
value, the parameter separator is escaped in the form `&amp;`. Even though the attribute-context
rule makes this escaping unnecessary in most cases, escaping gives uniform behavior instead of
relying on the rule working in "most cases."

The general rule covers three characters: in text content, `<` and `&` are escaped; in an
attribute value, the quote character surrounding the value is escaped as well. This has a
security dimension too, and not doing this escaping when text coming from a user is placed
into the document is the source of the most common web vulnerabilities; the subject is detailed
in later courses.

## Recovery Rules

The first lesson said "there is no such output as an invalid document." The script below makes
that claim concrete: it prints the open element stack at every token and shows what is done on
writing that breaks the rules.

```js
// recovery.mjs — the open element stack's trace in flawed markup
const VOID = new Set(["br", "img", "input", "meta", "link", "hr"]);
const CLOSES = { li: ["li"], dt: ["dt", "dd"], dd: ["dt", "dd"], p: ["p"], ul: ["p"], h1: ["p"] };

function trace(text) {
  const stack = [];
  console.log("source:", text);
  for (const m of text.matchAll(/<(\/?)([a-z0-9]+)[^>]*>/g)) {
    const closing = m[1] === "/";
    const name = m[2];
    let note = "";
    if (!closing) {
      const implicit = CLOSES[name] ?? [];
      while (stack.length > 0 && implicit.includes(stack[stack.length - 1])) {
        note = "(open " + stack.pop() + " closed on its own)";
      }
      if (VOID.has(name)) note = "(void element, not pushed to stack)";
      else stack.push(name);
    } else if (VOID.has(name)) {
      note = "(void element's closing tag is ignored)";
    } else {
      const position = stack.lastIndexOf(name);
      if (position === -1) note = "(no matching opening, ignored)";
      else {
        if (position !== stack.length - 1) note = "(" + stack.slice(position + 1).join(", ") + " above it also closes)";
        stack.length = position;
      }
    }
    console.log(("  " + m[0].padEnd(10) + "stack: [" + stack.join(" > ") + "] " + note).trimEnd());
  }
  if (stack.length > 0) console.log("  end of document  left open: [" + stack.join(" > ") + "] -> closed");
  console.log();
}

trace("<p>Measurement <b>stopped</b></p>");
trace("<p>Measurement <b><i>stopped</b></i>");
trace("<ul><li>first<li>second</ul>");
trace("<p>text</span></p>");
```

```
source: <p>Measurement <b>stopped</b></p>
  <p>       stack: [p]
  <b>       stack: [p > b]
  </b>      stack: [p]
  </p>      stack: []

source: <p>Measurement <b><i>stopped</b></i>
  <p>       stack: [p]
  <b>       stack: [p > b]
  <i>       stack: [p > b > i]
  </b>      stack: [p] (i above it also closes)
  </i>      stack: [p] (no matching opening, ignored)
  end of document  left open: [p] -> closed

source: <ul><li>first<li>second</ul>
  <ul>      stack: [ul]
  <li>      stack: [ul > li]
  <li>      stack: [ul > li] (open li closed on its own)
  </ul>     stack: [] (li above it also closes)

source: <p>text</span></p>
  <p>       stack: [p]
  </span>   stack: [p] (no matching opening, ignored)
  </p>      stack: []

```

Four rules become visible. A closing tag with no matching opening is **ignored**. When
elements are closed out of order, the elements above the one being closed close too — in the
second example, the `i` element closes when `b` closes, and the `</i>` that comes after falls
flat. Elements left open at the end of the document are closed. Some elements close on their
own when a sibling opens.

What these rules can together produce may not match the author's intent. In the second
example, the author wanted the `b` and `i` elements to span the same text, but in the end the
`i` element never spans the word `stopped` at all. The parser reports no error; the document
is built. This is the reason markup-validating tools are necessary: the flaw is not visible at
parse time, it is visible in the result differing from what was expected.

## Comments

A comment is a section starting with `<!--` and ending with `-->`, and it produces a comment
node in the tree. It is not painted, but it stays in the document and is readable. Nothing
written inside it is hidden; server addresses, notes about removed features, and temporary
explanations are sent along with the document.

The sequence `--` inside a comment can lead to undefined behavior; for this reason it is not
used as a separator.

## Summary

- Element and attribute names are case-insensitive and get lowercased; attribute order carries
  no meaning, and on a repeated name the first applies.
- Attribute values can be written unquoted too, but the unquoted form ends at a space; for this
  reason values are always quoted.
- For boolean attributes, what carries the meaning is not the value, it is the presence; the
  form `checked="false"` turns the attribute on too, and the only way to turn it off is to not
  write it.
- There are three forms of character reference; a named reference not ending in a semicolon
  gives a different result in text content than in an attribute value.
- The parser does not stop on flawed markup: an unmatched closing tag is ignored, closing out
  of order also closes the elements above it, and elements left open are closed at the end of
  the document.
- Comments are sent along with the document and are readable; they carry no hidden information.

## Next Step

While resolving character references, this lesson assumed one thing: which rule bytes are
turned into characters by was already known. That rule, though, is declared in the document
itself, and is guessed at when it is not declared. The next lesson takes up the metadata tags
in the head section, and starts with the most important of them: how the character-set
declaration decides the way bytes are interpreted.
