<a href="/tasks/123" up-target="#pane">Details</a> <!-- mark: up-target -->
<a href="/users/new" up-layer="new">Add user</a> <!-- mark: up-layer -->
<form up-validate>…</form> <!-- mark: up-validate -->
<main up-poll>…</main> <!-- mark: up-poll -->
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.
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:
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.
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:
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:
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 →Update the screen before the server answers, then reconcile with the real response. Previews revert cleanly when anything fails.
Learn more →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 →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.
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 -->
The hypermedia space has two philosophies. htmx states its own beautifully:
“Why should only
— htmx.org, on its motivation<a>&<form>be able to make HTTP requests? Why should onlyclick&submitevents trigger them?”
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.
In production at
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.
Tip Your coding agent can search these docs locally with the Unpoly agent skill