AbsoluteJS

Navigation & UI

Your links already work in the app, and Android Back, deep links and transitions behave the way people expect from a native app. Add a few attributes to plain HTML and you get safe areas, a tab bar and bottom sheets in every framework.

You do not change your routes or links. The app keeps each page change all-or-nothing, so a slow or failed load never leaves half a page on screen.

1
Tap a link
A tap on a link to your own site stays in the app. The page underneath stays usable while the next one loads, and body gets data-absolute-mobile-navigation-pending and aria-busy="true" so you can show progress.
2
Load the data
The app asks your server for the next page’s data. Tap again before it arrives and the first request is cancelled: the latest navigation always wins.
3
Swap the page
The new page replaces the old one, with a View Transition where the WebView supports it and the user has not asked for reduced motion. React, Svelte, Vue, Angular, HTML and HTMX pages hand over to each other cleanly.
4
Record history
History is written only once the new page is showing. A new page starts at the top with focus on its heading; Back and Forward bring back scroll, focus and form state.
When a page can’t load
The current page stays on screen with a retry button: “You are offline. Reconnect to load this page.” when the device is offline, otherwise “Unable to load this page.” A failed Back or Forward puts history back where the visible page is.

Ordinary anchors are all you need. data-absolute-link changes how one behaves, and outside the app each link still works as a normal link.

HTML
<a href="/orders/42">Order 42</a>
<a href="/checkout/review" data-absolute-link="replace">Review</a>
<a href="/orders" data-absolute-link="back">Back to orders</a>
<a href="https://help.example.com" data-absolute-link="external">Help</a>
LinkIn the app
<a href="/orders">Opens the page in the app. Links to your production origin work the same way.
data-absolute-link="replace"Opens the page in place of the current history entry.
data-absolute-link="back"Closes an open sheet, otherwise goes back. href is the fallback without JavaScript.
data-absolute-link="external"Opens an http or https page in the system browser.
<a target="_blank">Any link with a target is left to the WebView.

#Android Back

The hardware Back button and the back gesture are handled for you, in this order:

1
An open sheet closes
2
A page still loading is cancelled
3
The app goes back one page
4
On the first page, the app exits as Android expects

#Safe areas and keyboard

Every page in the app gets CSS variables and attributes on <html> describing the screen, kept up to date through rotation, the keyboard and page changes. Nothing is padded for you, so an existing responsive layout looks the same until you opt in. The same CSS works in the browser, in Capacitor and in Expo; the fallbacks apply on the web.

CSS
.app-shell {
  min-height: var(--absolute-available-height, 100dvh);
  padding:
    var(--absolute-safe-area-inset-top, 0)
    var(--absolute-safe-area-inset-right, 0)
    var(--absolute-safe-area-inset-bottom, 0)
    var(--absolute-safe-area-inset-left, 0);
}

:root[data-absolute-keyboard='visible'] .checkout-bar {
  bottom: var(--absolute-keyboard-height);
}

:root[data-absolute-network='offline'] .sync-badge {
  display: inline-flex;
}
CSS variableValue
--absolute-safe-area-inset-topAlso -right, -bottom and -left: the notch, status bar and home indicator
--absolute-keyboard-heightHeight of the on-screen keyboard
--absolute-viewport-heightAlso --absolute-viewport-width
--absolute-available-heightHeight left for content once the keyboard is open
Attribute on <html>Values
data-absolute-mobilePresent in the app
data-absolute-runtimecapacitor, expo, web or test
data-absolute-platformios, android and so on
data-absolute-form-factorphone, tablet, desktop or unknown
data-absolute-keyboardvisible or hidden
data-absolute-networkonline or offline
data-absolute-connectionwifi, cellular, ethernet, unknown or none
data-absolute-reduced-motionreduce or no-preference

The status and navigation bars follow the system’s light or dark appearance; systemBars from @absolutejs/devices changes them.

#App layout

Mark up a header, a scrolling main area and a tab bar, and the app handles safe areas, scrolling, the active tab and transitions. These are attributes on ordinary elements, not components, so the same markup works in JSX, Svelte, Vue and Angular templates, and in HTML and HTMX pages. Colors, type and spacing stay in your CSS.

