CSE 134B Web Client Languages
  1. Structure
  2. HTML
  3. Extending HTML

Lesson 10 · Metadata

Extending HTML

HTML's vocabulary is fixed, and every real page needs words it does not have: a trail card, a filter that is set to "hard," the id of the row a script should delete. Authors have always found ways to say more, some of them safe and many of them not. This last lesson of the series is about the safe ones, the extension points the language itself provides: data-* attributes for state and data, and custom element names for structure and, when a script arrives, behavior. It ends with the question to ask before using either: is there already an element for that?

Learning objectives

  • List the extension points HTML provides, and explain why invented attributes and unhyphenated element names are not among them
  • Write valid data-* attributes, read and write them through dataset, and style them with attribute selectors
  • Decide between class, a data-* attribute, a native attribute and an ARIA state for a given piece of state or a variant
  • Use custom element names to name structure with no script, and explain how they render, how they are styled and how :defined tells the two states apart
  • Describe in outline what a custom element definition adds, and where the Components section teaches it
  • Apply the "use the platform first" checklist before writing a component, and read what real pages actually do with these features

A fixed vocabulary and a growing page

Lesson 01 called HTML a language, and like any language it has a dictionary. The standard defines the elements and attributes, and browsers, screen readers, crawlers and validators all agree on what each one means. That shared dictionary is the whole value of the thing: a <nav> written in 2010 is still navigation to every reader in 2026.

The dictionary is also, by design, small. It has words for the shapes every document shares, and none for a trail card, a star rating or "this row is record 42." Every application needs names and state no general-purpose language could predict, so every application extends HTML, one way or another.

The slides list the ways people have done it, and the list is a tour of good and bad ideas: class names that act as private element names (<div class="superThing">), data-* attributes, <script> elements with made-up types used as inert text blocks, meta and link elements with invented names, values hidden in comments for a script to dig out, and custom elements. The HTML standard has its own list, in a section called "Extensibility," which opens: "HTML has a wide array of extensibility mechanisms that can be used for adding semantics in a safe manner." Among its list:

  • class, to extend an element "while using the most applicable existing 'real' HTML element, so that browsers and other tools that don't know of the extension can still support it somewhat well";
  • data-* attributes, which "are guaranteed to never be touched by browsers";
  • <meta name="" content=""> for page-wide metadata, and registered rel values for links;
  • <script> with a custom type, to embed raw data;
  • microdata, the itemscope and itemprop attributes, "to embed nested name-value pairs of data to be shared with other applications and sites";
  • custom elements, "to extend the vocabulary of HTML."

Two corrections to the slides: a meta element's value goes in content, not value, and HTML has no <quote> element; it has q and blockquote.

What makes an extension "safe" is a promise from the language. Lesson 03 showed what the parser does with names it does not know: an invented attribute such as meme="true" is kept, scripts and selectors can find it, and it is invalid, because nothing stops a future version of HTML from giving meme a meaning of its own. The same goes for an invented element without a hyphen. The two extension points this lesson covers are the names HTML has promised never to take: any attribute that starts with data-, and any element name with a hyphen in it. The head extensions (meta, link rel, data blocks and microdata) are lesson 09's subject.

The extension ladderSix steps from left to right, rising like stairs. Step 0, highlighted as the place to start: a native element or attribute, understood by every browser, screen reader and crawler. Step 1: class, understood by your CSS and scripts. Step 2: data attributes, read by your CSS and your scripts, through dataset. Step 3: a custom element name with no script, which the parser keeps, CSS selects and the DOM exposes as HTMLElement. Step 4: a custom element with a definition, where the browser runs your class. Step 5: a framework component, which only the framework understands until it emits HTML. An arrow along the bottom reads: each step up is more code you write, test and keep accessible.0. start herenative elementor attributeeveryone knows it1.classyour CSS andscripts2.data-*your CSS, andscripts via dataset3.<trail-card>no scriptparser keeps it,CSS selects it,HTMLElement4.customElements.define()the browserruns your class5.<TrailCard />frameworkonly the framework,until it emits HTMLeach step up: more code to write, test, ship and keep accessible
The extension ladder. Climb only as high as the problem needs, and start at step 0: the element or attribute HTML already has.

