§7 · Declarative HTML Components Level 1 · proposed shapes

Reactivity & Data

Reactivity is a declared dependency graph in markup: mutable state, computed state, and external resources, each with a distinct lifecycle. The graph is statically analyzable, so it lowers to React state, Vue refs, Svelte runes, or a signal-based browser runtime: one semantics, many backends, no eval().

Stage 0 · Level 1 proposal

Reactive values and resources

HTML Next keeps a few concepts separate rather than overloading one element, because their lifecycles differ. All of them are declarations: they live in the definition's <defs> region, not in the visible markup (see Components).

ElementIsChanges when
<state name type value>a local mutable value<set> runs in a handler, or bind: writes it
<computed name from>read-only computed state, derived from other valuesa dependency changes
<data name src>a remote resource: a read source or write sinkits params; fetched or synchronized reactively
<state name="query" type="string" value="">        <!-- empty string -->
<state name="page" type="number" value="1">        <!-- number -->
<state name="rows" type="list(object({ id: string, label: string }))" value="[]">
<computed name="hasQuery" from="query != ''">  <!-- derived boolean -->

The design, a declared dependency graph that can be analyzed statically rather than traced at runtime, has deep prior art in signals and fine-grained reactivity5: Solid signals and createMemo and Angular signals are its closest current relatives, with Knockout observables as the historical ancestor. RxJS is a deliberate contrast, it models push streams, not the settled value cells <state> and <computed> are.

A <state> uses the same type syntax as a prop (see Types). When type is omitted, the state has type unknown; its initial value does not establish a type. For example, value="1" is a string, while type="number" value="1" is the number 1. Declare the type of a state whose values need to be checked or used as numbers, booleans, or structured data.

value supplies a constant initial state value, parsed through the declared type. It does not establish the type when type is omitted. A handler's <set value> likewise writes a typed constant, while <set expr:value> evaluates an expression when the handler runs. A <computed from> declares computed state: it stays subscribed to the values it reads and derives a read-only value. Controllers read both mutable and computed state through host.state; only mutable state is writable. Bindings & Events compares all three value forms.

Invalid reactive results

A live expression evaluates when its dependencies change. Each evaluation proposes a value for its destination. If the value does not have the destination's declared type, the evaluation does not write. The destination keeps the value from its most recent successful write. If no evaluation has written successfully, it keeps its declared default, or null when there is no default. The invalid evaluation does not notify dependents of a value change or trigger an effect that requires that new value. The binding stays subscribed: a later valid result writes normally.

For an object or list, the immediate type check asks whether the result is an object or list. Nested fields are checked when expressions read those fields. A wrong-typed items.0.name therefore leaves that particular binding at its last value while another binding can still read a valid items.0.id from the same new list. This matches the typed-reference rule for <data> below; one bad field does not discard its siblings.

The source is not rolled back. A form control keeps the user's edit under its native rules; a component prop keeps its latest direct inputValue even when its accepted value stays at the last good/default/null value; and a <data> resource keeps the response it received. A failed downstream binding does not supply the destination prop. This is different from a well-typed result that fails min, max, values, or another value constraint: that result is written, and the destination reports its invalidity (see Validation). A missing value is also different from a present value of the wrong type; absence has its ordinary empty/removal behavior.

For example, the outer component accepts a directly supplied value. The inner component has a number prop with a default, and its invocation binds that prop to the outer value:

<template component="x-reading">
  <defs><prop name="amount" type="number" default="5">Reading.</prop></defs>
  <output from:data-amount="$amount"></output>
</template>

<template component="x-reading-owner">
  <defs><prop name="incoming" type="number">Source value.</prop></defs>
  <x-reading from:amount="$incoming"></x-reading>
</template>

<x-reading-owner incoming="oops"></x-reading-owner>

The authored invocation supplies the first value. The owner then updates its incoming prop through its framework adapter. Its incoming declaration has no default, so the accepted value is initially null; the invalid input remains separately readable through host.props.incoming.inputValue. "oops" is shown in quotation marks to make clear that it is a string, not a number:

