Revision code

Changes Version 3.0.0
Released on April 17, 2023 with 905 commits


The main concern of Unpoly 3 is to fix all the concurrency issues arising from real-world production use:

  • Forms where many fields depend on the value of other fields
  • Multiple users working on the same backend data (stale caches)
  • User clicking faster than the server can respond
  • Multiple requests targeting the same fragment
  • Responses arrives in different order than requests
  • User losing connection while requests are in flight
  • Lie-Fi (spotty Wi-Fi, EDGE, tunnel)

Unpoly 3 also ships numerous quality of life improvements based on community feedback:

  • Optional targets
  • Idempotent up.hello()
  • More control over [up-hungry]
  • HTML5 data attributes
  • Extensive render callbacks
  • Strict target derivation
  • Cleaner logging
  • Foreign overlays

In addition to this CHANGELOG, there is also a slide deck explaining the most relevant changes in more detail.

Finally we have reworked Unpoly's documentation in our ongoing efforts to evolve it an API reference to a long-form guide.

Upgrade effort

  • The upgrade from Unpoly 2 to 3 will be much smoother than going from Unpoly 1 to 2. We were able to upgrade multiple medium-sized apps in less than a day's work. As always, YMMV.
  • No changes were made in HTML or CSS provided by Unpoly.
  • Most breaking changes are polyfilled by unpoly-migrate.js, which automatically logs instructions for migrating affected code.
  • If you're only looking for breaking changes that need manual review, look for the ⚠️ icon in this CHANGELOG.
  • unpoly-migrate.js keeps polyfills for deprecated APIs going back to 2016. You may upgrade from v1 to v3 without going through v2 first.


Concurrent updates to the same fragment

When a user clicks faster than a server can respond, multiple concurrent requests may be targeting the fragments. Over the years Unpoly has attempted different strategies to deal with this:

  • Unpoly 1 did not limit concurrent updates. This would sometimes lead to race conditions where concurrent responses were updating fragments out of order.
  • Unpoly 2 by default aborted everything on navigation. While this would guarantee the last update matching the user's last interaction, it sometimes killed background requests (e.g. the preloading of a large navigation menu).
  • Unpoly 3 by default only aborts requests conflicting with your update. Requests targeting other fragments are not aborted. See a visual example here.

That said, Unpoly 3 makes the following changes to the way conflicting fragment updates are handled:

  • The render option { solo } was replaced with a new option { abort }.
  • The HTML attribute [up-solo] was replaced with a new attribute [up-abort].
  • A new default render option is { abort: 'target' }. This aborts earlier requests targeting fragments within your targeted fragments.

    For instance, the following would abort all requests targeting .region (or a descendant of .region) when the link is clicked:

    <a href="/path" up-target=".region">

    ⚠️ If your Unpoly 2 app uses a lot of { solo: false } options or [up-solo=false] attributes, these may no longer be necessary now that Unpoly 3 is more selective about what it aborts.

  • To programmatically abort all requests targeting fragments in a region, use up.fragment.abort(selector).
  • ⚠️ Unpoly now cancels timers and other async work when a fragment is aborted by being targeted:
    • Polling now stops when the fragment is aborted.
    • Pending validations are now aborted when an observed field is aborted.
    • When a fragment is destroyed or updated, pending requests targeting that fragment will always be aborted, regardless of the { abort } option.
  • Your own code may react to a fragment being aborted by being targeted. To so, listen to the new up:fragment:aborted event.
  • To simplify observing an element and its ancestors for aborted requests, the function up.fragment.onAborted() is also provided.
  • Fragment updates may exempt their requests from being aborted by setting an [up-abortable=false] attribute on the updating link, or by passing an { abortable: false } render option.
  • Imperative preloading with is no longer abortable by default.

    This makes it easy to eagerly preload links like this:


    You can make preload requests abortable again by passing an { abortable: true } option.

  • The option up.request({ solo }) was removed. To abort existing requests, use up.fragment.abort() or

Optional targets

Target selectors can now mark optional fragments as :maybe.

For example, the following link will update the fragments .content (required) and .details (optional):

<a href="/cards/5" up-target=".content, .details:maybe">...</a>

Strict target derivation

