Partial Page Replacement

Please note: this proposal is still being drafted and is not officially launched.

Authors
Alexander Petros (contact@alexpetros.com)
Carson Gross (carson@bigsky.software)
Date Created
-
Last Updated
June 12, 2026
Issue Tracker
-
Status
-
Table of Contents

Summary

Allow HTML to specify HTTP requests that replace part of the page.

Proposal 3/3 in the Triptych Proposals.

Goals

Proposed Changes

Update the target attribute on the <form>to accept a CSS selector. If the CSS selector matches an element in the document, the body content from the response should replace the element in the page.

The target attribute would thus accept the following possible values, in order of preference:

  1. Keywords (_self, _blank, etc.)
  2. An iFrame with a matching name attribute
  3. A valid CSS selector that matches an existing DOM element.

In conjunction with Button Actions, add target to the <button> element as well. It behaves identically.

Sample Usage

"Load More" Button

A common pattern on websites is to show the user a finite list of content and then give them the option to load more after they've scrolled through it all. This might be a news feed, list of search results, or a social media timeline.This structure is based on the ARIA Authoring Practice Guide for social media feeds.
<section role="feed">
  <h2>News Feed</h2>
  <article>Speed Up Your Computer With This One Weird Trick</article>
  <article>24 Signs You're a Die-Hard Houston Astros Fan</article>
  <article>Is Smoking The New Drinking?</article>
  <button class="load" target=".load" action="/articles/1">Load More</button>
</section>

If the user would like to read more content, they can click the "Load More" button. The browser will issue a request to `/articles/1`, and the server could return a response like this: Note how the response contains both the current "state" of the application (the articles are being displayed) and actions that the user can take (load more articles), represented in HTML. This is Hypertext as the Engine of Application State (HATEOAS).

  <article>We Could All Learn From This Baby's Inspirational First Word</article>
  <article>Move Over Espresso Martini, The Aperol Spritz is Here</article>
  <button class="load" target=".load" action="/articles/2">Load More</button>

When the browser receives that response, it will be parsed as HTML and inserted into the DOM. The button that triggered the request had target=".load" on it, and the first DOM element to match that CSS selector is itself. So the browser will replace the button with the contents of the server's response, resulting in the following HTML:

<section role="feed">
  <h2>News Feed</h2>
  <article>Speed Up Your Computer With This One Weird Trick</article>
  <article>24 Signs You're a Die-Hard Houston Astros Fan</article>
  <article>Is Smoking The New Drinking?</article>
  <article>We Could All Learn From This Baby's Inspirational First Word</article>
  <article>Move Over Espresso Martini, The Aperol Spritz is Here</article>
  <button class="load" target=".load" action="/articles/2">Load More</button>
</section>
Note how the server doesn't just return more content, it returns a new button as well, to replace the old one. The new button has a new URL to go with it—because the server is responding to a request at /articles/1, it knows that the next button the user should see is one to fetch the articles at /articles/2.

This pattern easily generalizes to any kind of network-based content update that targets a subset of the page. If the content is timely and fast-updating, a "refresh" button can delete the current list and load the most current one; if the webpage is geared towards discovery, a "randomize" button can fetch a different batch. This pattern is essentially a manual version of "Infinite Scroll," where new data loads when the user scrolls to the bottom of the page, but the user has to manually click the button to load more content. Why not extend Triptych to support infinite scroll? Because HTML doesn't have any readily-available semantics for changing the event that triggers an element's action; the only way to activate a button's action is with a click (or click-equivalent). Triptych focuses on extending existing metaphors in HTML, in order to limit the extensions to largely non-controversial behavior.

"The Missing Mechanic: Behavioral Affordances as the Limiting Factor in Generalizing HTML Controls" (Petros et al.) identifies this limitation and investigates possibly ways to remedy it.

Edit Table Items

Imagine a web service that maintains a list of of its current users and gives an administrator the option to remove them. Here's a simple, non-functioning version of that feature:

User Email
Alex alex@example.com
Grace grace@example.com
Pat pat@example.com

This table is described with the following HTML:

<table class="users">
  <tr>
    <th>User</th>
    <th>Email</th>
    <th></th>
  </tr>
  <tr class="user-1">
    <td>Alex</td>
    <td>alex@example.com</td>
    <td><button target=".user-1" action="/users/1" method="DELETE">Delete</button></td>
  </tr>
  <tr class="user-2">
    <td>Grace</td>
    <td>grace@example.com</td>
    <td><button target=".user-2" action="/users/2" method="DELETE">Delete</button></td>
  </tr>
  <tr class="user-3">
    <td>Pat</td>
    <td>pat@example.com</td>
    <td><button target=".user-3" action="/users/3" method="DELETE">Delete</button></td>
  </tr>
</table>

