---
title: Adapters
---

# Adapters

An adapter controls how placeholders and patches are encoded in the HTML, in
the markup a specific client (a library, the browser, a CDN) recognises and
acts on.

Vincle ships five built-in adapters. Import from `@vincle/flow/adapters`:

```ts
import {
  TurboAdapter, // Turbo Streams: <turbo-frame> / <turbo-stream>, morph on Turbo >= 8
  HtmxAdapter, // HTMX: hx-get / hx-swap-oob, morph on htmx >= 4
  NativeAdapter, // <template data-for> + inline polyfill, all 5 positions
  WebPlatformAdapter, // WICG declarative partial updates, native support required
  EsiAdapter, // CDN edge composition via esi:include
} from "@vincle/flow/adapters";
```

## Choosing an adapter

<table>
  <thead>
    <tr>
      <th>Adapter</th>
      <th>Client runtime</th>
      <th>Streaming</th>
      <th>Merge types</th>
      <th>Best for</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <strong>NativeAdapter</strong>
      </td>
      <td>~1.5 kB polyfill, gzipped (auto-injected)</td>
      <td>
        <CheckIcon />
      </td>
      <td>all 5 positions</td>
      <td>General purpose, no extra JS</td>
    </tr>
    <tr>
      <td>
        <strong>TurboAdapter</strong>
      </td>
      <td>Hotwire Turbo</td>
      <td>
        <CheckIcon />
      </td>
      <td>all 6</td>
      <td>Rails / Hotwire stack</td>
    </tr>
    <tr>
      <td>
        <strong>HtmxAdapter</strong>
      </td>
      <td>HTMX</td>
      <td>
        <CheckIcon />
      </td>
      <td>all 6</td>
      <td>Django / Laravel / any backend</td>
    </tr>
    <tr>
      <td>
        <strong>WebPlatformAdapter</strong>
      </td>
      <td>None (native browser support required)</td>
      <td>
        <CheckIcon />
      </td>
      <td>replace only</td>
      <td>Browsers with declarative partial updates</td>
    </tr>
    <tr>
      <td>
        <strong>EsiAdapter</strong>
      </td>
      <td>None (CDN-level)</td>
      <td>
        <XIcon />
      </td>
      <td>replace only</td>
      <td>Varnish / CDN edge composition</td>
    </tr>
  </tbody>
</table>

`morph` needs a diffing client (Turbo, HTMX); an adapter refuses a merge type it
does not support.

### NativeAdapter

Recommended for most applications. It uses the
[WICG Declarative Partial Updates](https://github.com/WICG/declarative-partial-updates)
format, with a polyfill **injected automatically** when fragments or active
`<template src>` / `<template for>` elements are present. Inert templates and
pages without deferred content do not require it.

Use Native for both SSG and streaming while native browser support is incomplete.
Native protects templates with `data-for`, so browsers that implement patching
but not `src` cannot consume them before the polyfill. It handles both comment
and processing-instruction markers.

SSG loads the emitted fragment files. Streaming consumes complete inline patch
templates and also handles external includes arriving in the page. External
responses are buffered; failed requests preserve fallback content.

Once target browsers support both WICG patching and Fragment Include, switch the
render call to `WebPlatformAdapter`. Keep the same JSX and generated URLs and
use `merge="replace"`, which both adapters support. WebPlatform emits standard
`for` attributes and adds no JavaScript.

### WebPlatformAdapter

The standard WICG format with no polyfill: the initial HTML holds a named range with
fallback content, and a matching `<template for="…">` later in the response
replaces it as the browser parses it.

```html
<?start name="comments">Loading comments…<?end>
<!-- Later in the same HTML stream: -->
<template for="comments"><p>First comment</p></template>
```

For a static-mode `<Defer>`, Vincle instead emits an external include:

```html
<?start name="comments">Loading comments…<?end>
<template for="comments" src="/fragments/comments.html"></template>
```

`emitFragments` writes raw HTML at that URL, with no `<template>` wrapper.
This follows the experimental
[Fragment Include proposal](https://github.com/WICG/declarative-partial-updates/blob/main/fragment-include-explainer.md).
You can also render `<template src="/partials/header.html" for="" />` directly
in JSX for an in-place include. Importing Flow also enables `buffer`, `sanitize`, `crossorigin`,
and `referrerpolicy` as well. Without native support, these includes remain inert.
The Native polyfill supports targeted and in-place includes, but buffers external
responses and does not implement the proposal’s sanitization API.

It needs native browser support; Google's
[Declarative partial updates article](https://developer.chrome.com/docs/web-platform/declarative-partial-updates)
covers the motivation and browser support. For the polyfill and the other merge
positions, use `NativeAdapter`.

### TurboAdapter

Encodes placeholders as `<turbo-frame>` elements and patches as
`<turbo-stream>` elements, for [Hotwire Turbo](https://turbo.hotwired.dev/).

```tsx
// Placeholder in shell:
//   <turbo-frame id="comments">Loading…</turbo-frame>

// Patch:
//   <turbo-stream action="replace" target="comments">
//     <template>…</template>
//   </turbo-stream>

// Patch, merge="morph" (Turbo >= 8):
//   <turbo-stream action="replace" method="morph" target="comments">
//     <template>…</template>
//   </turbo-stream>
```

### HtmxAdapter

Encodes placeholders (with `hx-get` when the fragment has a URL, in static mode)
and patches as `hx-swap-oob` elements, for [HTMX](https://htmx.org/). For request negotiation, see
[`negotiateHtmx`](/integration/http-serving).

```tsx
// Placeholder in shell:
//   <div id="comments" hx-get="/_fragments/comments" hx-trigger="load" hx-swap="outerHTML">Loading…</div>

// Patch:
//   <div id="comments" hx-swap-oob="outerHTML">…</div>

// Patch, merge="morph" (htmx >= 4):
//   <div id="comments" hx-swap-oob="outerMorph">…</div>
```

### EsiAdapter

Encodes placeholders as `<esi:include>` elements; an ESI-capable CDN or reverse
proxy (Varnish, Akamai, Fastly) fetches and inserts the content. Static only
(`streaming: false`), `replace` only: the origin emits one HTML template and the
edge fills in the dynamic parts.

## Custom adapters

Build your own with `createAdapter` from `@vincle/flow/adapters`:

| Export           | Purpose                                                         |
| ---------------- | --------------------------------------------------------------- |
| `Placeholder`    | Renders the shell placeholder for a deferred fragment           |
| `Patch`          | Wraps a resolved fragment for injection into the DOM            |
| `Frame`          | Wraps a static fragment file for SSG output                     |
| `capabilities`   | Declares supported merge types and streaming support            |
| `transformShell` | (Optional) Post-processes the shell before it enters the stream |

```tsx
import { createAdapter } from "@vincle/flow/adapters";

const MyAdapter = createAdapter({
  Placeholder: ({ id, children }) => <div data-defer={id}>{children}</div>,
  Patch: ({ id, children, merge }) => (
    <div data-patch={id} data-merge={merge}>
      {children}
    </div>
  ),
  Frame: ({ id, children }) => <div data-frame={id}>{children}</div>,
  capabilities: { streaming: true, merges: ["replace", "append"] },
});
```

An adapter does not implement the streaming wire: `renderToStream` owns it, and
`capabilities.streaming: true` only declares the adapter can stream.

`transformShell` can inject a client runtime or modify the shell HTML. It
receives a `ShellContext`, not the full flow context: just
`ctx.fragments.size`, so you can inject scripts only when fragments are
actually pending.
