Skip to content

Error State

Error State gives a failed page or content region a calm layout for a truthful explanation and a safe way forward. The application writes the heading, copy, recovery controls, announcement semantics, focus behavior, and ordinary Tailwind.

It is one component, not a family of title, description, icon, action, or details wrappers. It does not catch exceptions, normalize errors, retry requests, move focus, navigate, log, or expose diagnostic data.

ServicesError.vue
Slipway and Hagfish keep distinct product treatments, while a dynamically appearing failure opts into a native alert only when the caller needs it.

Installation

The command detects Vue, React, or Svelte and copies the matching one-file source into the conventional UI directory.

Run one command from a Boring Stack application. Klean detects the framework and conventional destination, then adds the framework-native source and its direct dependencies.

Terminal
npx klean-ui add error-state

  • No initializer or configuration file
  • No framework, alias, or theme questions
  • No Klean runtime dependency

The examples also use Button. Add it separately with npx klean-ui add button, or use an existing native button or framework-native Link.

Usage

Use native alert semantics only when a failure appears dynamically and warrants interruption. Static error pages should use ordinary page or section semantics so their heading is not announced twice.

Vue

ServicesError.vue
<script setup>
import ErrorState from '@/components/ui/error-state/ErrorState.vue'
import Button from '@/components/ui/button/Button.vue'

defineProps({ failed: Boolean })
defineEmits(['retry'])
</script>

<template>
  <ErrorState v-if="failed" role="alert" aria-labelledby="services-error-title">
    <h2 id="services-error-title">Services could not load</h2>
    <p>Slipway could not reach the deployment service.</p>
    <Button type="button" @click="$emit('retry')">Try again</Button>
  </ErrorState>
</template>

React

ServicesError.jsx
import ErrorState from '@/components/ui/error-state/ErrorState.jsx'
import Button from '@/components/ui/button/Button.jsx'

export default function ServicesError({ failed, retry }) {
  if (!failed) return null

  return (
    <ErrorState role="alert" aria-labelledby="services-error-title">
      <h2 id="services-error-title">Services could not load</h2>
      <p>Slipway could not reach the deployment service.</p>
      <Button type="button" onClick={retry}>
        Try again
      </Button>
    </ErrorState>
  )
}

Svelte

ServicesError.svelte
<script>
  import ErrorState from '@/components/ui/error-state/ErrorState.svelte'
  import Button from '@/components/ui/button/Button.svelte'

  let { failed, retry } = $props()
</script>

