Skip to content
Back to skills

Resilient Ui States

ASecurity

Design every view that fetches or mutates data around its full state machine — loading, success, empty, and error are all mandatory, none optional — with error handling that fails at the right boundary and produces a specific, actionable message. Use whenever building or reviewing a component that does async data fetching, mutation, or submission. Trigger on "loading state", "error state", "empty state", "skeleton", "error boundary", "form validation", "what happens if this fails", "spinner f...

  • 8 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 23, 2026
ai-agentsreactnextjsfrontend

Security analysis

A100/100

Scanned September 23, 2026

npx -y skills add annatchijova/SKILLS --skill resilient-ui-states --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Resilient Ui States?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Resilient Ui States
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/annatchijova-resilient-ui-states/badge)](https://www.skillsdirectory.com/skills/annatchijova-resilient-ui-states)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: resilient-ui-states
description: Design every view that fetches or mutates data around its full state machine — loading, success, empty, and error are all mandatory, none optional — with error handling that fails at the right boundary and produces a specific, actionable message. Use whenever building or reviewing a component that does async data fetching, mutation, or submission. Trigger on "loading state", "error state", "empty state", "skeleton", "error boundary", "form validation", "what happens if this fails", "spinner forever", "blank screen", "something went wrong". Not about visual polish of these states — about which states are required to exist and where failure is caught.
---

# Resilient UI States

A view with data has (at minimum) four states, and treating any of them as
optional is where the incident starts: the loading state that never resolves
because there's no timeout, the empty state that doesn't exist so the user
sees a blank screen and assumes the app is broken, the error that's caught
three components up and takes the whole page down with it.

Composes with the library:

- **honest-degradation** — this skill's error/empty states are the frontend
  instance of that skill's PASS/WARN/FAIL discipline: never render a
  best-effort or partial result as if it were the complete one
- **validate-at-the-boundary** — the error state produced by a rejected input
  is this skill's territory (how it's surfaced); the rejection logic itself
  is that skill's
- **frontend-accessibility-by-construction** — an error/loading state that
  isn't announced to assistive tech doesn't exist for a screen-reader user;
  see that skill for `aria-live`/`role="alert"` mechanics

---

## The four mandatory states (methodological decision)

Every component that depends on async data has exactly these states, and
**MUST** render something intentional for each — never assume one "won't
happen":

| State | MUST render | Common failure when skipped |
|---|---|---|
| Loading | A distinct, bounded-time indicator (skeleton or spinner) | Blank screen read as broken; or spinner with no timeout, spins forever on a hung request |
| Success | The actual data | (rarely skipped — this is the state everyone builds for) |
| Empty | A state distinct from both loading and error, explaining *why* it's empty and what to do next | Renders as an empty loading skeleton or an empty error — user can't tell "no results" from "still loading" from "broken" |
| Error | A specific, actionable message tied to what actually failed | Generic "Something went wrong", or the loading spinner just... stops, with no explanation |

**MUST** distinguish empty from error from still-loading in the rendered
output — these are three different facts and collapsing any two of them is a
comprehension failure, not just a polish gap.

## Error boundaries: scope the blast radius

Source: [Next.js error handling docs](https://nextjs.org/docs/app/getting-started/error-handling) (framework-level convention); general React error boundary pattern.

**SHOULD** place error boundaries per independent feature/section, not one
global boundary at the app root. **Applies when** a page has multiple
independently-useful sections (e.g. a dashboard with several widgets) — one
widget's fetch failure should not blank the whole page. **Does not apply**
when the failing section's data is load-bearing for everything else on the
page (e.g. auth state) — in that case a page-level boundary is correct,
because partial rendering would be actively misleading.

## Form validation timing and structure

Source: [TetraLogical, form validation and error messages](https://tetralogical.com/blog/2024/10/21/foundations-form-validation-and-error-messages/).

- **SHOULD** validate on blur, not on every keystroke — validating on
  keystroke punishes the user mid-typing for an incomplete value that isn't
  wrong yet, it's just unfinished.
- **MUST** associate the error with its field via `aria-invalid="true"` and
  `aria-describedby` pointing at the error text — a red border with no
  programmatic link is invisible to a screen reader.
- **SHOULD** render a summary of all errors at the top of the form on submit,
  in addition to inline errors, for forms with more than a couple of fields —
  a submit that silently scrolls to the first error loses everyone who tabs
  or uses a screen reader.
- **MUST** use `role="alert"` or an `aria-live` region for the error summary
  so it's announced without requiring the user to find it visually.

## Failure modes / anti-patterns

- **Infinite spinner**: no timeout or retry-with-backoff on a hung request —
  the loading state has no maximum duration, so a stalled network call is
  indistinguishable from a working one.
- **Generic error message**: "Something went wrong" / "Error" with no
  specifics and no next action — technically correct, operationally useless.
- **Empty-as-loading**: an empty result set renders the same skeleton as the
  loading state, so the user waits for data that will never arrive.
- **One error boundary to rule them all**: a single unhandled rejection in
  one widget blanks the entire page instead of just that widget.
- **Optimistic UI with no rollback path**: a mutation is rendered as
  succeeded before the server confirms, with no visible correction if it
  actually fails — the user acts on a state that silently reverts.
- **Validation on keystroke**: the field turns red while the user is still
  typing a valid value, before they've finished.
- **Form reset on error**: a failed submission clears fields the user already
  filled in, forcing them to redo work — see `non-overwhelming-ux` for why
  this is a cognitive-load failure, not just an annoyance.

## Verifiable checks

- [ ] Every component with async data has distinguishable loading, success,
      empty, and error renders (test: force each state and screenshot/diff).
- [ ] The loading state has a maximum duration (timeout) with a defined
      fallback, or is provably bounded by the request itself.
- [ ] The empty state names *why* it's empty when the reason is knowable
      (no results for this filter vs. nothing created yet) and offers a next
      action.
- [ ] Every error message is specific to the failure (network vs. validation
      vs. permission vs. server error) — grep for generic strings like
      "Something went wrong" and confirm each one has a fallback only, not a
      primary use.
- [ ] Error boundaries are scoped to independent sections, not a single
      app-root boundary, unless the failing section is load-bearing for the
      rest of the page.
- [ ] Form errors are both inline (per field, with `aria-invalid` +
      `aria-describedby`) and summarized (`role="alert"` region) on submit
      for forms beyond 1-2 fields.
- [ ] A failed submission preserves user-entered data — nothing is cleared on
      error.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.