data-* attributes

The slides give the case for data-* in three lines: it "allows for user-defined meta data," it "reduces name collision problems," and it "removes overloading of class attributes or invention of non-standard attributes." Lesson 03 introduced it among the global attributes. This section covers how to use it well.

Naming

The standard defines a custom data attribute as "an attribute in no namespace whose name starts with the string 'data-', has at least one character after the hyphen, is a valid attribute local name, and contains no ASCII upper alphas." Any element may carry any number of them, with any value. In practice that means lowercase words joined by hyphens: data-trail-id, data-length-km, data-state.

The "no upper alphas" rule rarely bites in HTML, because the parser lowercases attribute names as it reads them. data-Difficulty="hard" arrives in the DOM as data-difficulty, which was confirmed in Chrome for this lesson. It matters in XML documents and in scripts, and it is a reason never to rely on capitals in a name. A library that adds data attributes to other people's pages should, in the standard's words, "include their name in the attribute names, to reduce the risk of clashes": data-trailnotes-map rather than data-map.

dataset and the camelCase mapping

Scripts can read a data attribute with getAttribute() like any other, but every element also has a dataset property: an object with one property per data attribute. Its names follow a fixed mapping. Drop the data- prefix, then turn every hyphen followed by a lowercase letter into that letter in uppercase. So data-trail-id is dataset.trailId and data-length-km is dataset.lengthKm. Writing runs the mapping backwards: setting dataset.lastSeen creates data-last-seen. Deleting a property removes the attribute.

How data attributes map to datasetOn the left, three attributes in the markup of a list item: data-trail-id equals t03, data-length-km equals 6.1, data-status equals closed. Arrows labeled read lead right to the dataset properties trailId, lengthKm and status, each with its value in quotes, because every value is a string. Below, a reverse arrow labeled write leads from the script statement dataset.lastSeen equals 3 back to a new attribute in the markup, data-last-seen equals the string 3. A note gives the rule: drop data-, and each hyphen plus lowercase letter becomes the uppercase letter.in the markup (attributes)in the script (li.dataset)data-trail-id="t03"data-length-km="6.1"data-status="closed"trailId → "t03"lengthKm → "6.1"status → "closed"readdata-last-seen="3"dataset.lastSeen = 3writeDrop "data-"; each hyphen plus a lowercase letter becomes that letter in uppercase. Values are always strings.
One attribute, two spellings. The markup uses hyphens, the script uses camelCase, and both always hold a string.
<li data-trail-id="t03" data-length-km="6.1" data-status="closed">Canyon Spur</li> <script> const li = document.querySelector('li'); li.dataset.trailId; // "t03" li.dataset.lengthKm; // "6.1" (a string, not a number) li.dataset.lastSeen = 3; // adds data-last-seen="3" delete li.dataset.status; // removes data-status li.dataset['last-seen'] = 4; // throws a SyntaxError: use the camelCase name </script>

Every value is a string

An attribute value is text, so everything that comes out of dataset is a string, and everything that goes in is turned into one. That causes two classic bugs. The first is sorting: compared as strings, "12.6" comes before "2.4", so a list sorted on dataset.lengthKm without Number() is wrong in a way that looks almost right. The second is booleans. li.dataset.open = false stores the string "false", and a non-empty string is truthy, so if (li.dataset.open) runs anyway. Both were checked in Chrome for this lesson. For an on-off state, use the attribute's presence, as HTML's own boolean attributes do: li.toggleAttribute('data-open', isOpen) to set it and 'open' in li.dataset to test it.

Styling with attribute selectors

CSS does not need dataset; it selects the attribute directly. [data-status] matches any element that has the attribute, [data-status="closed"] matches one value, and the other attribute operators work as they do anywhere else. The Selectors page covers them all.

