Skip to content

Features

Ahmed edited this page Nov 10, 2025 · 5 revisions

Detailed explanations and practical examples for all htswap features.

Underlying Features

Dynamic Content Swapping

When a user clicks an anchor or submits a form, htswap intercepts the event, fetches new content, and replaces the target element without reloading the entire page.

<body data-htswap>
  <nav>
    <a href="/products">Products</a>
    <a href="/about">About</a>
  </nav>
  <main id="content">
    <h1>Home</h1>
    <p>Welcome to our store</p>
  </main>
</body>

Clicking "Products" swaps only the page content—navigation stays intact and there's no reload flash.


Progressive Enhancement

Add a single data-htswap attribute to individual elements or parent containers. Children automatically inherit the behavior unless they have their own data-htswap attribute.

Opt in specific links:

<nav>
  <a href="/products" data-htswap="#content">Products</a>
  <a href="/blog">Blog</a> <!-- This link works normally -->
</nav>
<main id="content">
  <!-- Product list -->
</main>

Or opt in entire sections:

<div data-htswap="#content">
  <nav>
    <a href="/products">Products</a>
    <a href="/services">Services</a>
    <a href="/contact">Contact</a>
  </nav>
</div>
<main id="content">
  <!-- All nav links will swap #content -->
</main>

It's also possible to opt-out certain links using data-htlocked:

<div data-htswap="#content">
  <nav>
    <a href="/products">Products</a>
    <a href="/about">About</a>
    <a href="/external-site" data-htlocked target="_blank"> <!-- This link works normally, while the others will be swapped -->
      Partner Site
    </a>
  </nav>
</div>
<main id="content">
  <!-- Content -->
</main>

Graceful Degradation

Since htswap progressively enhances standard HTML elements, users without JavaScript still get fully functional links and forms—they just experience full page reloads instead of dynamic swaps. Your <a href="/page"> still navigates and your <form action="/submit"> still submits, even with JS disabled.


Smart Defaults

Ctrl+Click Bypass: holding Ctrl (or Cmd on Mac) while clicking bypasses htswap for normal navigation. Users can Ctrl+Click to open in new tab, Shift+Click to open in new window, or right-click to access the context menu. This ensures htswap doesn't interfere with standard browser navigation patterns.

Form Duplicate Prevention: when a form is submitted, htswap disables the submit button until the request completes, preventing users from accidentally submitting the same form multiple times. The button becomes disabled immediately on submit, remains disabled while the request processes, and becomes enabled again after the response returns.

Request Header: htswap sends an x-htswap header with each request containing the selector being swapped. The server can use this to determine whether to return a full page or partial content.

Timeout Control: htswap cancels requests that take longer than 4 seconds, preventing indefinite waits. When timeout occurs, the request is aborted, aria-busy is set back to false, form submit buttons are re-enabled, and no content swap occurs.

Automatic Rebinding: after content is swapped, new anchors and forms in the swapped content are automatically activated with htswap (if data-htswap is used) without requiring manual re-initialization.


Core Features

Preserved Inlines

When content is swapped, htswap re-creates script tags to ensure they execute, and inline styles are maintained in the swapped content.

<!-- Server returns this -->
<div id="widget">
  <style>
    .widget-box { border: 2px solid blue; }
  </style>
  <div class="widget-box">
    <p>Dynamic widget loaded</p>
  </div>
  <script>
    console.log('Widget initialized at', new Date());
    // Initialize widget behavior
  </script>
</div>

The style applies correctly and the script runs after the swap completes.


Working Head Elements

When the server response includes a <head> tag, htswap updates the document title, adds new stylesheets and scripts, removes old ones (in replace mode), and prevents duplicate loading of resources.

<!-- Page 1 response -->
<html>
  <head>
    <title>Product Catalog</title>
    <link rel="stylesheet" href="/css/catalog.css">
    <script src="/js/catalog.js"></script>
  </head>
  <body>
    <main id="content">
      <!-- Product listing -->
    </main>
  </body>
</html>

<!-- Page 2 response -->
<html>
  <head>
    <title>Shopping Cart</title>
    <link rel="stylesheet" href="/css/cart.css">
    <script src="/js/cart.js"></script>
  </head>
  <body>
    <main id="content">
      <!-- Cart contents -->
    </main>
  </body>
</html>

Navigating from Page 1 to Page 2: title changes to "Shopping Cart", catalog.css is removed and cart.css is added, catalog.js is removed and cart.js is loaded.


History Support

htswap uses the History API to track navigation. Each swap adds a new history entry by default, allowing users to navigate backward and forward through their browsing session.

<body data-htswap>
  <nav>
    <a href="/page1">Page 1</a>
    <a href="/page2">Page 2</a>
    <a href="/page3">Page 3</a>
  </nav>
  <main id="content">
    <h1>Home</h1>
  </main>
</body>

