Skip to content

Page states

3 min read · Updated 20 Oct 2026

A page state covers the board with one message when every object shares the same cause. The state of one object is in Object states; a failed start-up is in the boot loader.

Page states: One shared cause becomes a single board-level message and recovery action.

Illustrative example with synthetic data. One shared cause becomes a single board-level message and recovery action.

Use one when the reader’s next step is the same for every object on the page.

The page needs Use
A placeholder or error while the runtime starts The boot loader
The state of one object Object states
A notice that several objects failed after an applied change An error toast
State Signal Heading Action Shown as
App needs publishing again status 409 and response.error.code app_unavailable; see Hosted app sessions “This app needs to be published again” None Covers the board
Data unavailable A page-level query rejects “The data could not be loaded”, then Astrato’s message and request ID Retry when retryable is true Covers the board
No data A page-level query returns no rows “No data for the current filters” or, without selections, “There is no data yet” Clear filters, with selections applied Covers the board
Offline navigator.onLine is false “You are offline. Data cannot be loaded or updated until the connection returns.” None A strip; the board stays
  • A page-level query is one the page runs anyway, such as a headline figure. Mounted objects do not report no rows.
  • Refused access has no documented error code. Show Astrato’s message rather than writing your own “no access” text; see Errors.
  • Republish has no action: reloading returns the same error.
  • navigator.onLine being true does not prove Astrato is reachable; a request can still fail.
<main id="main" class="app-main" tabindex="-1">
<div class="board"><!-- cards --></div>
<section class="page-state" aria-labelledby="page-state-title" hidden>
<h2 id="page-state-title" class="page-state__title" tabindex="-1"></h2>
<p class="page-state__message"></p>
<p class="page-state__request" hidden>Request ID: <code></code></p>
<button type="button" class="page-state__action" hidden></button>
</section>
</main>
<!-- Present from page load, so the change is announced. -->
<div class="offline" role="status" hidden>You are offline. The page shows the data it last loaded.</div>

When a state shows, focus its heading and make the board inert.

:root {
--offline-h: 40px;
}
.app-main {
display: grid;
}
.app-main > .board,
.app-main > .page-state {
grid-area: 1 / 1;
}
.page-state {
z-index: 2;
display: grid;
align-content: start;
justify-items: center;
gap: 12px;
/* The height of a board that fits the window, so the message is on the first screen. */
min-height: calc(100svh - var(--header-h) - var(--above-board, 0px) - 2 * var(--gap));
box-sizing: border-box;
padding: min(20svh, 160px) var(--gap) var(--gap);
background: var(--page);
text-align: center;
}
.page-state[hidden] {
display: none;
}
/* The board keeps its box, so mounted objects keep their size behind the state. */
.app-main:has(> .page-state:not([hidden])) > .board {
visibility: hidden;
}
.page-state__title {
margin: 0;
font-size: 18px;
line-height: 24px;
}
.page-state__message,
.page-state__request {
margin: 0;
max-width: 48ch;
color: var(--muted);
}
.offline {
position: fixed;
inset: auto var(--gap) var(--gap);
z-index: 10;
display: flex;
align-items: center;
box-sizing: border-box;
width: fit-content;
max-width: calc(100% - 2 * var(--gap));
height: var(--offline-h);
padding: 0 12px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
.offline[hidden] {
display: none;
}
/* Toasts stack above the strip; see /app-components/toasts/#css. */
:root:has(.offline:not([hidden])) {
--toast-bottom: calc(var(--offline-h) + 8px);
}

Hide the covered board with visibility: hidden, not display: none, so every host keeps its box; see Give every host a definite box.

import { AstratoAppRuntimeError } from '@astrato-io/app-runtime';
type PageState =
| { kind: 'republish' }
| { kind: 'unavailable'; message: string; requestId: string | undefined; retryable: boolean }
| { kind: 'no-data'; filtered: boolean };
export const classify = (error: unknown): PageState => {
if (error instanceof AstratoAppRuntimeError) {
const detail = error.response?.error;
if (error.status === 409 && detail?.code === 'app_unavailable') return { kind: 'republish' };
return {
kind: 'unavailable',
message: detail?.message ?? error.message,
requestId: detail?.requestId,
retryable: detail?.retryable === true,
};
}
return {
kind: 'unavailable',
message: error instanceof Error ? error.message : '',
requestId: undefined,
retryable: false,
};
};
  • Model reads and data-token requests can return app_unavailable, and both can happen during your initialization. Call classify in the boot loader’s catch as well, so a start-up failure gets the same wording.
  • Offer Retry only for a retryable error, and only on request.

Fill the heading, message, request ID and action from the state, then set section.hidden = false, board.inert = true and focus the heading. To hide, reverse both. Write every text with textContent.

No data. Check the rows of a page-level query you already run, after discarding superseded results; see Re-query your own objects only on applied changes. Hide the state when rows return. Clear filters calls clearAllSelections on the group; the objects stay mounted under the state and re-query as usual.

Offline.

const offline = document.querySelector<HTMLElement>('.offline');
const syncOnline = () => {
if (offline) offline.hidden = navigator.onLine;
};
addEventListener('online', syncOnline);
addEventListener('offline', syncOnline);
syncOnline();

Nothing re-queries when the connection returns. Objects that failed show Retry, and the next applied change re-queries them.

Action Cost
Show or hide a state None
No-data check on a query the page already runs None
Retry The page-level query again
Clear filters One query per mounted object in the group, and one per query you re-run
  • No error code identifies refused access.
  • No documented signal reports an expired hosted session; see Hosted app sessions.
  • Mounted objects do not report no rows.

See Data and objects.