Unpoly: dynamic UI through attributes on server-rendered HTML
Progressive enhancement for HTML
At a glance
- What is it?
- A small MIT-licensed JavaScript library that upgrades plain HTML with attributes so a server-rendered page can behave like an application. Written by one developer, sponsored by one company, documented on its own site rather than in the repository.
- Who is it for?
- Unpoly is a good fit for a server-rendered application that wants partial page updates and modal overlays without adopting a component framework or a full client-side build. The trade is that you are buying a single author's design vocabulary rather than a standard: the attribute names are Unpoly's own, the guides live on unpoly.com, and the repository README will not teach you the API.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- unpoly/unpoly2,797 ★ · +4
| Repository | 2026-10-14 | 2026-11-07 |
|---|---|---|
| unpoly/unpoly | 2.8k | 2.8k |
4 Stars in the last 24 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Attributes on server-rendered markup rather than a client app
The whole idea fits in the README's description line: progressive enhancement for HTML. Unpoly is a JavaScript library that enhances HTML with attributes to build dynamic UI on the server. The application renders ordinary markup, the server decides what the response contains, and Unpoly's attributes tell the browser how to apply a response instead of reloading the page.
Two claims in that paragraph carry most of the design. Unpoly works with any language or framework, and it degrades gracefully without JavaScript. The first means there is no framework plugin to install and no server-side adapter; the second means the same page still works when the script fails to load, because the underlying HTML is functional on its own. That is a materially different starting position from a client-rendered single-page application, where the page is empty until the bundle arrives.
The repository topics use the vocabulary of the hypermedia school: html, html-driven, html-over-the-wire, hypermedia, progressive-enhancement, server-side-rendering. The package keywords in `package.json` repeat that list exactly, which tells you the project is deliberately positioned in that lineage rather than presenting itself as a general-purpose JavaScript toolkit.
Installing from npm and reading the version number
The README defers all installation detail to a page at unpoly.com/install that covers several package managers and languages. For a JavaScript project the npm route is the obvious one:
npm install unpolyThe package name comes from `package.json`, which also declares what the library looks like from a bundler's point of view:
"main": "unpoly.js",
"style": "unpoly.css",That is the entire runtime surface: one script file and one stylesheet, no module graph to configure. The version in the same file is 3.14.3. There is a `dist/` directory in the tree and a `src/` directory beside it, so the published artifact is built rather than shipped as the TypeScript sources.
One wrinkle is worth knowing before you install. The repository has no GitHub releases attached, so there is no release page to diff against; the version history lives in `CHANGELOG.md` in the repository and in the notable changes page the README links on unpoly.com. Pin the version in your lockfile rather than tracking whatever is newest.
One author, one sponsor, documentation on another site
The credits section names a single developer, Henning Koch of makandra, plus contributors, and the header of the README carries a sponsored-by banner for makandra. The package metadata agrees: the author field points at the same person and the same company. This is a library with a clear single point of ownership, which is a strength for consistency and a factor to weigh for a dependency you intend to live with.
The documentation split is the more consequential detail. The README is a short index: unpoly.com for guides and documentation, demo.unpoly.com for a working example application, the install page, the GitHub discussions forum, and a notable changes page. Every one of those is an external URL. So the repository tells you what Unpoly is and where to read it, and nothing else. Nobody learns Unpoly's attribute names from this page.
GitHub reports no homepage field for the repository, while `package.json` declares `https://unpoly.com` as its homepage. Both facts are in their own file; the repository metadata simply is not populated, and the authoritative pointer lives in the package manifest.
There is one more thing in the tree worth noticing for a project of this size: an `AGENTS.md`, a `CLAUDE.md`, and `.agents/` and `.claude/` directories at the top level, alongside `CONTRIBUTING.md` for working on the framework itself. Instruction files for coding assistants are now a normal part of an open-source repository's shape.
Where the README stops on comparison, and what the manifest reveals instead
Google returns searches such as Unpoly vs HTMX and asks what the alternatives to HTMX are, which tells you the comparison readers actually want is about other hypermedia libraries. The README does not make that comparison. It makes no claim about any competitor, names no alternative, and does not explain which problems Unpoly solves better or worse than anything else. If you are choosing between libraries in this family, this page will not settle it, and the guides on unpoly.com are the next place to look.
What the repository does reveal is worth more than a marketing comparison would be. The `package.json` manifest has no `dependencies` field at all: everything listed is a `devDependencies` entry, covering the toolchain rather than the shipped library. Webpack, TypeScript, Sass, Jasmine, Puppeteer, jQuery and Express all appear there because they are needed to build and test the project, not because a consumer installs them. A library that adds behaviour to every page on your site and adds nothing to your bundle's dependency graph is a real property, and it is verifiable from the file rather than from a claim.
The `files` array in the manifest also defines the published package precisely: LICENSE, README.md, CHANGELOG.md, then `unpoly*.js`, `unpoly*.css` and `unpoly*.map`. No build tooling, no sources, no configuration ships to users.
Building and testing the library from source
Two npm scripts do the work, and both are plain Node invocations rather than a task runner:
npm testThat runs `node bin/test --all`, and `npm run build` runs `node bin/build`. The `bin/` directory holds both entry points as Node scripts, which is a small design choice that keeps the build reproducible without a framework around it.
The `spec/` directory and the devDependencies line up: Jasmine for the test framework, `jasmine-ajax` for request stubs, jQuery for compatibility checks, and Puppeteer for real browser runs. Express appears as well, which suggests a local server for the suite. The presence of a headless browser in the devDependencies is the interesting part, because attribute-driven DOM behaviour is exactly the sort of thing that passes a unit test and fails in a browser.
The build stack is webpack with ts-loader, sass and mini-css-extract-plugin, and there is an eslint config, a tsconfig, and an `.nvmrc` pinning the Node version. The `unpoly.code-workspace` file and the editor-adjacent `.nvmrc` and `.npmrc` suggest a single-developer setup built for one machine, which is consistent with everything else in this repository.
What attribute-driven enhancement costs you
The approach has a clear failure mode that its advocates accept and its critics attack. When your interactivity is expressed as attributes the server emits, the server has to know which variations the client might want, and every new interaction becomes a new attribute on a new element. For a page whose interactions are a form submit, a link navigation and a modal, that is a good trade. For an interface that grows a dozen stateful widgets, you are maintaining a vocabulary invented by one developer, with no framework convention to fall back on and no type checking between the server's template and the attribute name.
The second cost is documentation drift. Because the manual lives on an external site, the version you install and the version the guides describe are connected by a maintenance process rather than by the package itself. The README's branch scheme makes that explicit: `master` tracks 3.x, 2.x users are sent to the `2.x-stable` branch, and 1.x and 0.x users to `1.x-stable`. Cross-branch upgrades are where surprises live.
The third is that the graceful degradation promise narrows what you can build. Anything that requires client state to exist before the first response cannot be expressed this way, because the no-JavaScript version has to remain usable. That constraint is the whole point of the design, and for administrative tools, documentation sites and content management back ends it is the right constraint. For a consumer application that wants a rich client, it is a limitation you will feel on day one.
Editorial conclusion
Unpoly is a good fit for a server-rendered application that wants partial page updates and modal overlays without adopting a component framework or a full client-side build. The trade is that you are buying a single author's design vocabulary rather than a standard: the attribute names are Unpoly's own, the guides live on unpoly.com, and the repository README will not teach you the API. Check two things before committing. First, that you are comfortable with a library whose documentation is a separate site, since the README is thirty lines of links. Second, the version line, because 3.x is tracked on `master` while 2.x users are pointed at the `2.x-stable` branch, so an upgrade crosses a branch and not just a version bump. Install version 3.14.3 from npm and read the installation page before wiring attributes into templates.
Frequently asked questions
What is Unpoly and what does it do?
Unpoly is a JavaScript library that enhances HTML with attributes to build dynamic UI on the server. It works with any language or framework and degrades gracefully without JavaScript, so the underlying page remains usable when the script does not run.
How do you install Unpoly?
The README points to an installation page at unpoly.com/install covering several package managers and languages. For npm projects the package name is unpoly, and it publishes a single script and stylesheet, unpoly.js and unpoly.css, with no runtime dependencies listed in package.json.
What is the current version of Unpoly and which branch should I use?
The version in package.json is 3.14.3, and the master branch tracks the current 3.x major version. Users on 2.x should use the 2.x-stable branch, and users on 1.x or 0.x should use 1.x-stable. The repository has no GitHub releases, so version history is in CHANGELOG.md and on the changes page linked from the README.
Is Unpoly related to htmx, and how does it compare?
Both are used for server-driven HTML over the wire, and the README does not make a comparison with htmx or any other library. What the repository does show is that Unpoly ships one script and one stylesheet with no runtime dependencies, and that its documentation and guides live on unpoly.com rather than in the README.
Who maintains Unpoly and under what license?
The README credits Henning Koch of makandra along with contributors, and shows a sponsored-by banner for makandra. The project is released under the MIT License, with a LICENSE file at the repository root.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/unpoly-unpoly)Star history chart for your README
[](https://hysenlabs.com/projects/unpoly-unpoly)Was this useful?