Click "Page 1" → content swaps, history entry added. Click "Page 2" → content swaps, history entry added. Click back → Page 1 content restored without reload. Click back again → Home content restored without reload.


Fragment Support

htswap preserves URL fragments during navigation and automatically scrolls to the target element when present.

In-page fragments:

<body data-htswap>
  <nav>
    <a href="#section1">Section 1</a>
    <a href="#section2">Section 2</a>
  </nav>
  <main>
    <section id="section1">
      <h2>Section 1</h2>
      <!-- Content -->
    </section>
    <section id="section2">
      <h2>Section 2</h2>
      <!-- Content -->
    </section>
  </main>
</body>

Cross-page fragments:

<body data-htswap>
  <nav>
    <a href="/docs#installation">Installation Docs</a>
    <a href="/docs#usage">Usage Guide</a>
  </nav>
  <main id="content">
    <!-- Content -->
  </main>
</body>

Clicking these links navigates to /docs and scrolls to the respective section.


Maintained Scroll Position

When navigating forward, htswap saves the current scroll position in history state. When navigating backward, it restores that saved position. Scroll down a long article, click a link, click back—both content and scroll position are restored.


Redirect Handling

When the server responds with a redirect (302, 303, 301), htswap follows it and uses the final URL for swapping and history updates.

Post-Redirect-Get example:

<form action="/create-item" method="POST" data-htswap="#items">
  <input type="text" name="item_name" placeholder="Item name">
  <button type="submit">Create</button>
</form>
<div id="items">
  <!-- Existing items -->
</div>

Server flow: POST to /create-item → server processes and responds with 303 redirect to /items → htswap follows redirect and fetches /items → content from /items swaps into #items → browser URL updates to /items. This prevents duplicate submissions if the user refreshes the page.


Extra Features

Loading States

Before fetching, htswap sets aria-busy="true" on the document body and all target elements. After the swap completes, it's set to "false". This enables CSS-based loading indicators.

<style>
  [aria-busy="true"] {
    opacity: 0.6;
    pointer-events: none;
    position: relative;
  }

  [aria-busy="true"]::after {
    content: "Loading...";
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: white;
    padding: 1rem;
    border-radius: 4px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
  }
</style>

<body data-htswap>
  <nav>
    <a href="/reports">Generate Report</a>
  </nav>
  <main id="content">
    <!-- Content -->
  </main>
</body>

When the user clicks "Generate Report", the content area shows the loading overlay until the swap completes. For forms, the submit button is automatically disabled during the request to prevent duplicate submissions.


Multiple Targets

Specify multiple selectors in data-htswap, separated by commas. htswap will look for matching elements in the server response and swap each one.

E-commerce cart example:

<body>
  <header>
    <div id="cart-badge">
      <span>3 items</span>
    </div>
  </header>

  <main id="product-list">
    <div class="product">
      <h3>Laptop</h3>
      <form action="/add-to-cart" method="POST"
            data-htswap="#product-list, #cart-badge">
        <input type="hidden" name="product_id" value="42">
        <button type="submit">Add to Cart</button>
      </form>
    </div>
  </main>
</body>

When the user adds an item, both the product list (to show updated stock/status) and the cart badge (to show new item count) update simultaneously.

Dashboard refresh example:

<body>
  <a href="/dashboard/refresh"
     data-htswap="#stats, #notifications, #recent-activity">
    Refresh Dashboard
  </a>

  <div id="stats">
    <p>Active users: 1,234</p>
  </div>

  <div id="notifications">
    <p>5 new notifications</p>
  </div>

  <div id="recent-activity">
    <ul>
      <li>User signed up</li>
    </ul>
  </div>
</body>

One click refreshes all three sections at once.


History Modes

Control how swaps interact with browser history using data-hthistory.

Available modes:

  • push (default): Adds new history entry
  • replace: Updates current history entry without adding new one
  • none: Swaps content without changing URL

Filtering with replace mode:

<div data-htswap="#results" data-hthistory="replace">
  <nav>
    <a href="/products?sort=price-asc">Price: Low to High</a>
    <a href="/products?sort=price-desc">Price: High to Low</a>
    <a href="/products?sort=name">Alphabetical</a>
  </nav>
</div>

<div id="results">
  <!-- Product list -->
</div>

Users clicking different sort options update the URL to reflect the current sort, but clicking back doesn't step through each sort choice—it goes back to the previous page entirely.

Modal with none mode:

<body data-htswap>
  <a href="/settings-modal"
     data-htswap="#modal"
     data-hthistory="none">
    Open Settings
  </a>

  <div id="modal" hidden>
    <!-- Modal content will load here -->
  </div>

  <main id="content">
    <!-- Main page content -->
  </main>
</body>

Opening the settings modal loads its content without changing the URL. Users can close the modal and stay on the same page.


Head Modes

Control how head elements are managed using data-hthead.

Available modes:

  • replace (default): Removes old head elements not in new response, adds new ones
  • append: Keeps existing head elements, only adds new ones