Unpoly often needs to derive a target selector from an element, e.g. for [up-hungry], [up-poll] or up.reload(element). Unpoly 2 would sometimes guess the wrong target, causing the wrong fragment to be updated. This is why target derivation has been reworked to be more strict in Unpoly 3:

  • ⚠️ A longer, but stricter list of possible patterns is used to derive a target selector.

    The following patterns are configured by default:

    up.fragment.config.targetDerivers = [
      '[up-id]',        // [up-id="foo"]
      '[id]',           // #foo
      'html',           // html
      'head',           // head
      'body',           // body
      'main',           // main
      '[up-main]',      // [up-main="root"]
      'link[rel]',      // link[rel="canonical"]
      'meta[property]', // meta[property="og:image"]
      '*[name]',        // input[name="email"]
      'form[action]',   // form[action="/users"]
      'a[href]',        // a[href="/users/"]
      '[class]',        // .foo (filtered by up.fragment.config.badTargetClasses)

    Note that an element's tag name is no longer considered a useful target selector, except for unique elements like <body> or <main>.

  • ⚠️ Before a target selector is used, Unpoly 3 will verify whether it would actually match the targeted element.

    If it matches another element, another target derivation pattern is attempted. If no pattern matches, an error up.CannotTarget is thrown.

    If you see an up.CannotTarget error while upgrading to Unpoly 3, this probably indicates a bug in your app concerning elements with ambiguous selectors. You should fix it by giving those elements a unique [id] attribute.

    Verification of derived targets may be disabled with up.fragment.config.verifyDerivedTarget = false.

  • [up-poll] will only work on elements for which we can derive a good target selector.
  • [up-hungry] will only work on elements for which we can derive a good target selector.
  • Added a new function up.fragment.isTargetable(). It returns whether we can derive a good target selector for the given element.
  • When up.fragment.toTarget() is called with a string, the string is now returned unchanged.

Keepable elements

  • [up-keep] now preserves the playback state of started <audio> or <video> elements.
  • You may now use [up-keep] within [up-hungry] elements (reported by @foobear).
  • The event up:fragment:kept was removed. There is still up:fragment:keep.
  • The render option { keep } was renamed to { useKeep }. An [up-use-keep] attribute for links and forms was added.
  • Setting the value of [up-keep] to a selector for matching new content is no longer supported

Extensive render hooks

Unpoly 3 expands your options to hook into specific stages of the rendering process in order to change the result or handle error cases:

  • Rendering functions now accept a wide range of callback functions. Using a callback you may intervene at many points in the rendering lifecycle:

      url: '/path',
      onLoaded(event)        { /* Content was loaded from cache or server */ },
      focus(fragment, opts)  { /* Set focus */ },
      scroll(fragment, opts) { /* Set scroll positions */ },
      onRendered(result)     { /* Fragment was updated */ },
      onFailRendered(result) { /* Fragment was updated from failed response */ },
      onRevalidated(result)  { /* Stale content was re-rendered */ },
      onFinished(result)     { /* All finished, including animation and revalidation */ }
      onOffline(event)       { /* Disconnection or timeout */ },
      onError(error)         { /* Any error */ }
  • Callbacks may also be passed as HTML attributes on links or forms, e.g. [up-on-rendered] or [up-on-error].
  • To run code after all DOM changes have concluded (including animation and revalidation), you may now await up.render().finished. The existing callback { onFinished } remains available.
  • The up:fragment:loaded event has new properties { revalidating, expiredRequest }. This is useful to handle revalidation requests.
  • The up:fragment:loaded gets a new event.skip() which finishes the render pass without changes. Programmatic callers are fulfilled with an empty up.RenderResult. This is in contrast to event.preventDefault()m which aborts the render pass and rejects programmatic callers with an up.AbortError.
  • You may use up.fragment.config.skipResponse to configure global rules for responses that should be skipped. By default Unpoly skips:
    • Responses without text in their body.

      Such responses occur when a conditional request in answered with HTTP status 304 Not Modified or 204 No Content.

    • When revalidating, if the expired response and fresh response have the exact same text.

  • Callbacks to handle failed responses, now begin with the prefix onFail, e.g. { failOnFinished } becomes { onFailFinished }.

Various changes

  • You may now target the origin origin using :origin. The previous shorthand & has been deprecated.
  • ⚠️ When Unpoly uses the { origin } to resolve ambiguous selectors, that origin is now also rediscovered in the server response. If the origin could be rediscovered, Unpoly prefers matching new content closest to that.
  • Added a new property up.RenderResult#fragment which returns the first updated fragment.
  • The property up.RenderResult#fragments now only contains newly rendered fragments. It will no longer contain:
    • Kept elements.
    • Existing fragments that got new content appended or prepended.
    • Existing fragments that had their inner HTML replaced ({ content }).
  • New experimental function up.fragment.matches(). It returns whether the given element matches the given CSS selector or other element.
  • The function up.fragment.closest() is now stable.
  • Fix a memory leak where swapping an element did not clear internal jQuery caches.
  • Support prepending/appending content when rendering from a string using up.render({ content }) and up.render({ fragment }).
  • When prepending/appending content you may now also target ::before and ::after pseudos (double colon) in addition to :before and :after.
  • New fragments may now have HTML attributes containing the verbatim string <script> (fixes #462)

Custom JavaScript

Attaching data to elements

Unpoly 3 makes it easier to work with element data:

  • Simple data key/values can now be attached to an element using standard HTML5 [data-*] attributes (in addition to [up-data]).
  • The data argument passed to a compiler is merged from both [data-*] and [up-data] attributes. These three elements produce the same compiler data:

    <div up-data='{ "foo": "one", "bar": "two" }'></div>
    <div data-foo='one' data-bar='two'></div>
    <div up-data='{ "foo": "one" }' data-bar='bar'></div>
  • When reloading or validating, element data can now be forwarded with a { data } option:
  • When reloading or validating, element data can now be preserved with a { keepData } option:


  • up.hello() is now idempotent.

    You can call up.hello() on the same element tree multiple times without the fear of side effects.

    Unpoly guarantees that each compiler only ever runs once for a matching elements.

  • You can now register compilers after content was rendered.

    New compilers registered after booting automatically run on current elements. This makes it easier to split your compilers into multiple files that are then loaded as-needed.

    Note that compilers with a { priority } will only be called for new content, but not for existing content.

  • Compilers now accept an optional third argument with information about the current render pass:

    up.compiler('.user', function(element, data, meta) {
      console.log(meta.response.text.length)        // => 160232
      console.log(meta.response.header('X-Course')) // => "advanced-ruby"
      console.log(meta.layer.mode)                  // => "root"
      console.log(meta.revalidating)                // => true

Various changes

  • ⚠️ Unpoly now executes <script> tags in new fragments.

    You may disable this behavior with up.fragment.config.runScripts = false (this was the default in Unpoly 2).

    Note if you include your application bundle in your <body> it may now be executed multiple times if you're swapping the <body> element with Unpoly. We recommend moving your <script> tags into the head with <script defer>.

  • When a compiler throws an error, rendering functions like up.render() or up.submit() now reject with an error.
  • Fixed a bug where matching elements in the <head> were not compiled during the initial page load.


Foreign overlays

The overlays of Unpoly 2 would sometimes clash with overlays from a third party library ("foreign overlay"). E.g. clicking a foreign overlay would closes an Unpoly overlay, or Unpoly would steal focus from a foreign overlay.

Unpoly 3 lets you configure selectors matching foreign overlays using up.layer.config.foreignOverlaySelectors. Within a foreign overlay Unpoly will no longer have opinions regarding layers or focus.

Various changes

Passive updates

Hungry elements

  • You may now update [up-hungry] elements for updates of any layer by setting an [up-if-layer=any] attribute.

    A use case for this are notification flashes that are always rendered within the application layout on the root layer.

  • You may now restrict updating of [up-hungry] elements for updates that change history by setting an [up-if-history] attribute.

    A use case is a <link rel="canonical"> element that is related to the current history entry.

  • The render option { hungry } was renamed to { useHungry }. An [up-use-hungry] attribute for links and forms was added.
  • You may now use [up-keep] within [up-hungry] elements (reported by @foobear).


  • Polling is no longer disabled on poor connections. Instead the polling frequency is halved. This can be figured in
  • [up-poll] now prints fatal errors to the log.
  • [up-poll] now logs a message when it skips polling, e.g. when the tab is hidden or a fragment is on a background layer.
  • [up-poll] gets new attribute [up-keep-data] to preserve the data of the polling fragment
  • Targeted fragments are now marked with an .up-loading class while a request is loading.

    By styling elements with this class you can highlight the part of the screen that's loading.

    Note that .up-loading is added in addition to the existing .up-active class, which is assigned to the link, form or field that triggered a request.


  • The log now shows which user interaction triggered an event chain.
  • up.emit() now only prints user events when the user has enabled logging.
  • Unpoly now logs when an event like up:link:follow or up:form:submit has prevented a render pass.
  • Unpoly now logs when there was no new content to render.
  • Unpoly now logs when we're rendering a failed response using fail-prefixed options.



  • Smooth scrolling with { behavior: 'smooth' } now uses the browser's native smooth scrolling implementation.

    This gives us much better performance, at the expense of no longer being able to control the scroll speed, or the detect the end of the scrolling motion.

  • ⚠️ Removed property up.viewport.config.scrollSpeed without replacement.
  • ⚠️ Removed the option { scrollSpeed } without replacement.
  • ⚠️ up.reveal() no longer returns a promise for the end of a smooth scrolling animation.
  • ⚠️ up.viewport.restoreScroll() no longer returns a promise for the end of a smooth scrolling animation. Instead if returns a boolean value indicating whether scroll positions could be restored
  • Instant (non-smooth) scrolling is now activated using { behavior: 'instant' } instead of { behavior: 'auto' }.
  • You may now attempt multiple scrolling strategies in an [up-scroll] attribute.

    The strategies can be separated by an or e.g. [up-scroll="hash or :main"]. Unpoly will use the first applicable strategy.

  • You may now pass alternate strategies when scroll position could not be restored.

    E.g. { scroll: ['restore', 'main' ] } or [up-scroll="restore or main"]

  • Fix a bug where when attempting to restore scroll positions that were never saved for the current URL, all scroll positions were reset to zero.
  • When a render pass results in no new content, the { scroll } option now is still processed.
  • When scrolling to a fragment, obstructing elements that are hidden are now ignored.


  • You may now attempt multiple focus strategies in an [up-focus] attribute.

    The strategies can be separated by an or e.g. [up-focus="hash or :main"]. Unpoly will use the first applicable strategy.

  • Focus is now saved and restored when navigating through history:
  • When rendering without navigation or an explicit focus strategy, Unpoly will now preserve focus by default.
  • Links with an [up-instant] attribute are now focused when being followed on mousedown. This is to mimic the behavior of standard links.
  • When a render pass finishes without new content, the { focus } option now is still processed.
  • Fix a bug where focus loss was not detected when it occurred in a secondary fragment in a multi-fragment update.
  • <label for="id"> elements now always focus a matching field in the same layer, even when fields with the same IDs exist in other layers.


Preventing concurrent form interaction

  • Forms can now be disabled while they are submitting. To do so set an [up-disable] attribute to the <form> element or pass a { disable } option to a render function.

    By default all fields and buttons in that forms are disabled.

    To only disable submit buttons, pass a selector like [up-disable="button"].

    To only disable some fields or buttons, pass a selector that matches these fields or their container, e.g. [up-disable=".money-fields"]).

  • Fields being observed with [up-validate] and [up-watch] may also disable form elements using an [up-watch-disable] attribute:

    <select up-validate=".employees" up-watch-disable=".employees">

Batched validation for forms where everything depends on everything

Sometimes we don't want to disable forms while working, either because of optics (gray fields) or to not prevent user input.

Unpoly 3 has a second solution for forms with many [up-validate] dependencies that does not require disabling:

  • ⚠️ Multiple elements targeted by [up-validate] are now batched into a single render pass with multiple targets. Duplicate or nested target elements are consolidated.

    This behavior cannot be disabled.

  • Validation will only have a single concurrent request at a time. Additional validation passes while the request is in flight will be queued.
  • Form will eventually show a consistent state, regardless how fast the user clicks or how slow the network is.

See Dependent fields for a full example.

Watching fields for changes

Various changes make it easier to watch fields for changes:

  • The function up.observe() has been renamed to
  • The attribute [up-observe] has been renamed to [up-watch].
  • Added many options to control watching, validation and auto-submission:
    • You can now control which events are observed by setting an [up-watch-event] attribute or by passing a { watch } option.
    • You can now control whether to show navigation feedback while an async callback is working by setting an [up-watch-feedback] attribute or by passing a { feedback } option.
    • You can now debounce callbacks by setting an [up-watch-delay] attribute or by passing a { delay } option.
    • You can now disable form fields while an async callback is working by setting an [up-watch-deisable] attribute or by passing a { disable } option.
    • All these options can be used on individual fields or set as default for multiple fields or entire forms.
  • Delayed callbacks no longer run when the watched field was removed from the DOM during the delay (e.g. by the user navigating away)
  • Delayed callbacks no longer run when the watched field was aborted during the delay (e.g. by the user submitting the containing form)
  • Sometimes fields emit non-standard events instead of change and input. You may now use up.form.config.watchInputEvents and up.form.config.watchCangeEvents to normalize field events so they become observable as change or input.
  • Date inputs (<input type="date">) are now (by default) validated on blur rather than on change (fixes #336).
  • The configuration up.form.config.observeDelay has been renamed to up.form.config.watchInputDelay.
  • The this in an [up-watch] callback is now always bound to the element that has the attribute (fixes #442).
  • ⚠️ The function (formerly up.observe()) no longer accepts an array of elements. It only accepts a single field, or an element containing multiple fields.

Various changes

  • ⚠️ The up.validate() function now rejects when the server responds with an error code.
  • The up:form:validate event has a new property { params }. Listeners may inspect or mutate params before they are sent.
  • New function up.form.submitButtons(form) that returns a list of submit buttons in the given form.
  • New function that returns the form group (tuples of label, input, error, hint) for the given input element.
  • ⚠️ Replaced [up-fieldset] with [up-form-group].
  • Replaced up.form.config.validateTargets with up.form.config.groupSelectors. Configured selectors must no longer contain a :has(:origin) suffix, as this is now added automatically when required.
  • All functions with a { params } option now also accept a FormData value.
  • The [up-show-for] and [up-hide-for] attributes now accept values with spaces. Such values must be encoded as a JSON array, e.g. <element up-show-for='["John Doe", "Jane Doe"]'>. Fixes #78.
  • ⚠️ When submitting a form, { origin } is now the element that triggered the submission:
    • When a form is submitted, the :origin is now the submit button (instead of the <form> element)
    • When a form is submitted by pressing Enter within a focused field sets that field as the { origin }, the :origin is now the focused field (instead of the <form> element)
    • When a form is watched or validated, the :origin is now the field that was changed (instead of the <form> element)
    • To select an active form with CSS, select form.up-active, form:has(.up-active) { ... }.
  • Fix (formerly up.observe()) crashing when passed an input outside a form.

Network requests

Cache revalidation

In Unpoly 3, cache entries are only considered fresh for 15 seconds. When rendering older cache content, Unpoly automatically reloads the fragment to ensure that the user never sees expired content. This process is called cache revalidation.

When re-visiting pages, Unpoly now often renders twice:

  • An initial render pass from the cache (which may be expired)
  • A second render pass from the server (which is always fresh)

This caching technique allows for longer cache times (90 minutes by default) while ensuring that users always see the latest content.

Servers may observe conditional request headers to skip the second render pass if the underlying data has not changed, making revalidation requests very inexpensive.

That said, the following changes were made:

  • ⚠️ After rendering stale content from the cache, Unpoly now automatically renders a second time with fresh content from the server (revalidation).

    To disable cache revalidation, set up.fragment.config.navigateOptions.revalidate = false.

  • The cache now distinguishes between expiration (marking cache entries as stale) and eviction (completely erasing cache entries).
  • The old concept of clearing has been replaced with expiring the cache:
  • Features have been added to evict cache entries:
    • New configuration lets you define which requests evict existing cache entries.
    • ⚠️ By default Unpoly will expire, but not evict any cache entries when a request is made.

      To restore Unpoly 2's behavior of evicting the entire cache after a request with an unsafe HTTP method, configure the following: = (request) => !request.isSafe()
    • New configuration (default is 90 minutes).
    • Added response header X-Up-Evict-Cache.
    • Added function up.cache.evict(pattern).
    • Added configuration
  • Cache revalidation happens after up.render() settles.

    To run code once all render passes have finished, pass an { onFinished } callback or await up.render(..).finished.

  • Cache revalidation can be controlled through a render option { revalidate } or a link attribute [up-revalidate].

    The default option value is { revalidate: 'auto' }, which revalidates if up.fragment.config.autoRevalidate(response) returns true. By default this configuration returns true if a response is older than

Conditional requests

  • Unpoly now supports conditional requests. This allows your server to skip rendering and send an empty response if the underlying data has not changed.

    Common use cases for conditional requests are polling or cache revalidation.

  • Unpoly now remembers the standard Last-Modified and E-Tag headers a fragment was delivered with.

    Header values are set as [up-time] and [up-etag] attributes on updated fragment. Users can also set these attributes manually in their views, to use different ETags for individually reloadable fragments.

  • When a fragment is reloaded (or polled), these properties are sent as If-Modified-Since or If-None-Match request headers.
  • Server can render nothing by sending status 304 Not Modified or status 204 No Content.
  • Reloading is effectively free with conditional request support.
  • ⚠️ The header X-Up-Reload-From-Time was deprecated in favor of the standard If-Modified-Since.

Handling connection loss

Unpoly lets you handle many types of connection problems. The objective is to keep your application accessible as the user's connection becomes slow, flaky or goes away entirely.

Unpoly 3 lets you handle connection loss with an { onOffline } or [up-on-offline] callback:

<a href="..." up-on-offline="if (confirm('You are offline. Retry?')) event.retry()">Post bid</a>

You may also configure a global handler by listening to up:request:offline (renamed from up:request:fatal): :

up.on('up:fragment:offline', function(event) {
  if (confirm('You are offline. Retry?')) event.retry()

You may also do something other than retrying, like substituting content:

up.on('up:fragment:offline', function(event) {
  up.render(, { content: "You are offline." })

Handling "Lie-Fi"

Often our device reports a connection, but we're effectively offline:

  • Smartphone in EDGE cell
  • Car drives into tunnel
  • Overcrowded Wi-fi with massive packet loss

Unpoly 3 handles Lie-Fi with timeouts:

  • ⚠️ All requests now have a default timeout of 90 seconds (
  • Timeouts will now trigger onOffline() and use your offline handling.
  • Customize timeouts per-request by passing a { timeout } option or setting an [up-timeout] attribute.

Expired pages remain accessible while offline

With Unpoly 3, apps remain partially accessible when the user loses their connection:

  • Cached content will remain navigatable for 90 minutes.
  • Revalidation will fail, but not change the page and trigger onOffline().
  • Clicking uncached content will not change the page and trigger onOffline().

While Unpoly 3 lets you handle disconnects, it's not full "offline" support:

  • To fill up the cache the device must be online for the first part of the session (warm start)
  • The cache is still in-memory and dies with the browser tab

For a comprehensive offline experience (cold start) we recommend a service worker or a canned solution like UpUp (no relation to Unpoly).

More control about the progress bar

  • You may now demote requests to the background by using { background: true } or [up-background] when rendering or making a request

    Background requests are de-prioritized when the network connection is saturated.

    Background requests don't trigger up:network:late or show the progress bar.

  • Polling requests are demoted to the background automatically.
  • Preload requests are demoted to the background automatically.
  • You may now set a custom response times over which a request is considered late by using { badResponseTime } or [up-bad-response-time] when rendering or making a request

    This allows you to delay the up:network:late event or show the progress bar later or earlier.

    The default can now also be a Function(up.Request): number instead of a constant number value.

Caching of optimizing responses

Unpoly has always allowed server-side code to inspect request headers to customize or shorten responses, e.g. by omitting content that isn't targeted. Unpoly makes some changes how optimized responses are cached:

  • ⚠️ Requests with the same URL and HTTP method, but different header values (e.g. X-Up-Target) now share the same cache entry.
  • ⚠️ If a server optimizes its response, all request headers that influenced the response should be listed in a Vary response header.

    A Vary header tells Unpoly to partition its cache for that URL so that each request header value gets a separate cache entries.

    You can set a Vary header manually from your server-side code. You may also be using a library like unpoly-rails that sets the Vary header automatically.

  • Sending Vary headers also prevents browsers from using an optimized response for full page loads.
  • The configuration has been removed.

Support for Unicode characters in HTTP headers

  • When Unpoly writes JSON into HTTP request headers, high ASCII characters are now escaped. This is due to a limitation in HTTP where only 7-bit characters can be transported safely through headers.
  • The header X-Up-Title is now a JSON-encoded string, surrounded by JSON quotes.

Detecting failure when the server sends wrong HTTP status

Unpoly requires servers to send an HTTP error code to signal failure. E.g. an invalid form should render with HTTP 422 (Unprocessable Entity).

However, Misconfigured server endpoints may send HTTP 200 (OK) for everything. This is not always easy to fix, e.g. when screens are rendered by libraries outside your control. Unpoly 3 addresses this with the following changes:

  • Listeners to up:fragment:loaded can now can force failure by setting = true.
  • You may use to configure a global rule for when a response is considered to have failed.

Various changes

  • You may now pass FormData values to all functions that also accept an up.Params object.
  • When a request is scheduled and aborted within the same microtask, it no longer touches the network.
  • now defaults to 6 (3 while reducing requests)
  • ⚠️ When calling up.request() manually, the request is now only associated with the current layer if either { origin, layer, target } option was passed.

    If neither of these options are given, the request will send no headers like X-Up-Target or X-Up-Mode. Also, since the request is no longer associated with the layer, it will not be aborted if the layer closes.

  • The function has been deprecated. Use ! instead.
  • The events up:request:late and up:request:recover were renamed to up:network:late and up:network:recover respectively. We may eventually re-introduce up:request:late and up:request:recover to support tracking individual requests (but not now).
  • New method up.Request#header() to access a given header.
  • The method up.Response#getHeader() was renamed to up.Response#header(). It is now stable.
  • The property up.Response.prototype.request is now internal API and should no longer be used.
  • Disabling the cache with = 0 is no longer supported. To disable automatic caching during navigation, set up.fragment.config.navigateOptions.cache = false instead.

Utility functions

Support for iterable objects

Deprecated functions in favor of native browser API

DOM helpers

Deprecated functions in favor of native browser API

Various changes

  • ⚠️ up.element.booleanAttr() now returns true for a attribute value that is present but non-boolean.

    For example, the attribut value [up-instant='up-instant'] is now considered true.

    Previously it returned undefined.


Reworked documentation

In our ongoing efforts to evolve Unpoly's documentation from an API reference to a guide, we have added several documentation pages:

All existing documentation pages from Unpoly 2 remain available:

Migration polyfills

  • New polyfills for almost all functionality that was deprecated in this version 3.0.0.
  • When unpoly-migrate.js migrates a renamed attribute, the old attribute is now removed.
  • Fix a up where unpoly-migrate.js would not rewrite the deprecated { reveal } option when navigating.
  • ⚠️ The legacy option up.render({ data }) and up.request({ data }) is no longer renamed to { params } (renamed in Unpoly 0.57).

    Unpoly now uses the { data } option to preserve element data through reloads.


  • A new experimental event up:framework:booted is emitted when the framework has booted and the initial page has been compiled.
  • All errors thrown by Unpoly now inherit from up.Error.

    This makes it easier to detect the type of exception in a catch() clause:

    ```js try { await up.render('main', { url: '/foo' }) } catch (exception) { if (exception instanceof up.Error) { // handle Unpoly exception } else { // re-throw unhandled exception throw exception } }

Dropped support for legacy technologies

Dropped support for IE11 and legacy Edge

⚠️ Unpoly 3 drops support for Internet Explorer 11 and legacy Edge (EdgeHTML).

Unlike other breaking changes, support cannot be restored through unpoly-migrate.js. If you need to support IE11, use Unpoly 2.

The new compatibility targets for Unpoly 3 are major evergreen browsers (Chrome, Firefox, Edge) as well as last two major versions of Safari / Mobile Safari.

ES5 build has been replaced with an ES6 build

⚠️ Unpoly no longer ships with an version transpiled down to ES5 (unpoly.es5.js). Instead there is now a ES6 version (unpoly.es6.js).

Since most modern browsers now have great JavaScript support, we encourage you to try out the untranspiled distribution (unpoly.js), which has the smallest file size.

jQuery helpers are deprecated

jQuery helper functions have been moved to unpoly-migrate.js:

Unpoly 2 maintenance is ending

  • With the release of Unpoly we're ending maintenance of Unpoly 2. Expect little to no changes to Unpoly 2 in the future.
  • GitHub issues that have been fixed in Unpoly 3 will be closed.
  • The legacy documentation for Unpoly 2.x has been archived to
  • The code for Unpoly 2 can be found in the 2.x-stable branch.


If you're upgrading from an older Unpoly version you should load unpoly-migrate.js to polyfill deprecated APIs. Changes handled by unpoly-migrate.js are not considered breaking changes.

See our upgrading guide for details.