ActionOuter inputValueOuter accepted valueInner accepted amountWhat runs
Create the owner with incoming="oops""oops"null5The outer prop reports badInput. The inner binding has no conforming number to supply, so its default remains.
Supply 2222Both accepted values change; dependents update.
Supply "oops""oops"22The outer prop reports badInput, but its accepted value does not change, so inner dependents do not update.
Supply 7777Both accepted values change; dependents update again.

If "oops" is the first evaluation, the inner amount stays at 5. Remove default="5" from the inner declaration and it stays at null instead. An invalid evaluation never resets a destination that already accepted a value: after 2, it stays at 2, not 5 or null. The same sequence applies when an expression function returns a string for this number destination, or when a typed reference into a <data> response supplies the wrong kind of value.

For a separate string prop named label, from:label="abs($incoming)" proposes a number when incoming is 2. It does not write 2 into label: that prop keeps its last valid string, or its default. A build tool can flag this authored mismatch before the page runs; a permissive live implementation must still skip the write rather than throw. The source incoming stays 2.

For a one-time <set expr:value>, a result that fails the destination's immediate type skips that handler step's write; the state remains at its current value. A new list can still be written when one nested item has a wrong-typed field: references to that field become inert, while conforming fields remain readable. A <computed from> that reads a nonconforming typed reference keeps its last successfully computed value and does not publish a change. Before its first successful evaluation, the computed value is null. A directly edited control is different: its displayed input and native validity continue to reflect what the user entered even while a downstream typed destination keeps its last accepted value.

Controller writes through host.state follow these same type and invalid-write rules. An authored type mismatch, or an attempt to write read-only computed state or context, leaves the destination unchanged and reports a runtime warning once per authored location. Authored binding and handler mistakes use that warning policy too; statically knowable mistakes may be reported by build tools. Ordinary invalid user input does not warn or throw. See State writes.

Share state with descendant components

A component can share a state value with components rendered inside it, including components supplied through a slot. This lets independently authored parts of a widget react to the same value without passing a prop through every component between them. A descendant declares which ancestor state it reads in its own <defs>; the ancestor's <state> needs no additional marker.

<template component="x-steps">
  <defs>
    <state type="number" name="current" value="1">
  </defs>
  <ol><slot></slot></ol>
</template>

<template component="x-step">
  <defs>
    <prop name="number" type="number" required>
    <context name="current" from="x-steps" as="activeStep">
  </defs>
  <li from:aria-current="activeStep = index ? 'step' : null"><slot></slot></li>
</template>

<x-steps>
  <x-step index="1">Account</x-step>
  <x-step index="2">Payment</x-step>
</x-steps>

name identifies the ancestor state cell; from names its component tag. Optional as names the value in the reader's expressions and defaults to name, so the example could use current directly. The reader sees the ancestor cell's current value and type, not a new cell or a synthetic x-steps.current object. When an existing <set> or bind: changes that state, bindings that read the context update through the same reactive graph.

The nearest matching ancestor in the logical component tree supplies the value, including when another instance of the same component is nested inside it. An ancestor matches if it has the component tag named by from and a <state> named by name; no state export declaration is required. If no ancestor matches, the reader has a conformance error when instantiated. The imported name shares the reader's flat declaration namespace. A context read is read-only: <set> and bind: cannot write through it. These rules add no event or command channel.

This is an HTML Next declaration, not a native HTML element. The Web Components Context Protocol provides a useful event-based way for JavaScript components to exchange values, but its request follows DOM event propagation. HTML Next's lookup follows component ownership so projection and portals retain the same provider even when physical DOM ancestry differs. The browser runtime may use native observation primitives underneath; no authored callback is needed.

Level placement

This feature is part of the current proposal and its reference implementation. It could move to Level 2 if the Level 1 scope is narrowed later.

<data>: a declared, reactive resource

This is HTML Next's standards-shaped answer to htmx1: instead of hx-get/hx-trigger/hx-target string attributes swapping opaque HTML, a <data> element declares a typed, reactive resource whose parameters are visible right where it lives. For a read, each <param from:value> subscribes to the state or prop it reads: change one and the resource refetches, and bindings that read it re-render. A <param expr:value> supplies a request-time expression without subscribing to it; its inputs alone do not refetch. This is the surface Solid createResource already ships6, a resource whose fetch re-runs on source change and exposes loading and error; TanStack and Vue Query are the same idea with a params-keyed cache.

