A lot has changed for everyone who builds software since our last release. Many of us now write code together with AI agents.
This release makes Unpoly work better for this new way of building software. A completely rewritten documentation site Show archive.org snapshot helps humans and agents learn Unpoly best practices and look up API details. It is available as HTML, as Markdown and as an offline skill. A new contributor guide helps both humans and agents contribute to the project. We believe that hypermedia-driven apps (HDAs) fit this way of working well. They need less code, fewer dependencies and fewer moving parts, which makes them straightforward to understand and change for humans and agents alike.
We also made some changes to Unpoly itself. A major new feature is the ability to implement custom form fields as web components ("form-associated custom elements"). We fixed a ton of minor bugs and inconsistencies that had accumulated over Unpoly's 10+ years, and meaningfully improved our test coverage.
Changes that may need your attention are marked with 🧩 when
unpoly-migrate.js
Show archive.org snapshot
polyfills them and logs where you use the old API, with ⚠️ when they are not polyfilled but rare or minor, and with ❌ when you should check your code in any case.
A relaunched documentation site
You can now learn Unpoly on unpoly.com from start to finish, and look up details when you need them.
- The new Learn area covers all of Unpoly with best practices and examples, for beginners and pros alike.
- Getting started is a new guide that takes you from how Unpoly works to your first links, forms, overlays and compilers.
- We checked the code examples against the source code and fixed the ones that did not work. We split long guides into focused pages, merged guides that overlapped, and added pages for missing topics.
- The new search looks through the full text of every page. It ranks the feature you most likely mean first and favors matches in titles. Each result shows whether it is a guide, an attribute, a function or an event, and which chapter or module it belongs to. It also lists the sections that matched, so you can jump to the right heading. Press
/to search from any page. Search also works on phones. - The API reference is now separate from the Learn guides. Every API page links to the guide that explains its topic.
Old URLs redirect to the new pages, so your links and bookmarks keep working.
Docs for coding agents
An agent skill gives your coding agent the same documentation as you: every guide and the full API reference, with a search script that works offline.
With it, your agent follows Unpoly's common patterns for links, forms and overlays, and can look up the exact attributes, options and edge cases.
The skill also contains the release notes of every version, for when your agent upgrades an app.
Install the skill with the skills CLI Show archive.org snapshot :
npx skills add --global https://unpoly.com
If you use Claude Code, you can also install the skill as a plugin.
Agents can also read the docs without the skill:
- Every page is available as Markdown. Append
.mdto its URL, e.g.https://unpoly.com/api/up.render.mdShow archive.org snapshot . Agents that ask fortext/markdownfirst in theirAcceptheader get Markdown at the page's own URL. -
/llms.txtShow archive.org snapshot lists all guides and API modules, with links an agent can follow. - To give a chat a single page, use the copy button next to a page's title. It copies the page as Markdown.
Upgrades without human help
Coding agents can now upgrade Unpoly without human help. Give your agent the Unpoly skill and ask it to upgrade your app. The skill contains the upgrade guide and the release notes of every version.
To make sure this runs smoothly, we improved the release notes and unpoly-migrate.js, so your agent finds the changes your app needs. See unpoly-migrate.js for the details.
A guide for contributors
If you want to change Unpoly itself, start with
CONTRIBUTING.md
Show archive.org snapshot
.
It links 13 guides, written for people and for coding agents. They explain how to set up a development environment, where code lives, and how we test, document and release Unpoly. Two deep-dives explain the rendering core and the selector engine.
A walkthrough takes you through a first change: a new [up-*] attribute, its spec, its documentation and its commit.
Coding agents find the guide on their own, because AGENTS.md and CLAUDE.md point to the same file.
The guides come with new tools:
-
bin/devstarts a build watcher, a test server and the documentation site with one command. -
bin/testruns the specs in a real browser from the terminal, andbin/find-specfinds the specs relevant to your change, so you don't need to run the whole suite. - A scratch server shows your change on a few real pages, for things a spec cannot show, like how an overlay looks.
Custom form fields
Custom controls like date pickers or tag editors can now take part in Unpoly forms like a native <input>.
Unpoly now builds request params with the browser's own form-data algorithm, so a
form-associated custom element
Show archive.org snapshot
is submitted without any configuration.
When it also has a value property, Unpoly can watch, validate and switch it like any other field, and [up-disable] disables it while the form is submitting.
Here is a minimal date picker that reports its value to the form:
class DatePicker extends HTMLElement {
static formAssociated = true // mark: formAssociated
#internals = this.attachInternals()
#value = ''
get value() {
return this.#value
}
set value(newValue) {
this.#value = newValue
this.#internals.setFormValue(newValue) // mark: setFormValue
}
// Called when the element is inserted into the page
connectedCallback() {
// Render a calendar with a button for each day
this.innerHTML = `
<button type="button" data-day="2026-11-01">1</button>
<button type="button" data-day="2026-11-02">2</button>
…
`
this.addEventListener('click', (event) => {
let dayButton = event.target.closest('[data-day]')
if (dayButton) this.onDayPicked(dayButton.dataset.day)
})
}
// Called when the user clicks a day
onDayPicked(day) {
this.value = day
this.dispatchEvent(new Event('change', { bubbles: true }))
}
}
customElements.define('date-picker', DatePicker)
You can use it in a form like a native field, including with [up-validate]:
<form method="post" action="/events" up-submit up-validate>
<date-picker name="starts_at"></date-picker> <!-- mark: date-picker -->
<input type="text" name="title">
</form>
An element that is defined after the form was rendered, e.g. by a lazily loaded component, is picked up as soon as it is defined.
Values appended by a
formdata
Show archive.org snapshot
listener are now sent as well. See Adding values with a formdata listener.
The new guide Custom form fields compares four ways to build a custom control, from a hidden native field to a plain <div> that you register as a field.
Thanks to @kmmbvnr for contributing form-associated custom elements and formdata support.
Quality-of-life improvements
Some smaller changes in this release remove friction you may have worked around:
- To reload an element after an overlay closes, set the new experimental
[up-on-accepted-reload]attribute to its selector. You no longer need a JavaScript callback or a CSP nonce. See Overlays. - When Unpoly boots, it logs an error for a misspelled or renamed
up.*.configsetting, instead of ignoring it. See Mistakes are easier to find. - Pages with many watched forms no longer freeze for a moment after each fragment update. See Forms.
- Test runners, CI and coding agents now read Unpoly's log as plain text. See Mistakes are easier to find.
Mistakes are easier to find
When something is wrong, Unpoly now says so earlier and more clearly.
This also helps coding agents: clear errors and plain-text logs in automated browsers let an agent notice a mistake and fix it on its own.
- ⚠️ When Unpoly boots, it now logs an error for every setting that doesn't exist, like a misspelled key or one that was renamed in an earlier version. It checks the top-level keys of every
up.*.configobject, but not nested objects likeup.layer.config.modal, and only once, right after booting. If your tests fail on console errors, fix or remove these settings. - ⚠️ In a browser controlled by automation software, like Selenium, Playwright or Puppeteer, Unpoly now logs plain text without CSS formatting. Test runners, CI and coding agents that read the browser console get readable lines instead of
%cplaceholders and style strings. Normal browsers still format the log. To always format, setup.log.config.format = true. - Error messages, warnings and unformatted log output now print objects more readably. A plain object prints as
{ foo: "bar" }, a request asup.Request(GET /users/5)and an error asup.CannotMatch(message). A circular reference prints(circular)where the cycle closes, instead of replacing the whole value. - Fix: When an app's bundler minified
unpoly.js, errors could lose their names. For example, anup.CannotMatcherror was namedup.. Errors now keep their names under any minifier.
Enlarged click areas
A click on an [up-expand] area now does exactly what a click on its link does:
- When Unpoly follows the link, it follows the link itself. The link's attributes, events and origin all apply, including your own event listeners on the link and Rails'
[data-method]and[data-confirm]. - A link with
[download]downloads its file, and a link with[target=_blank]opens a new tab. - A link that Unpoly doesn't follow makes a full page load, as it would when clicked directly.
- After an overlay opened from the area closes, focus goes back to the link.
Fields in an expanded area keep handling their own clicks, and buttons, labels and <summary> elements now do too, instead of following the link.
Many apps we looked at had worked around the old limitations. You can now remove these workarounds. See Custom links and buttons for examples.
Form submissions now match the browser
Unpoly now uses the browser's form-data algorithm, so edge cases behave like a native form submission.
These changes cannot be polyfilled. If your server or your tests depend on the exact params of a submission, review them:
- ⚠️ A form with an
<input type="file">is now submitted asmultipart/form-data, even when no file is selected. - ⚠️ An
<input type="image">used as the submit button now sends itsname.xandname.ycoordinates instead of its[value]. - ⚠️ A
<button type="reset">or<input type="reset">with a[name]no longer sends a param. It is no longer watched or validated, but[up-disable]still disables it. - ⚠️ A field inside a
<fieldset disabled>is no longer submitted or validated. This includes custom elements inup.form.config.fieldSelectors. Watchers and[up-switch]still see the field. - ⚠️ A
[form]attribute now takes precedence over a surrounding<form>, like in the browser. A field or button within one form that is associated with another form no longer belongs to the surrounding form. It is no longer watched, validated or disabled with that form, and it is no longer that form's default submit button. - A form-associated custom element is only submitted when it calls
setFormValue(), even when it is listed inup.form.config.fieldSelectors. - ⚠️ Params now arrive in the order the browser produces them: the form's fields in document order, then values from
formdatalisteners, then custom elements registered inup.form.config.fieldSelectors. A submit button's[name]and[value]now appear at the button's position, not after the form's[up-params]. - ⚠️ A validation request no longer sends the params of the form's first submit button, and no longer uses that button's
[formaction],[formmethod],[up-params]or[up-headers]. Nobody pressed the button, so only the<form>decides where a validation goes. - ⚠️
up.Params.fromForm()now requires a<form>element, as documented. To read params from any other container, useup.Params.fromContainer(). - ⚠️
up.Params.fromForm()now includes the[name]and[value]of the form's first submit button, like a submission would. This shows most when you build a URL from the params that users see in the address bar, e.g. for a search. Pass{ submitButton: false }to only include fields, as before, or pass a different button element. - ⚠️
up.submit()andup.Params.fromForm()now throw an error when the{ submitButton }option is a button that doesn't belong to the form, as the browser'sFormDataShow archive.org snapshot does. Pass{ submitButton: false }to read a form without a button. - ⚠️ When a form with
[up-accept]or[up-dismiss]closes an overlay, the result value now also contains the[name]and[value]of the submit button the user pressed. - ⚠️
up.form.config.submitButtonSelectorsmay only match native submit buttons. A custom element or a<button type="button">matched as a submit button now makes submitting throw an error.
There is one exception: unlike the browser, a form only reads controls from its own layer. See Forms stay within their layer.
Forms stay within their layer
A form and its fields and buttons now always belong to one layer. Before, some features looked across layers: a form read the fields of a popup that was attached inside it, and a button in an overlay could submit a form below it. To let an overlay affect a form in another layer, have the overlay return a value when it closes, and let the layer below submit or update its form in an { onAccepted } callback.
- ⚠️
up.form.fields()andup.form.submitButtons()now ignore elements in another layer and elements that are being destroyed, like otherup.fragmentlookups. A field in a popup that is attached inside a form is no longer watched by that form or submitted with it. - ⚠️ When a submit button or a focused field has a
[form]attribute that points to a form in another layer, submitting now throws an error: "Prevented … from submitting a form in another layer". The browser would submit the other layer's form, e.g. when the same form ID exists in the layer below an overlay that shows the same page. To fix this, keep a form and its[form]controls in the same layer, and use form IDs that are unique across layers. - Fix: While an overlay was closing, an
[up-switch]field in it could show or hide elements in the layer behind it. This happened for a field outside a<form>, and for an[up-switch-region]that also matched in that layer.
A clear order of render steps
A render pass now inserts all new content first, then runs compilers, then updates focus and scroll position, and starts animations last. This order now holds for every placement, and when a render pass updates multiple fragments. A compiler can now see the new version of every other fragment updated in the same layer, including hungry elements. See Compilers see the DOM after the change.
- ⚠️ Fragments are now inserted and compiled in the order of the target selector, followed by hungry elements. Since Unpoly 3.0 they were compiled in reverse order, which the 3.0 notes did not mention until now.
up:fragment:insertedandup:fragment:destroyedare now also emitted in target order. When fragments are swapped without a transition, all their destructors now run before the first compiler, andup:fragment:destroyedis emitted after the last compiler. - ⚠️ When
:content,:beforeor:afteris updated with a transition or animation, focus now moves right away, like when swapping a fragment. It used to move when the animation ended. With:afterand{ focus: 'target' }, Unpoly now focuses the first appended element. It used to focus the container's first child, which could be an existing element. - ⚠️ When an error is thrown after new fragments were inserted, e.g. for an invalid
{ focus }or{ scroll }selector,up.render()now fulfills andup.render().finishedrejects. This was already the case when swapping a fragment, but:before,:afterand fully kept fragments rejectedup.render(). - Fix: When multiple fragments played a transition and the render pass scrolled, only the first old element kept its position on the screen.
Security note for apps on Unpoly 3.14
The 3.14.0 release notes named the setting for callback nonces up.script.config.callbackPolicy. The correct name is up.script.config.evalCallbackPolicy. The error was only in the release notes. The API reference and the guides always used the correct name.
If you copied the wrong name, Unpoly ignored your setting and used its default policy. Without a <meta name="csp-nonce"> tag, the default policy runs callbacks without checking their nonces. Your Content Security Policy then decides what runs. Injected HTML can run code through Unpoly callbacks if your CSP allows 'unsafe-eval', if it uses 'strict-dynamic' (which trusts every script that Unpoly inserts), or if you have no CSP.
To fix this, rename the setting to evalCallbackPolicy, then check that your callbacks still run. You don't need to upgrade to 3.15 for this.
Starting with 3.15, Unpoly logs an error when it boots for every setting that doesn't exist, so a misspelled or renamed setting no longer goes unnoticed. See Mistakes are easier to find.
Forms
- ⚠️ While a validation request is loading, feedback classes like
.up-loadingare now set by default, as for every other request. To turn them off, set[up-watch-feedback="false"]or pass{ feedback: false }. - A custom control now only needs a
valueproperty to work as a custom form field. It used to also neednameanddisabledproperties. Unpoly now falls back to the[name]and[disabled]attributes. - A
formdatalistener changes what Unpoly sends, not what it watches. See Adding values with aformdatalistener. - New experimental config
up.form.config.noFieldSelectorsexcludes elements from being treated as fields, e.g. a form-associated custom element that renders a button. - When Unpoly disables a custom control that has no
disabledproperty, it now sets the[disabled]attribute. - When Unpoly focuses a form-associated custom element, it now sets
.up-focus-visible, even after a mouse or touch interaction. Such an element is now a field, andup.viewport.config.autoFocusVisibleshows a focus ring for every field. - A custom control with a
[name]but no readable value no longer reports anundefinedvalue to watchers. - Pages with many watched forms are much faster. Before, every
[up-autosubmit],[up-validate],[up-switch]orup.watch()looked at every field on the page after each fragment update. A page with hundreds of small forms could freeze for a second after each update. - ⚠️
[up-switch]now switches immediately, ignoringup.form.config.watchInputDelayand[up-watch-delay]. Switching effects are client-side and don't benefit from debouncing. In a form that also autosubmits with a delay, switching now settles before the submission reads the form. If you set[up-watch-delay]on a switching field only to make it react faster, you can remove it. Keep it when the same field also validates or autosubmits. - Fix: When a form was submitted or validated again while a request had disabled its fields, e.g. with
[up-disable], the second request sent no params for the disabled fields. Fields and buttons that a preview disabled now still count as part of the form when Unpoly reads params or watches values. - ⚠️ Since Unpoly 3.12, submit buttons honor a
[data-confirm]attribute, like links and forms do. It is an alias of[up-confirm], so Rails helpers likelink_toorbutton_towork without changes. If your app handles[data-confirm]itself, users see two dialogs, and specs may fail with anUnexpectedAlertOpenError. Have your handler use another attribute instead. Unpoly only confirms for forms it submits, so buttons in an[up-submit=false]form or in a form that Unpoly doesn't handle still need your handler. - A nested
[up-autosubmit], e.g. on a field inside a form that already has[up-autosubmit], is now ignored when it belongs to the same form. It used to submit the form a second time for the same change. - New experimental class
.up-bootedon the<html>element after Unpoly has booted. It lets CSS hide switched elements until Unpoly has processed them. - Fix: When a focused field or button had no form group that Unpoly could target, disabling it with
[up-disable]threw aTypeError. This happened e.g. for a submit button that shares its[name]with another button, in a form without an[id]. - ⚠️
up.form.group()now returns the closest group, even when Unpoly cannot derive a target selector for it. To only get a group that Unpoly can target, pass the new{ targetable: true }option. - 🧩
up.form.config.genericButtonSelectorswas renamed toup.form.config.anyButtonSelectors. It now matches every kind of button, including submit and reset buttons. The old name is polyfilled byunpoly-migrate.js. - Fix: After clicking a submit button that Unpoly cannot target, e.g. a button without
[id]or[name], an error response moved the focus to the<body>. The re-rendered form now receives the focus, like when other focused elements are replaced. The same happened in other render passes that replaced a focused element without a derivable target.
Overlays
- New experimental
[up-on-accepted-reload]and[up-on-dismissed-reload]attributes reload an element in the parent layer after an overlay closes, e.g.up-on-accepted-reload=".company-list". Unlikeup-on-accepted="up.reload('.company-list')", they run no JavaScript, so they need no CSP nonce. The matching options{ onAcceptedReload }and{ onDismissedReload }also work in anX-Up-Open-Layerheader. See Reloading on acceptance. - New
[up-on-accept]and[up-on-dismiss]attributes run code before an overlay closes, and can keep it open withevent.preventDefault(). The matching{ onAccept }and{ onDismiss }options are now documented. - When an overlay that was opened without an
{ origin }closes, focus now moves to the parent layer, as described in focus in overlays. It used to fall back to the<body>. - Fix:
[up-focus-visible]and the{ focusVisible }option were ignored when opening an overlay. - Fix: Pressing Escape dismissed an overlay even when an element in it had already handled the key with
event.preventDefault(), e.g. a dropdown closing its list. A prevented Escape keydown now keeps the overlay open. - ⚠️ Aborting the root layer, e.g. with
up.fragment.abort({ layer: 'root' })or a render pass with{ abort: 'layer' }, no longer stops polling, pending validations or a scheduled preload in an open overlay. Requests in overlays were already kept.up.fragment.onAborted()andup.fragment.onKept()now only react to ancestors in the same layer. To do the same in your own code, pass the new{ layer }option toup.event.onClosest(). - Fix: The
{ duration }and{ easing }options ofup.layer.open()were ignored for the opening animation. This also affected[up-duration]and[up-easing]on a link that opens an overlay. Onlyup.layer.configwas used. - Fix: Double-clicking a link that opens an overlay right away, e.g. a styled confirmation dialog or an overlay with preloaded content, dismissed the new overlay at once. The second click landed outside the overlay. Double-clicking a link that opens a popup opened and closed the popup. The overlay now stays open.
- Fix: Callbacks like
onAcceptedoronDismissedpassed toup.layer.ask()were ignored. They now run when the overlay closes, before the promise settles.
Caching
- 🧩 New config
up.network.config.cacheEnabledturns off the cache entirely, e.g. for debugging. When it isfalse, no response is read from or written to the cache, and preloading is disabled. If you usedup.network.config.cacheSize = 0to turn off caching,unpoly-migrate.jsnow disables the cache and tells you to use this setting instead. It used to throw an error. - An
X-Up-Expire-Cache: falseresponse header now expires no cache entries, likeX-Up-Evict-Cache: false. It used to be treated as a URL pattern matching/false. - Fix: When a compiler requested the URL of a request that had just been aborted or had just finished, it sometimes got the old request from the cache. It now makes a new request.
Rendering
- Fix: The
{ onFailFinished }callback and[up-on-fail-finished]attribute are now called after an error response has finished rendering. They used to never be called. - Fix:
[up-reveal-padding]and the{ revealPadding }render option were ignored. Onlyup.viewport.config.revealPaddingwas used. - Fix: Target derivation for an SVG element with a camelCase tag name, like
<clipPath>, now produces a selector that matches the element. - Fix: While an overlay was open,
up.fragment.get(root, selector)could return an element outside the given root. - Fix: When an error was thrown after a render pass had started to change the page, Unpoly could render a fallback target on top of the update. The render promise now rejects with that error.
- ⚠️ When the server responds to polling or a deferred placeholder with an error code, Unpoly no longer throws an uncaught
up.CannotMatcherror. The response is not rendered: polling tries again after the next interval, and a deferred placeholder keeps its fallback content. A promise returned byup.reload()orup.deferred.load()still rejects, now with anup.RenderResult. To render error responses anyway, set[up-fail=false]on the[up-poll]or[up-defer]element, or render them into another fragment with[up-fail-target]. - ⚠️ With
up.history.config.enabled = false, a render pass no longer changes the window title, meta tags orhtml[lang]. Before, only the browser URL stayed unchanged, so the page could show the title of one page with the URL of another. To still update some aspects, setup.history.config.enabledto an array of the history aspects Unpoly may update:'location','title','metaTags'or'lang'. For example,up.history.config.enabled = ['lang']never changes the browser URL, but still updateshtml[lang]. When an aspect is turned off, layers report their rendered value for it, so[up-nav]follows the content. Closing an overlay no longer restores a turned-off aspect, andup.history.push()andup.history.replace()do nothing without'location'. When the initial page was loaded with aPOSTrequest and'location'is turned off, Unpoly no longer falls back to full page loads, no longer disables preloading, and no longer warns at boot. - 🧩
up.history.config.updateMetaTagsis deprecated. If you set it totrue, remove the line. To keep meta tags unchanged, omit'metaTags'from an array of aspects inup.history.config.enabled.unpoly-migrate.jspolyfills the old setting. It also no longer warns at boot when you haven't setup.history.config.updateMetaTags. - Fix: An explicit list of elements passed as
{ metaTags }was replaced by the meta tags from the response. - Fix:
[up-scroll-behavior]and the{ scrollBehavior }option were ignored when scrolling to thetoporbottom, to a pixel position, to a#hashor when restoring a scroll position. Only revealing an element honored them. - Fix: When the server sent an
X-Up-Locationheader with a#hash, Unpoly updated the browser URL but did not reveal the matching element. When the requested URL also had a#hash, the browser URL ended up with both. - Fix: A combined transition like
move-to-bottom/noneswapped the elements instantly. When one half isnone, the other half is now animated. - Fix: With animations disabled, animations and transitions no longer wait for the next animation frame before they finish. This bug was introduced in Unpoly 3.14.
Links and URLs
- ⚠️ An
[up-on-offline]callback now receives theup:fragment:offlineevent asevent, so it can callevent.retry(). It used to receive the event aserror. -
URL patterns now match full URLs on the page's own origin, like
https://example.com/users/*. Such patterns never matched before. - Fix: Pressing the Space key didn't activate a faux button, e.g. an element with
[up-emit]or[up-clickable]. Only the Enter key worked, although Space was meant to work for buttons too. - Fix: When the user pressed the mouse button on a preloading link while its preload request was still loading after hovering, Unpoly aborted the request and sent the same request again. This bug was introduced in Unpoly 3.8.
- Fix: When the user clicked a preloading link while its preload request was still loading, a newer render pass for the same fragment could not abort the click. Once the preload responded, the click rendered the old response over the newer content, and aborted the newer render pass. The click can now be aborted like any other request, while the preload keeps loading for the cache.
- ⚠️ A click on an
[up-expand]area now does what a click on its link does. When Unpoly follows the link, it now follows the link itself, not the area: events likeup:link:followare emitted on the link, the link is the origin, and its[data-method]and[data-confirm]attributes apply. When Unpoly doesn't follow the link, the area clicks it, and the browser does what the link does: a[download]link downloads its file, a[target=_blank]link opens a new tab, and a link without[up-follow]makes a full page load. To keep updating a fragment, give the link an[up-follow]attribute or follow all links. - Fix: Clicking a button, a
<label>or a<summary>inside an[up-expand]area, or inside another followable element that isn't a link, followed the link. A submit button didn't submit its form, a label didn't toggle its checkbox, and a<details>element didn't open. These elements now handle their own clicks, like links and fields already did. Inside an<a>element, buttons and labels still belong to the link. - ⚠️ Clicks made by JavaScript, like
element.click(), now always emitup:click. When a script clicks a link, Unpoly now follows it, also while handling another click or within a popup. To have the browser follow the link instead, set[up-follow=false]on it. - Fix: Clicking a form field inside an
[up-instant]link or area, e.g. an[up-expand]area around an[up-instant]link, did nothing. Unpoly didn't follow the link, but also prevented the click, so a checkbox didn't toggle. Fields now handle their own clicks. - Fix: An
[up-instant]link did nothing when another script prevented itsmousedownevent, e.g. to keep the focus in a search field. Unpoly now follows such a link onclick, and emitsup:clickonclickinstead ofmousedown.
unpoly-migrate.js
- ⚠️
unpoly-migrate.jsnow logs deprecations as errors, so test suites that fail on console errors find them without extra configuration. To log warnings as before, setup.migrate.config.logLevel = 'warn'. -
unpoly-migrate.jswarnings about renamed render, request or layer options now name where the option was used, like the origin element or URL, instead of printing the options object. Each place is reported once. - The
unpoly-migrate.jswarning for the deprecated origin shorthand&now names the target and the origin element. When the origin is a button or field in a form, it also reminds you that a form submission's:originis now the button or changed field, not the form, and thatform:has(:origin)targets the form. - Fix:
unpoly-migrate.jslogged deprecations that the app never caused when a script enumerated an object with deprecated properties, e.g. an analytics script walkingwindow.up, or another deprecation message printing a config object. - Fix: The
unpoly-migrate.jspolyfills forup.popup.attach(),up.modal.follow()andup.modal.extract()now open a new overlay again. - Fix: The
unpoly-migrate.jspolyfill forup.network.shouldReduceRequests()now logs a deprecation message instead of throwing an error. - Fix: The
unpoly-migrate.jspolyfill forup.util.microtask()threw an error. This bug was introduced in Unpoly 3.8. - Fix:
unpoly-migrate.jsignored renamed layer keys likeclosable,dismissable,closeLabelordismissAriaLabelwhen they were set inup.layer.config. They are now renamed, like when passed as options. - Fix: With
unpoly-migrate.js, old layer attributes like[up-flavor],[up-closable]or[up-history-visible]are now also translated on forms that open an overlay. - Fix: With
unpoly-migrate.js, a popup position like'bottom-right'is now split into{ position: 'bottom', align: 'right' }before the overlay opens. Before,up.Layer#positionand the<up-popup>attributes showed the combined value. - Fix: With
unpoly-migrate.js, a link with only an[up-href]attribute is now given[up-follow]. Before,unpoly-migrate.jsand Unpoly could disagree about whether the link was followable. It also no longer warns about a link that another attribute already makes followable, like<a up-href="/path" up-target=".target">.
Smaller fixes and changes
- ❌ The 3.14.0 release notes named the setting for callback nonces
up.script.config.callbackPolicy. The correct name isup.script.config.evalCallbackPolicy. If you copied the wrong name, Unpoly ignored your setting and used the default policy'auto'. Search your code forcallbackPolicyand rename it. Renaming makes your policy take effect. Check that your callbacks still run.unpoly-migrate.jsnow forwards the wrong name and logs a deprecation message. Withoutunpoly-migrate.js, Unpoly now logs an error for the wrong name when it boots, like for any other setting that doesn't exist. See the security note. - ⚠️
unpoly.jsis now compiled using ES2024 (up from ES2021). It needs Chrome 119, Firefox 121 or Safari 17.4, all released by March 2024. The ES6 build for legacy browsers remains available. - ⚠️ A
clickevent dispatched by a script is no longer passed toup.on()callbacks when the element has a[disabled]attribute, even for elements that browsers cannot disable. Before, only adisabledproperty had this effect. - The npm package now also includes source maps for the unminified files, like
unpoly.js.map. -
up.template.clone()with an{ origin }now also finds a template in an ancestor layer, as described in template lookup. It used to only look in the origin's layer. - Fix:
up.viewport.restoreFocus()threw an error when called without a{ layer }option, or with a layer name like{ layer: 'root' }. - Fix:
up.viewport.restoreScroll()ignored its{ location }option. It always restored the positions saved for the layer's current location. - Fix:
up.fragment.matches()with{ layer: 'new', mode }options expanded a selector like:mainfor the root layer, or threw an error while an overlay was open.
Note
Our sponsor makandra funded this release ❤️
You can hire makandra for Unpoly support Show archive.org snapshot .