{#if failed}
  <ErrorState role="alert" aria-labelledby="services-error-title">
    <h2 id="services-error-title">Services could not load</h2>
    <p>Slipway could not reach the deployment service.</p>
    <Button type="button" onclick={retry}>Try again</Button>
  </ErrorState>
{/if}

API

PurposeVueReactSvelte
Truthful elementasasas
Contentdefault slotchildrenchildren snippet
Root stylingclassclassNameclass

as defaults to div. Native attributes and events pass through unchanged, including a caller-authored role="alert", aria-labelledby, click handler, or data attribute.

There are no title, description, icon, actions, details, message, error, retry, variant, tone, compact, or fullPage props. Those decisions remain visible in application markup.

Static pages and dynamic alerts

A server-rendered 403, 404, 419, 429, 500, or 503 page already has normal document reading order. Use as="section" with a real heading, but do not add an alert merely because the page describes an error.

When a previously usable region changes into a failure after a request, add role="alert" in caller markup if immediate announcement is warranted. Keep the explanation specific and provide recovery only when recovery is real.

Do not automatically focus Error State. If the control that started the request remains useful, keep it mounted. If a focused retry control disappears after success, deliberately restore focus to a stable control or the newly loaded region.

Recovery stays durable

  • Retry commands are native buttons and keep their application-owned request logic.
  • Destinations are native anchors or framework-native Inertia Links, preserving modified clicks, history, and server-rendered fallbacks.
  • Preserve safe stale content, filters, and form values when recovery does not require clearing them.
  • Render only actions the current server response authorizes.
  • Do not label a permanent denial or missing page as retryable.

Klean does not accept an action array or convert descriptions into callbacks. Recovery is ordinary markup that remains understandable without client-side adaptation.

Safe diagnostics

The caller may add a native <details> element for diagnostics that are safe and genuinely useful to the current user. Never pass raw stack traces, provider responses, secrets, tokens, SQL, internal identifiers, or unsanitized HTML into a user-facing Error State.

Log private diagnostics through the application's normal observability path. User copy should explain what failed, what was preserved, and what can happen next.

Error State is not every error

  • Field validation stays beside its control and may be summarized with links to the invalid fields.
  • Alert gives contextual guidance while the surrounding content still exists.
  • Toast gives transient feedback after a mutation.
  • An application error boundary catches rendering exceptions; Error State may be the boundary's caller-authored fallback, but does not implement the boundary.
  • A deployment with a failed status is domain content, not necessarily a failed page or region.

This separation keeps the user's input intact and avoids turning every red message into the same interaction.

Accessibility

  • Use a heading level that fits the surrounding document.
  • Name a region with aria-labelledby when it should be discoverable.
  • Use role="alert" only for a newly appearing failure that needs interruption.
  • Do not combine role="alert" with another live region that repeats the same message.
  • Keep retry and navigation controls native and keyboard reachable.
  • Preserve focus by default; move it only when the interaction has a clear destination.
  • Decorative error icons use aria-hidden="true".
  • Write specific recovery copy. “Something went wrong” alone is not actionable.

Styling with Tailwind

The neutral baseline centers a wrapping column with comfortable space. class or className merges onto the root. Every icon, heading, paragraph, Link, button, and details disclosure is caller markup styled with ordinary Tailwind.

Slipway can use a calm dark region, Hagfish can keep its sharp monochrome borders, and a status page can become a left-aligned editorial layout. There is no visual-variant API.

When to use

Use Error State when a page or meaningful content region failed and the user needs an explanation, a safe recovery action, or an honest destination.

When not to use

  • Use Loading State while content is pending.
  • Use Empty State after a successful request returns no content.
  • Use Alert when important content still exists around the message.
  • Use Field validation for input-specific errors.
  • Use Toast for transient action feedback.

Complete framework source

Vue

ErrorState.vue
<script setup>
import { computed, ref, useAttrs } from 'vue'
import { twMerge } from 'tailwind-merge'

defineOptions({ inheritAttrs: false })

defineProps({
  /** Native element or framework component that truthfully fits the document. */
  as: { type: [String, Object, Function], default: 'div' }
})

const attrs = useAttrs()
const element = ref()
const rootAttrs = computed(() => {
  const { class: _class, 'data-slot': _dataSlot, ...rest } = attrs
  return rest
})

defineExpose({ element })
</script>

<template>
  <component
    :is="as"
    ref="element"
    v-bind="rootAttrs"
    data-slot="error-state"
    :class="
      twMerge(
        'flex min-h-48 w-full flex-col items-center justify-center gap-4 p-6 text-center text-gray-950 dark:text-white',
        attrs.class
      )
    "
  >
    <slot />
  </component>
</template>

React

ErrorState.jsx
import { forwardRef } from 'react'
import { twMerge } from 'tailwind-merge'

const BASE_CLASSES =
  'flex min-h-48 w-full flex-col items-center justify-center gap-4 p-6 text-center text-gray-950 dark:text-white'

const ErrorState = forwardRef(function ErrorState(
  { as: Component = 'div', className, 'data-slot': _dataSlot, ...props },
  ref
) {
  return (
    <Component
      {...props}
      ref={ref}
      data-slot="error-state"
      className={twMerge(BASE_CLASSES, className)}
    />
  )
})

export default ErrorState

Svelte

ErrorState.svelte
<script>
  import { twMerge } from "tailwind-merge";

  const BASE_CLASSES =
    "flex min-h-48 w-full flex-col items-center justify-center gap-4 p-6 text-center text-gray-950 dark:text-white";

  let {
    as = "div",
    children,
    class: className,
    "data-slot": _dataSlot,
    ...rootProps
  } = $props();

  let element = $state();

  export function getElement() {
    return element;
  }
</script>

{#if typeof as === "string"}
  <svelte:element
    this={as}
    {...rootProps}
    bind:this={element}
    data-slot="error-state"
    class={twMerge(BASE_CLASSES, className)}
  >
    {@render children?.()}
  </svelte:element>
{:else}
  {@const Component = as}
  <Component
    {...rootProps}
    bind:this={element}
    data-slot="error-state"
    class={twMerge(BASE_CLASSES, className)}
  >
    {@render children?.()}
  </Component>
{/if}

  • Loading State — represents pending content before success or failure is known.
  • Empty State — represents a successfully loaded surface with no content.
  • Alert — gives contextual guidance without replacing the surrounding region.
  • Button — supplies a native caller-owned retry command.
  • Toast — announces transient mutation feedback.
  • DataTable — may preserve safe rows while a refresh failure is explained.

All open source projects are released under the MIT License.