<data name="search" src="/api/search" type="object" debounce="200ms">
  <param name="q" from:value="query">     <!-- subscribes to query: refetches when it changes -->
  <param name="page" from:value="page">
</data>

Params are serialized, never interpolated into a URL string. A {name} placeholder in src is filled from the matching <param> (RFC 6570 URI Templates)2; on a read, params not named in the template become the query string. There is no string concatenation, and therefore no injection surface.

Expanded request methods

<data method> is an ASCII-case-insensitive enumerated attribute with lowercase canonical keywords. Level 1 keywords are get, query, post, put, patch, and delete. Before constructing a request, an implementation maps the selected keyword to GET, QUERY, POST, PUT, PATCH, or DELETE; patch and PATCH both produce HTTP PATCH. This rule is explicit because Fetch normalizes only a limited set of methods and does not uppercase patch for the caller.9

get follows the read lifecycle using URL query parameters. query follows the read lifecycle while encoding non-identity params as request content with the selected media type, as defined by RFC 10008.10 Both run on connection, re-run when a parameter changes, and cancel stale in-flight reads. The other methods follow the write lifecycle only when a send policy is present. The HTTP verb describes the request; it does not turn a command into reactive synchronization.

The keywords follow HTML's lowercase enumerated-attribute convention and map to uppercase HTTP methods before request construction. The form action dialog is not an HTTP method and is not valid on <data>.

A <data> exposes a small, typed surface any binding can read:

PathMeaning
search.pendinga request is in flight
search.valuethe resolved value (typed as type)
search.errorthe failure, if any
search.oksettled with a value and no error
saveDraft.dirtya write resource has body changes not yet acknowledged

Controllers read the same resource through host.data.search: for example host.data.search.pending and host.data.search.value. A resource is absent from host.state. Its response and status fields are read-only; reads participate in host.effect dependency tracking. A mutable local copy belongs in a declared <state>.

To refetch with unchanged inputs (a manual refresh, polling) there is no imperative call: bump a state param the source depends on, or declare a poll interval. For a read, a param change cancels the stale in-flight GET request. Reads are keyed by their resolved params, giving a natural cache key.

Locality of behaviour, end to end
<input bind:value="query" placeholder="Search…">
<template $match>
  <progress $when="search.pending"></progress>
  <output $when="search.error">$search.error.message</output>
  <ul $else>
    <li $each="r of search.value.results" $key="r.id">$r.title</li>
  </ul>
</template>

What goes on the wire

Typing oat into the control bound to query changes a declared value, which is the whole trigger: the <param> that binds it is a dependency, so the resource re-reads. Nothing calls the endpoint imperatively, and no code assembles a URL. With debounce="200ms" the keystrokes coalesce into one request, and each param is serialized into the query string because neither name appears in the src template:

GET /api/search?q=oat&page=1
Accept: application/json

The endpoint answers with JSON. The payload becomes search.value as it arrived; a declared type constrains the references into it rather than gating the response as a whole:

{
  "results": [
    { "id": "r-1", "title": "Rolled oats" },
    { "id": "r-2", "title": "Oat milk" }
  ]
}

From there the result is ordinary reactive data: search.pending was true while the request was in flight, search.value.results now feeds the $each above, and search.ok is true. The last resolved value stays bound while the next read runs, so a refetch does not blank what the page already shows. Editing query again cancels the in-flight read before starting the next one, so a fast typist never renders an earlier answer over a later one.

A reference conforms or it is inert

A declared type is checked where a reference is read, not where its value arrived. If results.0.title holds a number where the declaration says string, that reference cannot participate in reactivity: a binding reading it does not update and keeps what it last rendered, and a <computed> reading it does not recompute, so nothing downstream of it moves either. The payload is left exactly as the endpoint sent it, and references into its conforming parts keep working.