li[data-difficulty="easy"] { border-inline-start-color: var(--easy); } li[data-difficulty="hard"] { border-inline-start-color: var(--hard); } li[data-status="closed"] { opacity: 0.7; & h2::after { content: " · closed"; } } trail-list[data-density="compact"] li p { display: none; }

This is the pattern the rest of the lesson leans on. The markup holds the state, CSS reads it, and a script only ever changes the attribute: one source of truth, visible in DevTools.

data-* versus class

The slides put the problem plainly: "Many devs overload the class attribute using it to signal the interactive nature of an element, hold element/component state, or even hold data. The data-* attributes may be better." A typical overloaded element looks like this:

<!-- class doing everything --> <div class="card card--featured is-open js-trail-card trail-42"> … </div> <!-- a name, a variant, a state and a piece of data, each in its own place --> <trail-card data-variant="featured" data-state="open" data-trail-id="42"> … </trail-card>

The first version packs a name, a variant, a state, a script hook and an id into one list, each kind of token with its own naming convention, and nothing stops it from saying is-open is-closed at once. The second version gives each job its own place. The component's name is the element (a custom element, which the next section covers). The variant and the state are separate attributes, and an attribute holds exactly one value, so a card cannot be open and closed at the same time. The id is data a script can read as dataset.trailId.

This is the course's own convention, the one the site you are reading follows. Choose elements first, built-in and custom; put state and variants in data attributes; and use class rarely, for a group that differs from the default and has no better name. It is the cure for class-itis from lesson 05, carried one step further. A class does still have a proper job: the standard suggests class values "that describe the nature of the content," and a class that names a category (class="warning") is fine. What a class should not hold is changing state.

Native state comes first

Before inventing a data-state, check whether HTML or ARIA already has the state. A disclosure is <details open>, which the browser keeps up to date for you. A checked box is :checked, a disabled control is disabled, a hidden panel is hidden. A toggle button's state belongs in aria-pressed, the current page in a menu in aria-current="page", an expanded menu in aria-expanded. CSS can select every one of these ([aria-expanded="true"] works exactly like a data attribute), and they reach the accessibility tree. A data attribute does not: data-state="open" means nothing to a screen reader. If a state matters to the user, it needs a native or ARIA home, and a duplicate data-* copy of it is a second source of truth waiting to drift out of step.

Data attributes are for the rest: application state the platform has no word for, visual variants, and identifiers a script needs. The demo keeps all three in attributes.

What not to put in a data attribute

  • Secrets. Everything in the HTML is public: one View Source away, cached, and readable by every extension and script on the page. API keys, tokens and other users' details do not belong in an attribute any more than in a comment.
  • Anything the server must trust. A data-price or data-role="admin" can be edited in DevTools before a script reads it. Use it for display; check the real value on the server. Form Security makes the same argument about hidden fields.
  • Large or structured data. A kilobyte of escaped JSON in an attribute is hard to read and change. Use a data block, <script type="application/json">, or fetch() it.
  • Anything another tool should read. The standard is explicit: "These attributes are not intended for use by software that is not known to the administrators of the site that uses the attributes." Its example is a music site that stores track lengths in data-length; the site's own scripts may sort by them, but a search engine reading them would be misusing them. Metadata for other software belongs in a shared vocabulary such as schema.org, which lesson 09 covers.
  • What a standard attribute already says. Again the standard: "When a standard attribute is available, there is no need for a custom data attribute to be used." Not data-lang but lang; not data-disabled but disabled.

One more line from the standard is a good test for any design: "Authors should carefully design such extensions so that when the attributes are ignored and any associated CSS dropped, the page is still usable."

Custom elements as names

Data attributes extend what an element carries. Custom elements extend what an element is called. Lesson 03 showed the parser's side: <foo> becomes an HTMLUnknownElement, while <trail-card>, with its hyphen, becomes an ordinary HTMLElement waiting for a definition. That waiting element is already useful, before any script exists.

The naming rules

