Skip to content

Object states

3 min read · Updated 1 Nov 2026

Show each object’s state over its host, inside its card, so the rest of the page keeps loading. The loading placeholder’s look is in Loaders; what readiness reports is in Readiness.

Object states: Each affected object keeps its space and shows its own state and next action.

Illustrative example with synthetic data. Each affected object keeps its space and shows its own state and next action.

Give a state to every object that queries: mounted objects and the figures you draw from your own queries.

The page needs Use
A placeholder before the runtime is ready The boot loader
One message for a cause shared by every object Page states
State Shown when Message Action
Loading From mount, or from sending your query, until a result The loading placeholder None
Ready handle.ready resolves rendered; your query returns rows The object None
No data Your query returns no rows “No data for the current filters.” Clear filters, when selections are applied
Error handle.ready resolves error; your query rejects; a field is missing; its definition is refused “This chart could not be loaded.” Retry, except for a missing field or a refused definition
Timeout handle.ready resolves timeout while the page is visible “Still loading.” in a strip along the bottom of the host Retry
  • Word no data and error differently. No data means the query ran; changing the filters can help.
  • Show a timeout as a strip, not a cover: the chart stays mounted and can still render, and the strip stays until the reader chooses Retry, because no later render is reported.
  • Ignore unmounted.
  • Show a missing field or a refused definition as an error without Retry: retrying sends the same configuration again. Do not suggest changing filters for it. An operation refused while you prepare an object is not an error state: mount what was accepted and log the refusal. See Check operation results.
<figure class="chart" data-object-id="revenue-by-week" data-state="loading" aria-busy="true">
<figcaption>
<h2 class="chart-title">Revenue by week</h2>
<p class="chart-subtitle">USD, all regions</p>
</figcaption>
<button class="chart-menu" type="button" aria-haspopup="dialog" aria-label="Options for Revenue by week">
<!-- icon -->
</button>
<div class="chart-host"></div>
<div class="chart-state">
<p class="chart-state__message"></p>
<button class="chart-state__action" type="button" hidden></button>
</div>
</figure>
  • data-state on the card drives the CSS.
  • Set aria-busy="true" while loading.
  • Do not make the message a live region; many objects failing together would announce many times. Announce a page-level summary with a toast instead.
.chart-state {
/* The host's cell; see /app-components/object-frame/#css */
grid-area: 3 / 1 / 4 / -1;
z-index: 1;
display: none;
place-content: center;
justify-items: center;
gap: 8px;
padding: 12px;
background: var(--surface);
color: var(--muted);
text-align: center;
}
.chart:is([data-state='loading'], [data-state='empty'], [data-state='error']) > .chart-state {
display: grid;
}
/* A strip, so a chart that renders after its deadline stays visible. */
.chart[data-state='timeout'] > .chart-state {
display: flex;
align-self: end;
align-items: center;
justify-content: space-between;
padding: 4px 8px;
}
.chart-state__message {
margin: 0;
}
  • The layer shares the host’s grid cell, so the host keeps its size in every state. Do not hide the host with display: none; see Give every host a definite box.
  • An author display rule on the action overrides its hidden attribute; qualify such a rule with :not([hidden]).
type ObjectState = 'loading' | 'ready' | 'empty' | 'error' | 'timeout';
const setState = (card: HTMLElement, state: ObjectState, message = '', action?: 'clear' | 'retry') => {
card.dataset.state = state;
card.setAttribute('aria-busy', String(state === 'loading'));
const text = card.querySelector('.chart-state__message');
const button = card.querySelector<HTMLButtonElement>('.chart-state__action');
if (text) text.textContent = message;
if (button) {
// Hiding a focused button would drop focus to the page; move it to the card's menu first.
if (action === undefined && button === document.activeElement) {
card.querySelector<HTMLButtonElement>('.chart-menu')?.focus();
}
button.hidden = action === undefined;
button.dataset.action = action ?? '';
button.textContent = action === 'clear' ? 'Clear filters' : action === 'retry' ? 'Retry' : '';
}
};

Mounted objects. Handle handle.ready as Readiness shows and map the result with the table above. Ignore a result from a mount that a later mount has replaced.

Retry unmounts and mounts the object again through its lifecycle chain, which costs one query.

Background tabs. If timeout resolves while document.hidden is true, keep the loading state and mount the object again on visibilitychange. handle.ready does not report a later render; see Readiness.

Your own queries. Re-query on applied changes and ignore superseded results, as Re-query your own objects only on applied changes shows. Show no data when the rows are empty and error when the query rejects. Log the query with the error; see Reading the answer.

Clear filters clears the whole selection group, not only this object’s filters, including a period applied as a selection; see Periods and date ranges. Wire both actions once for the document:

declare const selections: { clearAllSelections(group: string): void };
declare const GROUP: string;
declare function retry(id: string): void;
document.addEventListener('click', (event) => {
if (!(event.target instanceof Element)) return;
const button = event.target.closest<HTMLButtonElement>('.chart-state__action');
const id = button?.closest<HTMLElement>('[data-object-id]')?.dataset.objectId;
if (!button || id === undefined) return;
if (button.dataset.action === 'clear') selections.clearAllSelections(GROUP);
if (button.dataset.action === 'retry') retry(id);
});

Focus. When the action the reader used is hidden, for example Retry becoming loading, move focus to the card’s menu button.

Action Cost
Show or hide a state None
Retry One query for that object
Clear filters One query per mounted object in the group, and one per figure you re-query
  • A mounted object cannot report that it returned no rows, so the no-data state applies only to your own queries.
  • For the timeout and cancellation limits of your own queries, see Data and objects.