HTML
<div data-absolute-app-shell>
  <header data-absolute-app-header>
    <h1>Account</h1>
    <button data-absolute-sheet-open="filters">Filters</button>
  </header>

  <main data-absolute-app-main>
    <section data-absolute-navigation-stack>
      <!-- your page content -->
    </section>
  </main>

  <nav data-absolute-tab-bar aria-label="Primary">
    <a href="/home">Home</a>
    <a href="/orders" data-absolute-tab-match="prefix">Orders</a>
    <a href="/account" data-absolute-tab-match="prefix">Account</a>
  </nav>

  <dialog id="filters" data-absolute-sheet aria-labelledby="filters-title">
    <h2 id="filters-title">Filters</h2>
    <button data-absolute-sheet-close>Done</button>
  </dialog>
</div>
AttributeOnDoes
data-absolute-app-shellContainerFills the available height, inside the safe areas
data-absolute-app-headerHeaderClears the status bar and notch
data-absolute-app-mainMainThe scrolling region; its position is restored on Back
data-absolute-navigation-stackPage viewSlides forward and back with View Transitions
data-absolute-tab-barnavClears the home indicator and sets aria-current="page" on the active tab
data-absolute-tab-match="prefix"Tab linkKeeps the tab active on nested routes; exact match otherwise
data-absolute-sheetdialogA bottom sheet with focus kept inside it
data-absolute-sheet-open="id"Button or linkOpens the sheet with that id
data-absolute-sheet-closeControl in a sheetCloses the sheet and returns focus to what opened it

Tab bars stay navigation landmarks and tabs stay links; AbsoluteJS does not add tablist roles, which describe tabs within one page.

#Sheets

A sheet is a <dialog> with data-absolute-sheet. It opens from the bottom with focus moved inside, and closes from its close control, Escape, a tap on the backdrop, Android Back or a back link, always before the page changes. One sheet is open at a time, and focus returns to the control that opened it.

#Scroll, focus and forms

Back and Forward bring a page back as it was: form values, selection, open disclosures, focus and scroll position. This state lives only in memory. It is never written to history, storage or Sync, and password, file, hidden, card-number and one-time-code fields are left out entirely. Data that must survive the app being closed belongs in your app state or Sync.

HTML
<!-- Values in this form reset when the page is opened again -->
<form data-absolute-navigation-preserve="off">…</form>

<!-- Keep this list's scroll position on Back and Forward -->
<ul data-absolute-scroll-restoration>…</ul>

<!-- Focus this element when the page opens -->
<h2 data-absolute-navigation-focus tabindex="-1">Your orders</h2>
AttributeEffect
data-absolute-navigation-preserve="off"Values inside reset when the page is recreated
data-absolute-scroll-restorationRestores this element’s scroll position too (the main region is automatic)
data-absolute-navigation-focusReceives focus on a new page, ahead of the first h1 in main, the first h1 and main

An element with autofocus takes priority over all of them.

#Events

Every event is dispatched on window and typed in TypeScript, so event.detail needs no casting.

TS
addEventListener('absolute:navigation-change', (event) => {
  const { direction, from, to } = event.detail;
  analytics.track('screen', { direction, from, to });
});

addEventListener('absolute:sheet-change', (event) => {
  if (!event.detail.open) refreshFilters(event.detail.id);
});

addEventListener('absolute:adaptive-shell-change', (event) => {
  const { keyboard, network, platform } = event.detail;
  setCompactMode(keyboard.visible || platform.formFactor === 'phone');
  setOffline(!network.connected);
});
EventDetailFires
absolute:navigation-change{ direction: "forward" | "back" | "replace", from, to }After a new page is showing
absolute:sheet-change{ id, open }When a sheet opens or closes
absolute:adaptive-shell-change{ availableHeight, keyboard, network, platform, viewportHeight, viewportWidth }On rotation, keyboard, network and safe-area changes
absolute:shell-renderednoneOnce, when the first page has painted

#In the browser

The app installs all of this for you. To use the same layout, sheets and Back handling on your website, call installAbsoluteMobileUiPrimitives from any client entry. It returns navigate, refreshDocument, requestBack and dispose; requestBack returns true when an open sheet handled it.

TS
import {
  closeAbsoluteMobileSheet,
  installAbsoluteMobileUiPrimitives,
  openAbsoluteMobileSheet
} from '@absolutejs/absolute/mobile/ui';

const mobileUi = installAbsoluteMobileUiPrimitives();

openAbsoluteMobileSheet('filters');
closeAbsoluteMobileSheet('filters');
mobileUi.requestBack();