Note that the rows all have the same structure. Repeating row structures like this are very common and trivial to implement with both traditional HTML templates and JSX. Each user has a name, email, and unique ID. The name and email are displayed as content, and the ID uniquely identifies each user at a resource URL. The IDs are sequential integers here to ease readability, but they could just as easily be UUIDs.

<tr class="user-1">
  <td>Alex</td>
  <td>alex@example.com</td>
  <td>
    <button target=".user-1" action="/users/1" method="DELETE">Delete</button>
  </td>
</tr>

The user "Alex" can be found at the URL /users/1, "Grace" is at /users/2, and so on. That URL is used in each button's action attribute, such as in the first one. This example uses all three Triptych proposals together to illustrate how the proposal work together to make HTML expressive and concise, but this pattern can also be accomplished without the other two proposals, using form wrappers and ad-hoc URI semantics.

<button target=".user-1" action="/users/1" method="DELETE">Delete</button>
When the user clicks that button, the browser issues a DELETE request to /users/1. The server then responds with HTML that describes what happened and the browser will replace the DOM node described by .user-1 with the response body. In this case that's the whole row.

This is an extremely expressive pattern. In htmx, this would be an outerHTML swap. It's not, however, the default replacement stragey in htmx, which defaults to an innerHTML replacement. Our experience is that outerHTML should be the default and the JS ecosystem has also evolved in that direction. For example, outerHTML is the only type of swap supported by htmz, a minimalist DOM replacement library whose overloading of the target attribute inspired the interface of this proposal. They argue: replacement is a more powerful option. With replacement, you can replace, delete (replace with nothing), and insert-into (replace with the same container as original). We agree. For a successful delete, the response body can simply be empty, deleting the target from the page; a more complicated pattern might return a confirmation message of some kind. If the delete failed, the server can return HTML that reflects that error. All of these work "out of the box" with existing CSS transitions, because this mechanism replaces DOM elements exactly the same way that JavaScript would.

Technical Specification

Core Behavior

Requests with a CSS target have their responses inserted into the page, replacing the first DOM element that matches the CSS target. This exact pattern is already supported by browsers, via JavaScript. Suppose the CSS target corresponds to a DOM node targetNode and the HTML response is instantiated as responseNodes. This feature works just as if targetNode.replaceWith(responseNodes) has been called. As the name implies, responseNodes could contain multiple nodes and that would work just fine. If more than one DOM node matches the CSS selector, the browser should target the first one. This is also how Document.querySelector() works.

Partial page replacement does not create a new navigation history entry. "Forward" and "Back" navigations should treat partial page updates the same way they would treat ephemeral updates to the DOM that had been made with JavaScript. Loosely speaking, this means that, when going back to a page, the browser will attempt to show the final state of the page before it was navigated away from. This tends to work best with pages that are mostly static and document-based, rather than single-page applications.

By default, network requests with a partial page target should trigger the network progress bar. The technical difficulty of adding a progress bar for network requests that aren't full page navigations is not clear to the authors. Although it is included int he main recommendations, it could be omitted at the start if it introduced significant complexity. JavaScript libraries do not have access to this feature and it provides useful information to the web user without additional effort from the web author.Although we believe it to be the appropriate default, we expect that web authors might want the option to disable the progress bar—this is included in the Alternatives and Additions section.

Request Headers

The Sec-Fetch-Mode header should be set to replacement, a new value. The navigate value is not appropriate as navigation is not taking place. cors could be re-used but why not add a more-specific one?

Sec-Fetch-Mode: replacement

The Sec-Fetch-Dest header should be enhanced to include the replacement target of the of the request. In addition to the security considerations, this also allows authors to have the same URL serve slightly different content depending on the target; the author might design a profile page to load the page with the full website chrome, but serve only the profile content after being edited. In keeping with how a forward slash indicates a subcategory in MIME types, the same can be done for document in Sec-Fetch-Dest.

Sec-Fetch-Dest: document
Sec-Fetch-Dest: document/.target
Sec-Fetch-Dest: document/section>div.profile

If using the existing document value raises any concerns, a substitute like replacement would be perfectly fine.

Sec-Fetch-Dest: replacement/.target
jec-Fetch-Dest: replacement/section>div.profile

Events

JavaScript is not a requirement to use partial page replacement, but having an event lifecycle allows further customization for those who want it. Events also make it possible for library authors to extend the HTML primitives, rather than replacing them.

We propose the following events to launch with Partial Page Replacement: The naming structure attempts to mirror the structure of the Navigation API. The names beforerequest and afterrequest would also be reasonable alternatives.

Event Target Phase preventDefault action
request Request-issuing element Before the request is issued Aborts the request and the swap
requestsuccess Request-issuing element After the request is issued, but before it is parsed Aborts the swap
requesterror Request-issuing element After the request is issued, if it failed None
beforeswap Replacement target Before the swap Aborts the swap
afterswap Parent of the (now-replaced) replacement target After the swap completes None