This is distinct from absence. A property that is not there is absence, which renders as empty text and is a normal state for data that has not arrived. A property that is present but breaks its declared type is a broken contract: rather than rendering a value the declaration forbids, or discarding a response a page may be mid-way through using, the offending reference goes quiet and the violation is reported to the author.

A declaration with no type constrains nothing, and unknown is the type every value satisfies. A closed object shape states that an undeclared field is not there, so a reference to one is a violation; an open shape (...) says nothing about fields it does not name, which is what a payload that may grow should declare.

concat(value, value, …) converts accepted scalar values to text; it does not bypass a declaration's type check. If title is declared string but the payload currently holds the number 42, concat($results.0.title, '!') is inert like any other read of that reference. The payload remains available for inspection and conforming fields continue to update. See Functions.

Here is the effect across three responses when record.value.label is declared string. The resource always keeps the latest payload; each binding decides separately whether its read conforms:

<template component="x-record">
  <defs><data name="record" src="/api/record" type="object({ label: string, note: string })"></data></defs>
  <section>
    <output class="label">{$record.value.label}</output>
    <output class="note">{$record.value.note}</output>
  </section>
</template>
Response.label output.note output
{ label: 'first', note: 'a' }firsta
{ label: 42, note: 'b' }first (last valid result)b
{ label: 'third', note: 'c' }thirdc

The second response does not turn the whole resource back to its first payload. It changes record.value.note to b and leaves only the invalid label read without a new output. When the third response restores a string label, that binding updates again.

The outbound half is symmetrical. A body param of a synchronized write is serialized as request content rather than a query parameter, while a param consumed by the src template identifies the resource:

PATCH /api/posts/42
Content-Type: application/json

{ "title": "Draft title", "tags": ["design", "docs"], "clientRevision": 0 }

id filled the {id} placeholder and therefore does not repeat in the body; title, tags, and the sampled clientRevision are the body. The same rule decides both directions, so an author reading a declaration can tell what the request will look like without reading any JavaScript.

Writes: reactive data effects

A synchronized write is the outbound half of the same reactive resource model. A <data> with a modifying method and send="change" observes its from:value body parameters and sends their latest snapshot when they change. An expr:value parameter is sampled when another parameter causes the resource to prepare a request; changing only that expression's inputs does not queue a request. This mode represents state synchronization: each body is the latest state, never a request to perform a one-shot command. debounce controls the quiet period before the effect runs, so autosave does not mean one request per keystroke. The declaration lives in <defs> and exposes .dirty, .pending, .value, .error, and .ok.

<defs>
  <state name="post" type="object({ id: string })" value="{ id: '42' }"></state>
  <state name="draft" type="object({ title: string, tags: list(string) })"
         value="{ title: 'Draft title', tags: ['design', 'docs'] }"></state>
  <state name="revision" type="integer" value="0"></state>
  <event name="publish" type="object({ title: string, tags: list(string) })"></event>
  <!-- A modifying <data> is a reactive sink, not a hidden form. -->
  <data name="saveDraft"
        method="patch"
        src="/api/posts/{id}"
        send="change"
        debounce="500ms">
    <param name="id" from:value="post.id"></param>        <!-- resource identity -->
    <param name="title" from:value="draft.title"></param> <!-- reactive body field -->
    <param name="tags" from:value="draft.tags"></param>
    <param name="clientRevision" expr:value="revision"></param> <!-- sampled on a write; does not trigger one -->
  </data>

  <!-- An explicit command stays an event for the owner to handle. -->
  <handler name="requestPublish">
    <dispatch event="publish" expr:value="draft"></dispatch>
  </handler>
</defs>

<!-- These controls may already be inside a consumer-owned native <form>. -->
<input bind:value="draft.title">
<button type="button" on:click="requestPublish">Publish</button>

<p aria-live="polite" $if="saveDraft.pending">Saving…</p>

Initial connection samples the current body as that resource key's baseline and does not send a write. After the first acknowledged write, the baseline is the last acknowledged body for that key. A body-param change marks the resource dirty and queues a write. Every queued or in-flight write captures an immutable resolved resource identity and body snapshot. While a write is pending, further body changes for the same key replace one trailing queued snapshot with the latest one. A sent write is never cancelled because cancellation cannot undo a request the server may already have applied.

