---
title: "Show loading without flicker · Agent Wranglers"
url: https://agentwranglers.se/playbook/app-ui/data-views/loading-without-flicker
updated: 2026-10-20
licence: "All rights reserved"
---

# Show loading without flicker

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Recommended
- Chapter: [Data views](https://agentwranglers.se/playbook/app-ui/data-views)
- Rule: Show a loading indicator only when loading takes longer than 300 ms. Once shown, keep it for at least 500 ms. Reserve the final size, so nothing jumps.
- How: Start a timer when the request starts, and show the indicator when it fires. Use a skeleton with the same height as the loaded view, which keeps [layout shift](https://agentwranglers.se/playbook/website/performance/core-web-vitals) at zero. Set `aria-busy="true"` on the region while it loads. Keep the old data on screen while a refresh loads, and mark it as updating.
- Check: A browser test delays the data by 100 ms. A mutation observer records that no indicator appeared at any moment. With a 2 second delay, the indicator appears, and layout shift for the region stays under 0.01. With a 400 ms delay, the indicator stays for at least 500 ms.

Example:

```ts
const SHOW_AFTER_MS = 300;
const MIN_VISIBLE_MS = 500;

export async function withLoadingIndicator<T>(region: HTMLElement, work: Promise<T>): Promise<T> {
  let shownAt = 0;
  region.setAttribute('aria-busy', 'true');
  const timer = setTimeout(() => { shownAt = performance.now(); region.dataset.loading = 'true'; }, SHOW_AFTER_MS);
  try {
    return await work;
  } finally {
    clearTimeout(timer);
    if (shownAt) await new Promise(done => setTimeout(done, Math.max(0, MIN_VISIBLE_MS - (performance.now() - shownAt))));
    delete region.dataset.loading;
    region.removeAttribute('aria-busy');
  }
}
```

```python
import time
from playwright.sync_api import expect

WATCH_FOR_INDICATOR = """
new MutationObserver(() => {
  if (document.querySelector('[data-loading]')) window.sawIndicator = true;
}).observe(document, { subtree: true, childList: true, attributes: true });
"""

def delay_100ms(route):
    time.sleep(0.1)
    route.continue_()

def test_fast_load_never_shows_the_indicator(page):
    page.add_init_script(WATCH_FOR_INDICATOR)
    page.route("**/api/orders*", delay_100ms)
    page.goto("https://staging.example.com/orders")
    expect(page.get_by_role("table", name="Orders")).to_be_visible()
    assert page.evaluate("window.sawIndicator === true") is False
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers app interface playbook: https://agentwranglers.se/playbook/app-ui/data-views/loading-without-flicker`.

- Previous: [Give every data view all its states](https://agentwranglers.se/playbook/app-ui/data-views/every-data-state)
- Next: [Tell an empty start from an empty search](https://agentwranglers.se/playbook/app-ui/data-views/empty-states)
- [Data views](https://agentwranglers.se/playbook/app-ui/data-views)