A valid custom element name, in the standard's terms:

  • starts with a lowercase ASCII letter, so the parser reads it as a tag name;
  • contains a hyphen;
  • contains no uppercase ASCII letters, so the name can be matched without regard to case;
  • is otherwise a valid element name, which allows a wide range of characters (the standard's own example is <math-α>);
  • is not one of eight names already taken by SVG and MathML: annotation-xml, color-profile, font-face, font-face-src, font-face-uri, font-face-format, font-face-name and missing-glyph.

The hyphen is the important rule, and it is a promise. In the standard's words, it is "used for namespacing and to ensure forward compatibility (since no elements will be added to HTML, SVG, or MathML with hyphen-containing local names going forward)." The slides describe dashed names as a convention; they are a requirement now. <trailcard> can be written and the parser will keep it, but it is invalid, it can never be given a definition (customElements.define('trailcard', …) throws a SyntaxError), and a future standard could claim the name.

Structure without script

An undefined custom element is a plain element with a name. It is in the DOM, querySelector('trail-card') finds it, and CSS selects it by its name with no class at all. It has no role in the accessibility tree, so assistive technology looks straight through it to the headings, lists and paragraphs inside; checked in Chrome's accessibility tree, a <trail-card> around an h2 and a p exposes just the heading and the paragraph. That is the right division of labor. The custom name tells authors and style sheets what the box is; the native elements inside tell users.

The course already uses this. The <form-field> pattern in Forms wraps a label, a control and its messages in an element whose name says what it holds, with no script and no div. The same idea scales to any component a site repeats: <trail-card>, <site-header>, <price-tag>.

Three details make it work. First, an unknown element renders inline, like a span, because the browser's default style sheet has no rule for it. Almost every custom element that holds blocks needs display set first. Second, :defined tells the two states apart. It matches every built-in element (including <foo>, as Chrome confirms) and every custom element whose definition has run; :not(:defined) matches a custom element still waiting. Third, the name is a hook for later: when a definition does arrive, every <trail-card> on the page is upgraded in place, without a line of the markup changing.

trail-card, trail-stats { display: block; } trail-card { border: 1px solid var(--rule); border-radius: 0.375rem; padding: 0.75rem 1rem; & h3 { margin: 0; } } trail-card:not(:defined) { border-style: dashed; } trail-card:defined { border-inline-start-width: 0.35rem; }

The slides make the strategic case for doing this even before you need behavior. <div class="wizard"> and <my-wizard> look the same today, but the custom element "actually allows us to more easily 'upscale' to a web component later on." With the class, upscaling means a framework component that emits the div, or a script that hunts for the class. With the element, it means one customElements.define() call, which the demo runs.

Clashes the hyphen cannot prevent

The hyphen keeps your names away from the language. It does not keep them away from each other. A page can hold only one definition per name; a second customElements.define() for a name already defined throws a NotSupportedError. If a site's own <date-picker> meets a library's, one of them loses. The defense is a short prefix used everywhere; the most common custom elements on the web, later in this lesson, are mostly prefixed.

Custom elements with behavior

A definition turns a name into a component. The Components section teaches this in nine steps; this is the outline, so that the parts have names when you get there.

class TrailCard extends HTMLElement { static observedAttributes = ['data-status']; connectedCallback() { // in the document: add listeners, render, read attributes } disconnectedCallback() { // removed: clean up listeners, timers and requests } attributeChangedCallback(name, oldValue, newValue) { // an observed attribute changed: update to match } } customElements.define('trail-card', TrailCard);
  • Registration and upgrade. customElements.define() ties a name to a class. Elements already in the page are upgraded, and new ones get the class as they are parsed or created. Components 02 covers it, with whenDefined() for scripts that need to wait.
  • Lifecycle callbacks. The browser calls connectedCallback when the element enters a document, disconnectedCallback when it leaves, attributeChangedCallback when an attribute listed in observedAttributes changes, and adoptedCallback when it moves to another document. attributeChangedCallback is how a component reacts when a script changes the data attributes from earlier in this lesson. Components 05.
  • Shadow DOM. An optional private subtree, with its own styles that the page's CSS does not reach. Useful for a widget that must look the same on any site; unnecessary for most of a site's own components, which are happy to be styled by the page. Components 04.
  • Templates and slots. <template> holds inert markup to clone; <slot> shows where the element's own children appear inside its shadow tree. Components 06, with declarative shadow DOM, which builds a shadow tree from HTML alone, in Components 09.
  • Form-associated custom elements. With static formAssociated = true and attachInternals(), a custom element can submit a value, take a label, validate and reset like a native control. Components 08. The same ElementInternals object can give the element a default role and expose custom states to CSS through :state(), Baseline since 2024.

Custom elements may also take attributes of their own. The standard allows "any namespace-less attribute that is relevant to the element's functioning, as determined by the element's author" on an autonomous custom element, so <trail-map zoom="12"> is valid in a way that <div zoom="12"> is not. On built-in elements, keep using data-*.

Autonomous and customized built-in elements

Everything so far is an autonomous custom element: a new name, a class extending HTMLElement. The standard also defines customized built-in elements, which extend an existing element and are written with the is attribute: <button is="plastic-button">, a real button with extra behavior. The idea is attractive, since the element keeps all of its native semantics. Chrome, Edge and Firefox support them. Safari does not and has said it will not: WebKit's published standards position on customized built-ins is "oppose," arguing that form-associated custom elements and ElementInternals cover the use cases better, and in September 2026 it closed a renewed request as a duplicate of that position. Write autonomous elements, and when you want a native element's behavior, put the native element inside: <copy-button><button>Copy</button></copy-button>.

HTML first, then the upgrade

The upgrade model is what makes custom elements fit the course's approach to building pages. The HTML arrives first and is already meaningful. The CSS arrives and styles it by element name. The script arrives last, if it arrives at all, and adds behavior to elements that are already on screen. The Foundations page calls this progressive enhancement: each layer adds to the one below it, and none of them is required for the one below it to work.

A custom element across a page loadThree stages from left to right. Stage one, HTML parsed: trail-card is an HTMLElement that matches not defined; its heading, list and paragraph are already in the DOM and readable. Stage two, CSS applied: the element is styled by its name and looks finished. Stage three, the script runs customElements.define: the element is upgraded to TrailCard, matches defined, and connectedCallback adds behavior. A red dashed branch from stage two shows the script failing or never arriving: the page stays at stage two, styled and readable, with no behavior.1. HTML parsed<trail-card>HTMLElement:not(:defined)content readable2. CSS appliedtrail-card { … }styled by its namelooks finishedno behavior yet3. script defines itcustomElements.defineupgraded to TrailCard:definedconnectedCallback runsscript fails or never arrives:the page stays at stage 2Every stage is a usable page. Behavior is the only thing the script adds.
Upgrade, not replacement. The element and its content exist from the first byte; the definition only adds behavior to what is already there.

The question to ask of any component is what the page looks like at stage 2, because scripts fail more often than authors expect: a slow train connection, a blocked domain, an error elsewhere, a crawler that never runs JavaScript. A component built from native elements inside a named wrapper degrades into exactly those native elements. A component that ships an empty <trail-card></trail-card> and builds everything in connectedCallback degrades into nothing.

Two habits follow. Put the real content in the light DOM, as children the HTML already contains, and let the definition enhance it. And style the waiting state with :not(:defined) rather than hiding it: a snippet that hides undefined elements avoids a flash of unstyled content for the half-second before a script arrives, and leaves a blank page for everyone whose script never does.

Use the platform before writing code

The slides end their tour of custom elements with "more words of warning," and they are worth taking literally. "Just because you can should you?" "Custom elements are pretty cool but shared semantics are even cooler." Elements built by authors, "whether via framework or native DOM API, are never as fast and resilient as a natively implemented element." And: "When in doubt use the platform." Before reaching for a custom element with behavior, let alone a framework, walk down this list:

  1. Is there an element for it? Landmarks and sectioning (lesson 05), headings, text and lists (06), tables (07), and the interactive elements (08): disclosure, accordions, dialogs, popovers, meters.
  2. Is there an attribute for it? hidden, inert, open, disabled, lang, popover, and the rest of the global attributes from lesson 03.
  3. Is it a form control? Dates, colors, ranges, search, suggestions with datalist, and built-in validation are in Form Fields.
  4. Can CSS do it? Hover and focus states, :has(), :checked, container queries and scroll snapping replace a great deal of old JavaScript. The CSS section covers them.
  5. Only then a custom element, built from native parts, named, and working at stage 2.
  6. Only then a framework, for the problems that are genuinely application-sized.

The slides' last point explains why the order matters over time: "If something isn't available go ahead and simulate it with a framework but be aware it might eventually become native so let the old habits die." Lesson 08 is full of examples. The accordion library became <details name>, the modal library became <dialog>, the dropdown positioning library became popover and anchor positioning. Code written for the gap does not disappear when the gap closes; someone has to notice and delete it.

Divitis with dashes

Custom elements can repeat every mistake of div-itis with a more confident name. <site-nav> with no nav inside it is not a navigation landmark. <card-title> instead of an h3 is not a heading, and drops out of the outline a screen-reader user scans. <fancy-button> is the <div class="button"> problem again: no focus, no keyboard, announced as plain text. This is the first rule of ARIA, quoted in lesson 05 and lesson 08, applied to components: if a native element has the semantics and behavior you need, use it, and if a component needs them, put the native element inside. The custom name is for the wrapper. What users meet should still be HTML they know.

HTML by the data: correct versus common

This series has taught HTML as it is specified, the "Latin of the web." The slides close their introduction with a reminder that there is also HTML as it is written, the "English," and point to the HTTP Archive's Web Almanac, which measures what millions of real pages contain. Its most recent markup chapter, in the 2024 edition, is a useful reality check on this lesson's subjects. All figures below are for mobile pages.

  • div still rules. 28.7% of all elements are divs, ahead of a (12.6%) and span (11.2%). The chapter's verdict: "divitis" is "still a thing."
  • class is everywhere. It is the most used attribute "by far": 48 billion occurrences, 33% of all attributes in the data set.
  • Data attributes are nearly universal. 90% of pages use at least one. The most common is data-id, on 24% of pages; next come data-load-time and data-tagging-id (20% each), both from analytics and tagging tools.
  • Custom elements are rising from a small base. 7.9% of pages used them in 2024, up from 5.4% in 2023 and 3.6% in 2022.
  • Most of those custom elements come from products, not authors. The top ten are wow-image (2.7% of pages, from Wix), a set of rs-* elements from the Slider Revolution plugin, and predictive-search (1.4%, a Shopify component).
  • They come with script. At the median, pages with custom elements loaded 1,286 kB of JavaScript, against 522 kB for pages without.

Read those numbers with care. The JavaScript figure is a correlation, not a cost of custom elements: the pages that use them mostly run on heavy platforms, and a name like <form-field> costs no script at all. Several of the most common data attributes belong to third-party tools, the use the standard warns against. And the div figure is the gap this series exists to close. The common version is the one you will inherit; the correct version is the one worth writing.

Ten lessons, one language

Lesson 01 opened with a phrase from the slides: HTML is "the Latin of the web." Learn it well, and everything built on top of it makes more sense. Ten lessons later, the claim can be checked against what the series covered.

  • Principles. HTML as a language with a forgiving parser and a tree that actually runs (01); the document, its doctype, encoding and MIME type (02); elements, attributes and character references (03); content models, validity and the validator (04).
  • Semantics. Landmarks and the accessibility tree (05); headings, text and lists (06); tables for data (07); the interactive elements that replaced a decade of widget libraries (08).
  • Metadata. The head and the machine-readable layer (09); and this lesson, the extension points for everything the vocabulary leaves out.

One idea runs through all ten. The browser will accept almost anything you write, and render it, and the forgiving parser will never tell you that the result means nothing to a screen reader, a crawler or the next developer. The discipline is yours: choose the element for what the content is, keep state where every reader can see it, extend the language only at the points it has set aside, and check the tree rather than the pixels. Written that way, HTML is the most durable code you will ever ship. Pages from the early 1990s still render, and a page you write carefully this week can outlast every framework that will try to generate it.

Try it: extend a page the safe way

  1. Open data-attributes.html and, in the console, run document.querySelector('trail-list').dataset.filter = 'hard'. The list filters with no event fired, because the CSS only reads the attribute. Now set dataset.open = false on a trail and test it with if. What runs, and why?
  2. In the same console, try document.querySelector('li').dataset['length-km'] and …dataset.lengthKm. Which name works for reading? Then try setting dataset['length-km'] = 1. What error comes back?
  3. Open custom-names.html and, before pressing the button, run customElements.define('trail-stats', class extends HTMLElement {}). Nothing changes on screen. Why? Then try customElements.define('trailstats', class extends HTMLElement {}) and customElements.define('trail-card', class extends HTMLElement {}) twice. Name each error.
  4. Take a recent project of your own and list every class value on one page. Sort them into four piles: component names, variants, states and script hooks. Which ones would become an element, which a data-* attribute, and which already have a native or ARIA home?
  5. Pick one component from that project and write it as a custom element name with native elements inside, no script. Turn off CSS and JavaScript (in Firefox, View, Page Style, No Style; in DevTools, disable JavaScript). Is the content still in a sensible order, with real headings and lists?
  6. Run the validator from lesson 04 on a page that uses <trailcard>, <trail-card>, meme="true" and data-meme="true". Predict which two it reports before you look.

Summary

ConceptKey points
Why extendThe vocabulary is small and shared; applications need more names and state. The standard lists safe extension points: class, data-*, meta, rel, data blocks, microdata, custom elements. Invented attributes and unhyphenated names are kept but invalid.
data-* namingStarts with data-, at least one more character, no uppercase. Lowercase words joined by hyphens; libraries add their name as a prefix.
datasetDrop data-; hyphen plus lowercase letter becomes the uppercase letter. data-length-km is dataset.lengthKm. Writing and delete change the attribute.
StringsEvery value is a string: convert with Number() before sorting; "false" is truthy, so use presence and toggleAttribute() for booleans.
data-* versus classElement for the name, data attributes for state and variants, class rarely and for categories. One attribute holds one value, so states cannot contradict each other.
Native state firstopen, disabled, hidden, :checked, aria-expanded, aria-pressed, aria-current reach the accessibility tree; data attributes do not.
What not to storeSecrets, values the server must trust, large structured data, metadata for other tools (use schema.org), anything a standard attribute already says.
Custom element namesLowercase start, a hyphen, no uppercase, not one of eight reserved SVG and MathML names. The hyphen keeps names away from future HTML, not from other libraries; use a prefix.
Names without scriptAn undefined custom element is an HTMLElement, renders inline (set display), has no role, and is styled by element selectors. :not(:defined) and :defined tell the states apart.
BehaviorcustomElements.define() upgrades elements in place. Lifecycle callbacks, shadow DOM, templates and slots, form association: the Components section. Use autonomous elements; WebKit opposes customized built-ins (is=).
Progressive enhancementHTML, then CSS, then the definition. Content in the light DOM; every stage is a usable page; style the waiting state rather than hiding it.
Platform firstElement, attribute, form control, CSS, then a custom element, then a framework. Custom names wrap native elements; they never replace a heading, a button or a landmark.
By the dataWeb Almanac 2024, mobile: div 28.7% of elements; class 33% of attributes; 90% of pages use data-*; 7.9% use custom elements, mostly from products such as Wix and Slider Revolution.

Next steps

This is the end of the HTML series, and the start of everything that uses it. Several parts of the vocabulary are taught in sections of their own: Linking takes the a element from href to the request a click sends; Forms builds the form-field pattern from this lesson into complete, accessible forms; and Images covers img, picture and responsive images. CSS turns the attribute and element selectors used here into a whole styling system, and the DOM section is where the tree this series kept pointing at becomes something a script reads and changes. When a custom element needs behavior, Components picks up exactly where this lesson stops.