A parameter consumed by the src URI template identifies the resource. Changing it activates an independent baseline for the new key; queued and in-flight work keeps its captured old key and may finish there. Completion for an old key updates only that key's stored result. It must not change the active new key's .dirty, .pending, .value, .error, or .ok.

Disconnecting removes observations, pauses unsent queued work, and preserves its dirty latest snapshot and captured key. A sent write may finish, but it cannot schedule DOM work for a detached instance. On reconnect, preserved queues resume under their own keys. For the current key, the runtime samples the current body: an unseen key gets an initial no-send baseline; a known key remains dirty and resumes synchronization when its body differs from its last acknowledged baseline. While no instance is connected, nothing new is observed or scheduled.

A writable data resource does not own controls or create a rendered <form>. The same state can feed several resources with different endpoints, methods, parameter subsets, and send options. The visible controls may be standalone, inside a native form owned by the component, or inside a form that already contains the component instance.

Resource and control ownership

A <data> declaration lives in <defs> and owns request synchronization. It names parameters, selects a method, and serializes request data while producing no rendered or form-associated element. Visible controls keep their native form owner, validation, and submitter behavior. A component instance can therefore live inside an author-owned form without adding another submission scope.3

Async validity composes with native forms

Consider a slug field whose component declares a reactive <data> lookup to ask whether the current slug is reserved. The component's dependency graph keeps that lookup and its availability result current. The validation contract applies the result to the field's validity. If the component invocation lives inside a page author's native form, any native input produced by the component remains associated with that outer form after lowering, and the author-owned form decides whether and where the complete form is submitted. The same component may instead render a standalone control or no form control at all.

Commands remain explicit

Publishing, charging, sending email, and deleting are commands, not synchronization. They must not run because a dependency happened to change. Their commitment point remains author-owned: native form submission, an imperative controller call, or a component event dispatched from an internal button. An internal button may itself be a native submit button associated with the surrounding form; when it is not, its handler can dispatch the command intent without owning the endpoint. HTML Next therefore adds no <send> handler step and does not distort reactive state into a command trigger.

Open for review

Direction: reads and synchronized writes are <data> resources; form/control ownership and one-shot commands remain separate. The initial write policy is send="change" with optional debounce. Still open: additional send policies, retry and conflict policy, resource identity carried outside the URI template (including query-shaped identity), the declarative bridge from an async lookup into an author-owned control's validity, pagination accumulation, optimistic updates, revalidation, request headers/auth, and real-time push (SSE/WebSocket) where the server drives change without a param bump. Outside-world reactions (timers, subscriptions) are the JavaScript layer's job, see Lifecycle below.

Lifecycle

In a reactive component most of what framework lifecycle callbacks did is absorbed by the dependency graph, so HTML Next needs far fewer hooks, and names the ones it keeps after the platform's custom-element reactions, for least surprise.

What the graph already handles

  • Prop changes: when a prop changes, through a parent template's binding on the invocation or a framework passing a new value, the bindings, <computed>, and <data> that read it re-run automatically, so you never write attributeChangedCallback. A literal attribute on an invocation is the prop's initial configuration; the invocation is replaced when it lowers (see Lowering, provenance & hydration).
  • Fetch on mount, refetch on change: declare a <data>; it runs when its params resolve and again when they change. This is the connectedCallback fetch.
  • Initial and derived state: <state type value> declares a writable cell and its literal initial value; <computed from> declares read-only computed state. Initial focus is autofocus.

Declarative lifecycle events

For a reaction that is not a derivation, on:connect and on:disconnect run a handler when the component is connected or disconnected, mirroring connectedCallback/disconnectedCallback7 and firing again on reconnect. Because handlers are declarative, they set state or <dispatch> an event, with no imperative code. They are client-only: SSR renders the static tree, and hydration is what connects, so nothing lifecycle-driven runs on the server.

<defs>
  <state type="boolean" name="visible" value="false"></state>
  <handler name="show"><set name="visible" value="true"></set></handler>
  <handler name="hide"><set name="visible" value="false"></set></handler>
