An element is a start tag, its content and an end tag. The attributes inside the start tag add information or change behavior. Tag and attribute names are case-insensitive, so <P CLASS=x> works, but lowercase is the universal convention.

Attribute values may be double-quoted, single-quoted or, if they contain no spaces, quotes, =, <, > or backticks, unquoted. Boolean attributes such as hidden, disabled and required are true when present and false when absent, whatever their value: disabled="false" still disables. Enumerated attributes accept a fixed list of keywords (dir="rtl", autocomplete="off") and fall back to a default for invalid ones. Global attributes (id, class, style, lang, title, data-* and others, Global Attributes) work on every element.
Thirteen void elements have no content and no end tag: area, base, br, col, embed, hr, img, input, link, meta, source, track and wbr. The trailing slash in <br /> is allowed but ignored; on a non-void element such as <div /> it is ignored too, so the div stays open and swallows what follows.
Elements must nest properly: close the most recently opened one first. When they do not, the browser's parser repairs the tree by rules the standard defines precisely, so every engine builds the same DOM. You can watch it happen with parse5 3,936 (https://github.com/inikulin/parse5 3,936 ) (npm 2,036 install parse5), the spec-compliant parser used by jsdom 21,691 , Angular 42,088 , Lit 310,034 and Cheerio 30,512 .
import { parse, serialize } from 'parse5';
const html = '<p>One<div>Two</div><b><i>Bold italic</b> italic?</i><ul><li>A<li>B</ul>';
const doc = parse(html);
const body = doc.childNodes[0].childNodes[1]; // <html> -> <body>
console.log(serialize(body));<p>One</p><div>Two</div><b><i>Bold italic</i></b><i> italic?</i><ul><li>A</li><li>B</li></ul>
Three repairs happened. A <div> cannot sit inside a paragraph, so its start tag closed the <p> first. The misnested </b> triggered the adoption agency algorithm, which closes <i> and reopens a new <i> for the remaining text. And each <li> implicitly closed the previous one, a legal shortcut.
Pitfall: the repair can surprise JavaScript. After <p><div> the div is a sibling of the paragraph, so p.querySelector('div') returns null. Validate with the Nu Html Checker (Validation) to catch these errors before they become bugs.