Replace mode (default):

<body data-htswap data-hthead="replace">
  <nav>
    <a href="/tools/calculator">Calculator</a>
    <a href="/tools/converter">Unit Converter</a>
  </nav>
  <main id="content">
    <!-- Content -->
  </main>
</body>

Each tool page can have its own specific CSS/JS. When navigating from Calculator to Converter, Calculator's dependencies are removed and Converter's are loaded.

Append mode:

<body data-htswap data-hthead="append">
  <nav>
    <a href="/map">Store Locations</a>
    <a href="/3d-viewer">Product 3D View</a>
  </nav>
  <main id="content">
    <!-- Content -->
  </main>
</body>

With append mode, visiting the map page loads its mapping library, then visiting the 3D viewer adds the 3D library. Both remain loaded, useful for shared dependencies.

Script re-evaluation:

Scripts can be marked with data-htreeval to force re-execution on every swap:

<head>
  <script src="/analytics.js" data-htreeval></script>
  <script src="/core.js"></script>
</head>

/analytics.js runs on every page swap (useful for page view tracking), while /core.js only loads once and stays cached.


Auto Targeting

When data-htswap="auto" is used, htswap looks at all elements with IDs in the server response and swaps them with client elements of matching IDs.

<body data-htswap="auto">
  <header id="site-header">
    <h1>Dashboard</h1>
    <p>Last updated: 10:00 AM</p>
  </header>

  <main id="main-content">
    <p>Content here</p>
  </main>

  <footer id="site-footer">
    <p>Version 1.0</p>
  </footer>

  <a href="/sync-ui">Sync Interface</a>
</body>

Server response for /sync-ui:

<header id="site-header">
  <h1>Dashboard</h1>
  <p>Last updated: 14:32 PM</p>
</header>

<footer id="site-footer">
  <p>Version 1.1</p>
</footer>

Only the header and footer swap—main content is untouched because the server didn't include #main-content. This is useful when different pages update different combinations of sections, you want the server to control which sections update, or you don't want to list every possible target.


Target Aliases

Use #server-selector->#client-selector to tell htswap to look for one selector in the server response but swap it into a different selector on the client.

Generic server responses:

<!-- Client page -->
<aside id="shopping-cart">
  <h2>Your Cart</h2>
  <ul>
    <li>Item 1</li>
  </ul>
</aside>

<form action="/add-item" method="POST"
      data-htswap="#generic-cart->#shopping-cart">
  <input type="hidden" name="item_id" value="789">
  <button type="submit">Add Item</button>
</form>

The server always returns a generic #generic-cart element, but it gets swapped into #shopping-cart on this page. This allows you to reuse server templates across different page layouts.

Multiple aliases:

<div id="product-grid"></div>
<div id="filters-panel"></div>

<form action="/filter-products"
      data-htswap="#results->#product-grid, #sidebar->#filters-panel">
  <select name="category">
    <option>Electronics</option>
  </select>
  <button type="submit">Apply</button>
</form>

Server returns #results and #sidebar, but they're swapped into #product-grid and #filters-panel respectively.


Swap Modes

Control how content is inserted into target elements by adding @mode after the selector: #selector@mode

Available modes:

  • innerHTML (default): Replaces inner content of target
  • outerHTML: Replaces entire target element
  • beforebegin: Inserts before the target element
  • afterbegin: Inserts as first child of target
  • beforeend: Inserts as last child of target
  • afterend: Inserts after the target element
  • remove: Removes the target element

Load more comments:

<div id="comments">
  <article class="comment">First comment</article>
  <article class="comment">Second comment</article>
</div>

<a href="/comments/page-2"
   data-htswap="#comments@beforeend">
  Load More Comments
</a>

New comments are appended to the existing list instead of replacing it.

Prepend notifications:

<ul id="notifications">
  <li>Welcome to the app</li>
</ul>

<form action="/check-notifications"
      data-htswap="#notifications@afterbegin">
  <button type="submit">Check Notifications</button>
</form>

New notifications appear at the top of the list.

Remove on delete:

<div id="item-123" class="list-item">
  <p>Task: Buy groceries</p>
  <form action="/delete-item" method="POST"
        data-htswap="#item-123@remove"
        data-hthistory="none">
    <input type="hidden" name="id" value="123">
    <button type="submit">Delete</button>
  </form>
</div>

When the user clicks delete, the item is removed from the DOM.

Replace entire element:

<div id="user-card" class="card">
  <h3>John Doe</h3>
  <p>Member since 2020</p>
  <a href="/users/john/vip"
     data-htswap="#user-card@outerHTML">
    Upgrade to VIP
  </a>
</div>

Server response:

<div id="user-card" class="card vip-card">
  <h3>John Doe</h3>
  <p>VIP Member since 2024</p>
  <span class="badge">VIP</span>
</div>

The entire card element is replaced, including its class name.


Note currently and likely until v1, this page is AI generated from tests that cover all the feature base.