</defs>

<!-- lifecycle events run handlers; client-only (SSR never connects) -->
<section on:connect="show" on:disconnect="hide">…</section>
Web Components reactionHTML Next
constructornone, the <template component> declaration is the definition
connectedCallbackon:connect (declarative) · the JS behavior's connect (imperative, below)
disconnectedCallbackon:disconnect · the JS behavior's teardown
attributeChangedCallbacknot written: a prop changes through a binding or a framework, and reactivity re-runs its dependents
adoptedCallbackon:adopt (rare, cross-document moves)
form-associated callbacksthe forms & validation story (see Validation)
Connection defines the lifecycle boundary

on:connect fires each time an element joins a live, interactive document, including after a move and re-insertion. That boundary also fits SSR: the server produces static markup, then client hydration adopts and connects the node. Framework developers can read connect/disconnect as the platform-specific counterpart to mount/unmount, with repeated connection made explicit.

Imperative lifecycle is the JavaScript layer

Timers, subscriptions (SSE/WebSocket), IntersectionObserver, third-party libraries, imperative animation: these are genuinely imperative, with setup and teardown, and have no declarative form. They live in the reserved JavaScript layer (see The JavaScript Layer), at a later Level, where a component may attach an ES-module controller whose connect hook returns a disposer run on disconnect, the shape connectedCallback/disconnectedCallback and React/Svelte effects already established.8 The declarative layer stays free of lifecycle ceremony; the imperative layer is the only part with a real lifecycle, and it is opt-in.

The dependency graph

Every expression exposes the paths it reads, and every <param from:value> names a subscription, so the graph is known statically. This buys type-checkable expressions, predictable invalidation, ahead-of-time generation for any reactive framework, and a browser runtime that needs no dynamic code.

TargetLowers reactivity to
ReactuseState / useMemo / a resource hook
Vueref / computed / watch
Svelterunes ($state / $derived / $effect)
Browser runtimesignals (the TC39 Signals proposal as a candidate substrate)4

Relationship to TC39 Signals

Decision: HTML Next integrates with, but does not depend on, the TC39 Signals proposal.4 Signals are a good optional substrate for a browser runtime and a potential implementation interoperability point. They are not the authored markup model, a required implementation strategy, or a second source of observable semantics. A runtime may use native Signals, a small userland graph, or a target framework's reactivity so long as the HTML Next result is equivalent.

The TC39 proposal is intentionally lower-level than a UI system: it standardizes writable and computed cells plus dirtiness notification, while leaving effects, scheduling, DOM ownership, and automatic disposal to frameworks. HTML Next already has the missing owner: the connected template instance.

Connection owns observation

When an instance connects, the runtime wires only the bindings, computed values, data parameters, and controller effects that its currently instantiated tree can reach. When a conditional branch disappears, its observations disappear with it. When the instance disconnects, the runtime removes every live observation, cancels stale GET reads, pauses unsent write effects without losing their captured state, and runs controller-effect cleanup. Already-sent writes may finish, but cannot schedule detached DOM updates. A later reconnection wires the same declared graph again, resumes preserved writes under their captured resource keys, and evaluates it against the values then current.

Connection owns observation. Template expressions are pure and expose their dependencies directly; their connection to the DOM supplies the lifetime. State may remain on a still-referenced detached instance, but nothing observes it and no DOM work is scheduled while that instance is disconnected. Authors therefore write no watch, unwatch, or untrack; the DOM integration supplies the subscription bookkeeping and teardown that a low-level Signals implementation requires.

The minimum useful Signals subset

If an implementation chooses TC39 Signals, HTML Next needs the semantics of State, Computed, and Watcher: writable cells, lazy cached and glitch-free derivations, and a notification that lets the DOM layer schedule a flush. The DOM layer owns when to call watch/unwatch; authors do not.

