Build a complete frontend in HTML
<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.
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:
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:
- Link
- Form
- Fragment
- Request
- History
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.”
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 -->
Is Unpoly right for you?
There are two philosophies for building hypermedia-driven applications. htmx states its own beautifully:
“Why should only
<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.
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.