The missing application layer for HTML

<a href="/tasks/123" up-target="#pane">Details</a> <!-- mark: up-target -->
updates a fragment
<a href="/users/new" up-layer="new">Add user</a> <!-- mark: up-layer -->
opens in a modal overlay
<form up-validate>…</form> <!-- mark: up-validate -->
validates against your server
<main up-poll>…</main> <!-- mark: up-poll -->
keeps itself fresh
+ many more

The application stays on your server, in any language and any framework. The browser learns just enough to feel like an app. Your HTML keeps its meaning. Agents and crawlers can read your app without running JavaScript.

Update only what changed

Following an Unpoly link doesn't tear down the page. The server responds with HTML (a full page is fine), and Unpoly extracts the targeted fragment, discarding the rest:

How Unpoly uses a server response Three requests, each shown across four columns. The server always responds with a complete page. Unpoly uses only the fragments that changed, leaving the rest of the screen in place. A server may optionally respond with those fragments alone. BROWSER REQUEST SERVER RESPONSE FRAGMENTS USED OPTIMIZED RESPONSE OPTIONAL GET /notes (initial load) GET /notes/3 PATCH /notes/3

The rest of the page stays: your scroll position, your focus, the form you were halfway through.

And because every response can be a full page, every URL keeps working on its own: in a new tab, for a bookmark, for a crawler. You don't build separate fragment endpoints. Unpoly works with the routes you already have. Rendering only the fragment is a simple, optional optimization; most screens never need it.

Edge cases included

Real applications are full of cases that demos skip: back buttons, racing requests, stale caches, users who double-click. Unpoly's defaults cover them. Here is a single enhanced link:

<a href="/projects" up-follow>Projects</a> <!-- mark: up-follow -->

When it's clicked, Unpoly:

  • updates the main fragment and the browser URL
  • preserves scroll positions, focus and half-typed input outside the fragment
  • aborts an earlier request racing for the same fragment
  • caches the response, and revalidates it when it goes stale
  • restores the page with its scroll positions and focus when the user presses Back
  • cleans up your JavaScript components before their elements are removed

Built for tough requirements

These are the screens that usually get an app rewritten in React. With Unpoly they stay on your server. You render them as HTML and enhance them with a few attributes:

Reactive server forms

Fields that depend on other fields, like selects loading their options or previews updating as you type. Rendered by your server, wired with one attribute.

Learn more →

Optimistic rendering

Update the screen before the server answers, then reconcile with the real response. Previews revert cleanly when anything fails.

Learn more →

Complex overlay flows

Branch off into an overlay, complete a multi-page flow there, and return the result to the parent screen, which stayed open behind the overlay.

Learn more →

Escape hatches everywhere

When attributes aren't enough, you can recompose Unpoly's behavior using its extensive JavaScript API. Every attribute has a JavaScript twin: [up-submit] is up.submit(), and modifying attributes become options.

// Let the user create a record in an overlay, then use the result:
let { email } = await up.layer.ask({ url: '/users/new' })
up.submit('#invite-form', { params: { email } })

Along the way, Unpoly emits the events you wish the platform had:

And your own JavaScript plugs in through compilers. Here a Google Map gets its center from the server:

<div class="map" up-data="{ lat: 50.9, lng: 7.4 }">
  <!-- chip: Map will appear here -->
</div>
up.compiler('.map', (element, data) => {
  var map = new google.maps.Map(element)
  map.setCenter(data)
})

The same pattern enhances existing tags, invents new elements, or mounts a whole React island.

Your HTML keeps its meaning

Unpoly enhances HTML elements without changing what they are. A link still navigates and a form still submits. Unpoly works like a polyfill for the application features HTML never got.

“A wonderful JavaScript library with best-in-class progressive enhancement.”

— Carson Gross, creator of htmx

Everything that already understands HTML keeps understanding your app: search engines index the HTML your server sends, agents read your content without executing JavaScript, and screen readers find actual <a> and <form> elements, not clickable <div>s. And since your server can always answer with a complete page, every screen has its own URL that you can share, bookmark or open directly.

Every feature has a graceful degradation story. For example, when we built lazy loading, we modeled it as a self-loading hyperlink. Without JavaScript it's still a link, and the content is one click away instead of gone:

<a href="/menu" up-defer>Load menu</a> <!-- mark: up-defer -->
With JavaScript
  • Projects
  • Tasks
  • Reports
Without JavaScript
Load menu

Is Unpoly right for you?

The hypermedia space has two philosophies. htmx states its own beautifully:

“Why should only <a> & <form> be able to make HTTP requests? Why should only click & submit events trigger them?”

— htmx.org, on its motivation

Unpoly makes the opposite bet: the elements HTML already gave us are the right ones to make requests, because the whole web already knows what they mean. So instead of generalizing requests to any element, Unpoly makes links and forms more capable, with strong defaults for the cases every app hits.

Both approaches make sense. Small primitives give you maximum control and a minimal footprint. A higher-level layer gives you fewer attributes for common cases, with the edge cases already handled.

Unpoly is for you if…

  • You render HTML on a server (Rails, Django, Laravel, Phoenix, Node, anything) and want to keep it that way.
  • Your app has outgrown full page loads, but an SPA would mean duplicating your product in the browser.
  • You want one mental model for navigation, overlays and forms, not a kit of separate libraries to combine.
  • You want overlays, reactive forms and instant navigation without wiring the plumbing yourself.
  • You expect to maintain this app for years.

Choose another tool when…

  • Your application state truly belongs in the browser, as in an editor, a game or an offline-first app.
  • You want maximum control over every request and DOM change. htmx is excellent for that temperament.
  • You need native iOS/Android shells around your app and don't want to write your own. Hotwire ships ready-made adapters for that.
  • A dedicated frontend team owns a separate client-side product.

In production at

Start with one screen

You don't need to migrate your application. Start where full page loads hurt most: a filter form that resets the page, an edit screen that should be an overlay. Enhance one link and leave the rest of your app as it is.

Give it ten minutes. The Getting started guide shows you how to install Unpoly with two lines and no build step, then walks you through your first link, form and overlay.