Signals facilityHTML Next position
Signal.StateA suitable backing cell for declared <state> and changing props.
Signal.ComputedA suitable backing cell for <computed>; HTML Next additionally knows its dependency paths statically.
Signal.subtle.WatcherAn internal invalidation hook. One instance-owned scheduler batches affected DOM work into a microtask and unwatches it on disconnect.
built-in effects and schedulingNot supplied by TC39 and not requested from it. The host.effect DOM API supplies lifecycle, scheduling, and cleanup.
Signal.subtle.untrack, graph introspection, subclassing, custom equalityNot required by HTML Next's authored model. An implementation may use them, but markup semantics never expose them.
Signals can advance independently

The TC39 proposal is currently Stage 1 and explicitly treats DOM integration as separate future work. HTML Next should provide that integration evidence without freezing itself to the proposal's present class names or subtle surface. If Signals advance, a native runtime can adopt them underneath this contract; if they change or do not ship, the markup and its lifecycle semantics remain intact.

Element reference

<state> · <computed> local reactive values

Attributes
<state name type? value?> · <computed name from>
Semantics
mutable state is written by <set>, bind:, or a controller through host.state; computed state is read-only, pure, and recomputed from dependencies.
Level
L1

<context> read-only descendant state

Attributes
name: ancestor state cell · from: ancestor component tag · as?: local name (defaults to name)
Semantics
Reads the nearest matching ancestor's state reactively; cannot be written through <set> or bind:. The ancestor's <state> needs no marker.
Level
L1 · could move to Level 2

<data> · <param> declared reactive resource

Attributes
name, src, method?, type?, send?, debounce?, poll?, enctype? · <param name from:value> or <param name expr:value>
Methods
get, query, post, put, patch, and delete; parsed ASCII-case-insensitively and mapped to uppercase HTTP methods before request construction.
Exposes
.dirty, .pending, .value, .error, .ok
Reads
GET runs on connection and when a from:value param changes; expr:value params are sampled for each resulting request without triggering one. Stale reads are cancelled and results are keyed by resolved params.
Writes
send="change" observes body params after the initial no-send baseline; debounce delays and coalesces the effect. Queued work captures an immutable identity and body, and sent writes are not cancelled.
Identity
{name} in src is an RFC 6570 path param. Each key has an independent last-acknowledged baseline and result state; old-key completion cannot update the active key.
Disconnect
Observation stops. Unsent writes pause without losing their dirty snapshot; sent writes may finish but cannot schedule detached DOM work.
Level
L1 (proposed)

Sources

  1. htmx (hx-get/hx-trigger/hx-target) and its Locality of Behaviour essay.
  2. IETF, RFC 6570: URI Template (the {name} placeholder syntax).
  3. WHATWG HTML, form submission (the native model for constructing an entry list and selecting an endpoint, method, and encoding).
  4. TC39, Signals proposal (Stage 1): a candidate reactive substrate whose State, Computed, and Watcher primitives deliberately leave effects, scheduling, DOM ownership, automatic disposal, and future HTML/DOM integration to a higher layer.
  5. Signals and fine-grained reactivity as a declared, statically-analyzable dependency graph: Solid signals and createMemo, Angular signals, with Knockout observables as the historical ancestor (and Vue ref/computed, Preact signals). Contrast: RxJS models push streams, not value cells.
  6. Solid createResource: a resource whose fetch re-runs when its source changes, exposing loading and error, the exact surface <data> exposes as .pending/.error/.value/.ok. TanStack Query and Vue Query key a cache by request params, matching requests keyed by resolved params here.
  7. WHATWG HTML, custom element reactions (connectedCallback/disconnectedCallback, the naming precedent for on:connect/on:disconnect).
  8. The return-a-disposer teardown shape: Solid onCleanup and React effect cleanup.
  9. WHATWG Fetch, methods: method normalization uppercases DELETE, GET, HEAD, OPTIONS, POST, and PUT; it does not include PATCH.
  10. IETF, RFC 10008: The HTTP QUERY Method: a safe, idempotent method that carries request content.
  11. WHATWG HTML, issue #12594: the active proposal to support method="query" and formmethod="query" in HTML forms.

Declarative HTML Components Level 1: Reactivity & Data. Unofficial Editor's Draft · Stage 0. Published in the HTML Next collection by the Next Web Working Group; not a W3C or WHATWG deliverable. Version history.