A web-native type system
HTML Next types are wider than JavaScript's: the web already has richer value domains than string/number/boolean, and the type system borrows them from CSS rather than inventing its own. A type does two jobs at once, it validates an incoming value, and it tells a reader how to parse the serialized string back.
Types are declared, and default to string
Every HTML attribute is already a string, so an undeclared prop is a string, no ceremony, and string-to-string round-trips for free. A type is opt-in: you declare one only to get something other than a string. This is the same line the platform already draws, an unregistered CSS custom property is the universal * syntax (any token stream) until @property gives it syntax: "<length>".
<defs>
<prop name="label">Any string (the default when no type is given).</prop>
<prop name="count" type="number">A parsed number.</prop>
<prop name="loading" type="boolean">Presence means true.</prop>
<prop name="variant" type="outline | solid | ghost" default="outline">A keyword set.</prop>
<prop name="gap" type="<length>">A CSS length: 8px, 1rem, 2ch.</prop>
<prop name="accent" type="<color>">A CSS color.</prop>
<prop name="tags" type="<string>#">A comma-separated list of strings.</prop>
<prop name="rows" type="array" required>Structured data, bound by reference (shape below).</prop>
</defs>Notation, borrowed from CSS
Type expressions reuse the CSS value-definition syntax rather than a new grammar: the | bar is “exactly one of”, <type> names a data type, + is a space-separated list, # a comma-separated list, and ? marks optional. default follows XML Schema's attribute defaulting; required is the HTML boolean attribute of the same name; a prop's description is its element text, as with <option>.
Two families: serializable and reference
A type either has a lossless string form or it does not, and that single fact decides everything about how it behaves.
| Family | Types | Behaviour |
|---|---|---|
| Scalar & list L1 L2 | string, number, integer, boolean, keyword enums (L1); the CSS dimension family <length>, <percentage>, <color>, <angle>, <time>, <url>, and lists of these (L2) | A single value token. Reflects to data-*, survives SSR, round-trips losslessly via the declared type. |
| Structured L2 | object, array; shape via nested <prop> or a referenced JSON Schema | Authored as an object expression, bound by reference for reactivity, and serialized as JSON only when it crosses a boundary (SSR payload, interop). Not reflected per-attribute. Must be declared, since it cannot fall back to string. |
Functions and callbacks are excluded by design. A component communicates upward with <dispatch> and on:event, never a callback prop, so no unserializable, imperative value enters through the contract. DOM nodes and promises are excluded for the same reason.
Serialization: the string plus the type is the value
Because a serializable value has a string form and its type is declared, lowering can record every serializable prop on the native root as data-<name>. The invocation is then fully reconstructable from the DOM, replacement loses no information, and reading a value back is unambiguous: take the attribute string, parse it per the contract's type.
<x-badge variant="solid" count="3">New</x-badge>
<!-- lowers to: each serializable prop recorded as data-*, so the invocation
is fully reconstructable from the DOM -->
<span data-component="x-badge" data-variant="solid" data-count="3">New</span>
<!-- data-count="3" + the contract (count is a number) reads back as the number 3.
the string plus the declared type is the value; nothing is lost. -->| DOM | Declared type | Reads back as |
|---|---|---|
data-label="Save" | string | "Save" |
data-count="5" | number | 5 |
data-loading (present / absent) | boolean | true / false |
data-gap="1rem" | <length> | a length |
data-variant="solid" | outline | solid | ghost | "solid" |
Typed values carried in strings are everywhere in HTML and CSS already: <time datetime="2026-09-08">, <input type="number" value="5">, <meter value>, and a registered @property custom property. HTML Next reuses the idea rather than inventing a parallel primitive-preservation channel. Reflection is uniform: a prop is recorded as data-* even when it also maps to a native attribute, so data-* is always the complete provenance record. Structured props are the one exception, they are carried as JSON rather than reflected. See lowering & provenance.
Structured data: shape, authoring, wire
CSS has no object or array type, so structured data steps outside CSS to the web's other data standards. Three concerns stay separate, and each has its own well-defined form:
- Shape is declared in HTML, recursively, with nested
<prop>(the contract's own vocabulary, no data island), or by referencing a JSON Schema for shared or external shapes. - Authoring a value uses an object expression (
{ id: 1, name: 'Ada' }), lighter than JSON and pure, not a JS object. In practice most structured values are a reference to<state>or<data>rather than an inline literal. - Wire form is JSON: the mandatory-double-quote serialization is a machine concern, used when a value must cross a boundary (SSR payload, network, interop), never the authoring syntax.
<!-- shape described in HTML, recursively, with nested <prop> -->
<prop name="rows" type="array">
<prop type="object">
<prop name="id" type="number">
<prop name="name" type="string">
</prop>
</prop>
<!-- or reference a JSON Schema for shared / external shapes -->
<prop name="rows" type="array" schema="/schemas/rows.json">A declared type already does what a userland schema validator does: parse an input, validate it against a shape, and hand back a typed value. Libraries like Zod and Valibot exist precisely because neither HTML nor JavaScript offers that natively. Get the type layer right here, and the interop with the JavaScript layer above it, and schema validation becomes a DOM-native capability: a component prop, a form field, or a data source carries its own schema and the platform enforces it, with no bundled validator. The contract is the schema.
Keywords and the quoting rule
An enum is just string constrained to a set of keywords. It is written with bare keywords (outline | solid | ghost, never "outline" | "solid") because in attribute position an unquoted identifier and a quoted string are the same token: <foo bar=quiz> equals <foo bar="quiz">, and [bar=quiz] equals [bar="quiz"]. So the string value "solid" satisfies outline | solid | ghost.
That equivalence flips in expression position, which is not attribute position. In the assignment-free expression language a bare word is an identifier (a binding) and quotes make a string literal, exactly as in a CSS selector value versus a scripting expression:
<!-- ATTRIBUTE position: bare and quoted are the same token (as in HTML/CSS) -->
<x-button variant=solid> is identical to <x-button variant="solid">
<!-- and the enum type is written bare: outline | solid | ghost -->
<!-- EXPRESSION position: bare is a reference, quotes make a string literal -->
<button class:active="variant = 'solid'"> <!-- 'solid' is the string -->
<button class:active="variant = solid"> <!-- WRONG: solid read as a binding name -->Attribute / type / selector land: bare equals quoted (both the string). Expression land: bare is a reference, quotes are a literal. Both are where HTML and CSS already put the line, so neither needs a new rule to learn.
Web value types
The dimension family is only part of a larger set of value domains the web already defines, which a markup type system should name rather than collapse to string:
| Domain | Examples |
|---|---|
| Attribute value spaces | boolean & enumerated attributes, token lists, sets |
| Web value types | URLs, MIME types, IDs & ID references, colors, lengths, percentages, times, angles |
| Constrained values | numeric ranges, element references, selectors |
| Content models | permitted / required / repeated / mutually exclusive children (e.g. tabs constraining their panels) |
| Trusted content | a dedicated trusted-HTML type: the only value $html places without sanitizing (an ordinary string is sanitized) |
Beyond values: layered identity, content, and trust
These are Level 3 directions that a value-only type system does not reach, and where "web-native" means structural, not just scalar.
The same author-facing attribute can parse to a value, reflect to a DOM property under a different name, and participate in the reactive graph as a third thing. Naming those layers explicitly is what lets one source lower correctly to React props, Vue refs, and native attributes without guessing. Lit @property converters are prior art for the attribute-to-property half of this, typed reflection with a declared converter.8
A type can constrain markup, not only values: which children are permitted, required, repeated, or mutually exclusive (a tab list constraining its panels). This is the XML-Schema-shaped half of the system, applied to component composition.
Placing raw, unsanitized markup accepts only a dedicated trusted-HTML type, never an ordinary string. There is no .innerHTML authoring syntax: markup goes through the $html directive, which sanitizes an ordinary string and places a trusted-HTML value as-is. The platform sinks a target ultimately lowers onto (innerHTML, outerHTML, srcdoc) are governed by the platform's Trusted Types; a bare string reaching them is a conformance error, which is what keeps the language eval()-free end to end.
References
- CSS Values and Units Level 4, value definition syntax and dimension types (
<length>,<color>,|,+,#,?). - IETF, JSON Schema (the standard shape language for structured data).
- CSS Properties and Values API, @property (registered typed values; universal
*syntax when unregistered). - WHATWG HTML,
data-*attributes and boolean attributes. - W3C XML Schema Part 1, attribute defaulting (the
defaultattribute). - W3C, Trusted Types (the trusted-content type model HTML Next mirrors for raw markup via
$html). - Zod and Valibot (userland schema validation, the parse/validate/infer pattern this makes DOM-native).
- Lit @property converters: typed attribute-to-property reflection with a declared converter, prior art for the three-layer model where an author-facing attribute, a reflected DOM property, and the value it parses to are named separately.