Chapter 22: Attributes Are a Protocol Surface
Attributes are one of the web’s simplest extension points. They’re also one of its most durable.
An attribute can be written by hand, rendered by a server, produced by React, set by Lit, inspected by tests, read by CSS, parsed by JavaScript, queried by accessibility tools, and understood by build pipelines and analyzers. Attributes survive across frameworks because they belong to HTML, not to the framework that produced them. A <button data-track="checkout"> rendered by a 2014 jQuery plugin reads the same way to a 2025 Lit component, a CSS attribute selector, a Playwright test, or a runtime delegation handler. The protocol is the document.
This makes attributes a powerful place to define application metadata. Modern frontend architecture can use them as a protocol surface — a way for components and markup to declare meaning without importing application infrastructure.
This chapter argues that the pattern isn’t new. The platform has been using attributes as protocol since the beginning. Web-native architecture takes the pattern seriously instead of working around it.
The Platform’s Existing Attribute Protocols
Section titled “The Platform’s Existing Attribute Protocols”A short tour of the protocols already encoded in HTML attributes:
Link semantics. The <a> element accepts a rel attribute — rel="noopener", rel="nofollow", rel="prev"/rel="next", rel="canonical", rel="stylesheet", rel="preload". Each value declares a specific relationship between the link and its target, and the browser interprets the declaration. rel="noopener" changes the security boundary of a new window. rel="canonical" (on <link> in the head) tells search engines which URL is authoritative. The link itself looks like just a hyperlink; the attributes declare meaning the platform acts on.
Form input behavior. The <input> element accepts type, name, value, required, pattern, min, max, step, minlength, maxlength, inputmode, autocomplete, enterkeyhint, placeholder, readonly, disabled, multiple, and a long list of others. Each attribute is a declarative protocol the browser implements — type="email" triggers email validation, inputmode="numeric" requests a numeric keyboard on mobile, autocomplete="email" lets the browser’s password manager and autofill systems recognize the field, pattern="[A-Z]{2}\d{6}" runs the value against a regular expression at validation time. The form’s behavior is largely a function of the attributes its controls carry.
Accessibility. The entire ARIA vocabulary — role, aria-label, aria-labelledby, aria-describedby, aria-expanded, aria-pressed, aria-current, aria-live, aria-controls, and dozens of others — is a protocol layer on top of HTML. Each ARIA attribute decorates the accessibility tree with additional meaning. The platform reads the attributes and adjusts how it exposes the element to assistive technologies. ARIA was, when it was introduced, an explicit acknowledgement that attributes are a protocol — when the HTML element wasn’t enough, the standards bodies added more attributes rather than reaching for JavaScript APIs.
Microdata and structured data. The itemscope, itemtype, and itemprop attributes let HTML carry machine-readable structured data. <div itemscope itemtype="https://schema.org/Person"> declares a person; <span itemprop="name">Brendan Eich</span> inside it identifies the person’s name. Search engines, rich-result tools, and other consumers can read the schema-tagged content without parsing the visual layout. The pattern has been part of HTML5 since the start.
Custom data. data-* attributes are HTML’s blessed extension surface. Any attribute that starts with data- is reserved for application use and accessible through element.dataset. The pattern is widely used for tracking IDs (data-id, data-user-id), behavior flags (data-toggle, data-trigger), test selectors (data-testid), and analytics tags. The W3C explicitly designed data-* as the place to put application-specific metadata so it wouldn’t collide with future HTML additions.
Custom element observed attributes. When a custom element declares static get observedAttributes() { return ['size', 'theme'] }, the browser calls the element’s attributeChangedCallback whenever those attributes change. The element can react to attribute changes the same way it reacts to events. This makes attributes a reactive extension point — applications can set an attribute on a custom element and have the element respond.
Module and resource hints. <script type="module">, <link rel="modulepreload">, <img loading="lazy">, <img fetchpriority="high">, <iframe sandbox="allow-scripts">. Each of these is an attribute the browser interprets as instructions about loading, security, or resource scheduling. The attribute is declarative; the browser implements the behavior.
The pattern across all of these is consistent. Declare meaning in the attribute. Let the platform (or a runtime layer above it) interpret the meaning and produce behavior. HTML has been doing this for thirty years. It’s the platform’s most established extension mechanism.
The HTTP Parallel
Section titled “The HTTP Parallel”For readers from a backend background, the closest parallel is HTTP headers.
An HTTP request is a method, a path, a body, and a list of headers. The headers are the protocol’s declarative metadata surface. Content-Type declares the body’s format. Accept declares what the client is willing to receive. Cache-Control declares caching policy. Authorization declares authentication. Cookie declares session state. The headers don’t change what the request does in some fundamental sense; they decorate the request with information that intermediaries (caches, proxies, gateways, the server itself) can act on.
HTML attributes serve the same function for the document. The element declares what it is (a button, a form, an input, a link). The attributes declare additional meaning the platform or the application can interpret. A <button data-meta-event="checkout.started"> is still a button — its native behavior is preserved — but the attribute decorates the button with semantic intent that an application’s runtime can use.
This is one of the chapter’s deeper points. Attribute-as-protocol is a pattern frontend shares with the rest of the web stack. HTTP uses it. HTML uses it. The application’s metadata layer can use it. The pattern’s strength is exactly the same in each case — declarative, inspectable, framework-independent, capable of being read by many consumers without coordination.
Framework Props Are Not the Same Thing
Section titled “Framework Props Are Not the Same Thing”This is the part of the argument the chapter has to make carefully.
Framework props are useful, but they belong to a framework boundary. A React prop exists in React. A Vue prop exists in Vue. A Lit reactive property exists on a Lit element instance. Server-template variables live inside the template engine. Each of these is real and valuable in its own scope.
None of them is visible to anything that doesn’t speak the framework.
A React component might render like this:
<Button intent="primary" track="checkout.started" onClick={handleSave}> Save</Button>The intent and track props exist in React’s component tree. The Playwright test trying to find the button doesn’t see them. The CSS selector looking for primary buttons doesn’t see them. The accessibility tool inspecting the DOM doesn’t see them. The browser dev tools’ Elements panel doesn’t see them. The server-side rendering pass might serialize them as HTML attributes — or it might not, depending on how <Button> is implemented. The framework consumes the props internally and renders an HTML button with whatever attributes it chooses.
For an attribute protocol to be reliable, it has to appear in the rendered HTML. The whole point is that the protocol can be read by anything looking at the document — tests, runtime listeners, browser tools, server-side analytics, the test framework, the next framework after this one. Framework props don’t satisfy this. They live above the document.
The architectural move is to make the meaningful attributes show up in the rendered HTML, not only in the framework’s internal model. A React button that should declare analytics intent should render:
<button data-meta-event="checkout.started" data-meta-intent="primary-action"> Save</button>The framework’s job is to put the attributes there. The runtime’s job, once they’re there, is to interpret them.
A Working Metadata Protocol
Section titled “A Working Metadata Protocol”Here’s what a metadata protocol on top of attributes looks like in practice.
<kit-button meta-event="checkout.started" meta-intent="primary-action" meta-prop-location="hero"> Start checkout</kit-button>The meta-event attribute names the application-level event the interaction represents. meta-intent describes the button’s role in the design system. meta-prop-location is a custom property the application has chosen to track (here, where on the page this button appeared, since the same action might exist in multiple places).
A runtime boundary above this button listens for clicks. When the click fires:
- It walks the target’s ancestors to find the nearest element with a
meta-event(or adata-meta-event— the parser accepts both, since custom elements and plain HTML both want to participate). - It reads the metadata:
event = "checkout.started",intent = "primary-action",props = { location: "hero" }. - It walks up further to collect context from any enclosing boundary — surface, feature, entity, route, mode.
- It emits a normalized event object into the runtime.
Modules subscribed to the runtime receive the event. The analytics module logs checkout.started to whatever analytics service the team uses. The audit module records the action if it’s an audit-worthy event type. The observability module adds a breadcrumb. The debug overlay (during development) shows the event flowing through the system. None of these modules imports the button. The button doesn’t import any of these modules. The attribute protocol is the contract.
This is the architectural shape Kitsune formalizes. The pattern is general — any application can adopt it. What makes it possible is the platform’s existing attribute substrate. The runtime isn’t inventing a way to attach metadata to DOM elements; it’s reading metadata that’s already there.
Data Attributes and Custom Elements: A Small Coordination
Section titled “Data Attributes and Custom Elements: A Small Coordination”For plain HTML — server-rendered pages, framework output without custom elements — the safest public surface is data-*:
<button data-meta-event="profile.saved" data-meta-intent="primary-action">The data-* prefix guarantees the attribute won’t collide with any HTML or ARIA standard, now or in the future.
For custom elements, attribute names without the data- prefix can feel more natural:
<kit-button meta-event="profile.saved" meta-intent="primary-action">A <kit-button> is a custom element; the application defines its attribute surface; using meta-event directly mirrors the way HTML’s own elements use unprefixed attribute names (href, src, type, name).
A metadata boundary should support both. The parser checks meta-event first; if absent, it falls back to data-meta-event. The application can mix the two forms freely — custom elements use the bare form, plain HTML uses the prefixed form. The runtime treats them identically.
This matters because the architecture is meant to work across plain DOM, server-rendered HTML, Lit components, React-rendered output, and AI-generated markup. The protocol can’t depend on one authoring model. Any element that carries the metadata is a valid participant.
Attributes Should Not Leak Data
Section titled “Attributes Should Not Leak Data”Metadata on attributes is powerful and visible. The visibility is, in some cases, exactly what you don’t want.
An attribute like this is usually fine:
<button data-meta-event="profile.saved">The string profile.saved describes the event semantically without revealing user-specific information.
An attribute like this is dangerous:
<input data-meta-prop-email="person@example.com">The user’s email address is now visible in the rendered HTML. Anyone who can read the document — including any third-party script the application has loaded, including any browser extension the user has installed, including the user’s local network if the page is served over HTTP, including any analytics or error-tracking tool the application sends DOM snapshots to — can see the email. The metadata protocol has accidentally become a data-leakage surface.
The architectural rule: the protocol describes meaning, not user data. By default, metadata attributes should not capture input values or any user-specific content. If a module wants to capture user values, it should do so deliberately — through explicit API calls in JavaScript, with the team’s privacy and security review applied, and with explicit redaction patterns for sensitive fields.
This is why a working metadata protocol distinguishes event meaning (profile.saved, checkout.started) from payload capture (the form values being submitted). The first is safe to render in HTML. The second isn’t.
The discipline is part of what makes the attribute protocol durable. A protocol that’s easy to abuse will be abused. A protocol that draws a clear line between what describes the interaction and what was submitted with it is one that can be used across a real application without producing accidental privacy incidents.
Attributes as a Tooling Surface
Section titled “Attributes as a Tooling Surface”A declarative protocol helps more than the application runtime.
Tests can assert metadata:
expect(button).toHaveAttribute('data-meta-event', 'profile.saved')The assertion doesn’t depend on the rendering framework. It works for jQuery-rendered, React-rendered, Vue-rendered, Lit-rendered, server-rendered, and hand-written HTML alike. The test reads the DOM the same way every other consumer reads it.
Dev tools can inspect it. The browser’s Elements panel shows every attribute. A team can write a custom dev-tools extension that highlights elements with data-meta-* attributes and shows their values in a side panel. The metadata is visible without any application code being involved.
Documentation can show it. A component library’s docs can list the metadata each component emits, alongside its props and slots. A team’s design-system documentation can include the analytics events that designers and product managers should expect from each component.
Static analysis can read it. A tool can scan a codebase for all data-meta-event values, produce a list of every analytics event the application emits, and check it against the analytics schema the team maintains. Discrepancies surface at build time rather than in production.
Accessibility tools can correlate it. A linter can check that any button with data-meta-intent="primary-action" also has a properly accessible name, on the theory that primary actions should always be clearly labeled.
Design systems can enforce it. A component-library lint rule can require that every interactive component receive a meta-event attribute, or that the values follow a naming convention the design system has agreed on.
When application meaning lives only in click handlers and component props, it’s hard to see. Attributes make some of that meaning visible — and once it’s visible, it’s available to every tool in the ecosystem.
What This Means for Modern Frontend
Section titled “What This Means for Modern Frontend”Attributes aren’t a complete architecture. They’re a protocol surface.
They work best when combined with semantic HTML (which the previous chapter argued for), the DOM as a context tree (the previous chapter), event delegation, custom events that respect composed propagation, boundaries that supply enclosing context, a runtime that listens at boundaries and routes events to modules, diagnostics that make the flow visible, and privacy rules that distinguish describing an interaction from capturing user values.
Kitsune’s metadata boundary reads attributes, collects context from the DOM tree, normalizes events and commands, and dispatches them into the runtime. The runtime distributes the events to subscribed modules. The component declared its meaning in the markup; the runtime carried out the consequences. Neither side had to know about the other except through the attribute protocol.
This is what the document describes application meaning means in practice. The DOM stops being a render artifact and starts being an active participant in the application’s architecture. The shift is small in code (a few hundred lines of runtime, some conventions about attribute names) and large in architecture (every component is decoupled from every cross-cutting concern, observable from the outside, testable without instrumenting the framework, debuggable from the browser’s existing tools).
What Comes Next
Section titled “What Comes Next”The next chapter takes the events side of this story seriously — what custom DOM events are, how bubbling and composed propagation work, why callback-prop patterns lose information that event-bubbling patterns preserve, and how the application’s event system can use the platform’s native event machinery instead of inventing a parallel one.
Exercise: Define a Tiny Metadata Protocol
Section titled “Exercise: Define a Tiny Metadata Protocol”Create a page with three actions:
<button data-meta-event="profile.saved" data-meta-intent="primary-action" data-meta-entity-type="profile" data-meta-entity-id="me"> Save</button>
<button data-meta-event="profile.cancelled" data-meta-intent="secondary-action"> Cancel</button>
<button data-meta-command="dialog.open" data-meta-prop-target="help-dialog"> Help</button>Write a parser that, given a DOM element, returns a normalized metadata object:
function parseMeta(element) { return { event: element.getAttribute('data-meta-event'), command: element.getAttribute('data-meta-command'), intent: element.getAttribute('data-meta-intent'), entity: { type: element.getAttribute('data-meta-entity-type'), id: element.getAttribute('data-meta-entity-id') }, props: {} }}Extend the parser to read all data-meta-prop-* attributes into the props object. (Hint: iterate over element.dataset and find keys that start with metaProp. Remember that data-meta-prop-target becomes dataset.metaPropTarget in the camelCased form.)
Wire up a delegated click listener that calls parseMeta on the clicked element (or its nearest ancestor with metadata) and logs the result.
Then answer:
- Which meaning became visible in markup? Could a reader of the HTML now tell what the buttons do?
- Which meaning still belongs in JavaScript? (Probably the consequence — what actually happens when the event fires.)
- Which attributes might be unsafe to expose? Imagine adding the user’s email address as
data-meta-prop-email="..."— what happens? - How could a Playwright or Cypress test use this protocol?
- How could a runtime use it to wire analytics, audit logging, and observability without the buttons importing any of those modules?
The point is to see the document as an active participant in the application’s architecture. The buttons described meaning. The parser interpreted it. The runtime (which you’d build next) would do something with the interpretation. Everything stayed declarative, inspectable, and tool-friendly.