Only five events are proposed for the first version of this feature, far more limited than what can be found in JavaScript libraries. This is done to limit scope. It is easy to add events and impossible to remove them; this proposal errs on the side of doing less. It will be easier to determine what additional events would be useful once the feature has real-world usage.

request event

Property Name Value
action The URL target of the request
method The HTTP method of the request
replacementtarget The CSS selector for the replacement target
formData Any form data associated with the request
abortSignal An AbortSignal object for the request

Fires before the network request is issued.

Calling preventDefault() aborts the cycle; no request is sent, no further events fire.

requestsuccess event

Property Name Value
action The URL target of the request
method The HTTP method of the request
replacementtarget The CSS selector for the replacement target
response A ReadableStream of the response

Fires when the request completes successfully, but before the response is parsed. The event's details contains an object with the following properties:

Calling preventDefault() ends the cycle before the body content is parsed; beforeswap does not fire.

Keep in mind that response codes do not impact whether the request completed successfully. A request that finishes with a 401 UNAUTHORIZED response will still fire this event.

requesterror event

An ErrorEvent that fires when the request does not complete successfully. Following navigateerror, the event includes an error property with a JavaScript value representing the error associated with the event. That error could be one of the following:

Error Meaning
NetworkError The request failed at the transport layer
AbortError The request was aborted via its AbortSignal
TimeoutError The request timed out
NotFoundError The CSS selector matched no element

Calling preventDefault() has no effect; a swap will never take place if the request fails.

beforeswap event

Property Name Value
action The URL target of the request
method The HTTP method of the request
replacementtarget The CSS selector for the replacement target
newelements NodeList of the elements to be inserted

Fires on the replacement target after the response body is parsed but before the target is replaced. The event's details contains an object with the following properties:

Calling preventDefault() aborts the swap; afterswap does not fire. An author who wants to make swap decisions based on a response header or content type can do so here.

afterswap event

Property Name Value
action The URL target of the request
method The HTTP method of the request
replacementtarget The CSS selector for the replacement target
newelements NodeList of the elements to be inserted
replacedelement The replaced element, now removed from the DOM

Fires on the parent of the former target after the swap completes. The event's details contains an object with the following properties:

Note that newelements will be empty if the response body was empty.

CORS

Partial page replacements are subject to CORS. All cross-origin requests require a preflight and there are no so-called "simple" cross-origin requests for partial page replacements. Same-origin requests proceed as normal.

This can be accomplished without changes to fetch spec, by adding a non-safelisted header to the request.

Fortunately, Paul Kinlan from the Chrome team has put together a demonstration of "htmx-style attributes," implemented using the new steaming-replacement functions from the second part of the Chrome proposal. h/t Oliver Williams for sharing this. In the excerpt below, a "load news" button inserts the response value, as a child of the section with id="out".

<section>
  <button
    data-get="/03/fragment/news"
    data-target="#out"
    data-swap="inner"
  >load news</button>
</section>

<h3>Target #out</h3>
<section id="out"></section>

Kinan's demonstration is additional evidence of the demand for a declarative HTML API to access this exact functionality. Although customizing the swap mechanism is out of scope for this proposal, which takes the simplifying position that a replacement (a.k.a "outer") swap can cover most use-cases. The Chrome proposal brings new steaming APIs to the browser, via JavaScript; this proposal builds on their work by adding an interface for those APIs to HTML.

Alternatives and Additions

_self keyword

The target attribute has pre-existing keywords that refer to relative browsing contexts. The addition of a _self keyword, which target the item that made the request, would significantly simplify the targets by removing the need for IDs in many common cases.

Recall the delete button from the editable table earlier:

<button class="load" target=".load" action="/articles/1">Load More</button>

With the _this keyword, the code becomes trivial.

<button target="_this" action="/articles/1">Load More</button>

Although this is a minor convenience in isolation, it becomes a dramatic simpliciation for the server when applied to items that are generated progmatically from a list, because it removes the need to create iterative target/id pairs for each item. Without it, there are three dynamic values in the following button:

<button
  class=".article-1"
  target=".article-1"
  action="/articles/1/upvote"
  method="PUT">
  Upvote
</button>
Instead, we could just have one.
<button
  target="_self"
  action="/articles/1/upvote"
  method="PUT">
  Upvote
</button>

Control response placement with headers

TODO

CSS Classes

TODO

Web authors will want to customize the view of inflight request

Allow disabling the progress bar?

Control Headers

TODO

Headers that let the server control the response location

Oob swaps

New Attribute For CSS Targets

Don't re-use the target attribute, instead add a new one specifically for CSS targets.

Footnotes