# EmptyState

> Empty, error and loading states for a region (dashed frame with icon, title, description and actions; skeleton rows), plus the Skeleton primitive.

Source: https://docs.nasaqui.com/components/states

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/states.json
```

Four exports for the three states every data region has:

- **`EmptyState`**: nothing to show yet.
- **`ErrorState`**: loading failed. Carries an icon and words, never colour alone.
- **`LoadingState`**: skeleton rows that preview the layout that is coming.
- **`Skeleton`**: the single placeholder block used by `LoadingState`, for custom loading layouts.

`EmptyState` and `ErrorState` share one frame: a dashed, card-radius box with an optional icon tile, a title,
a description and an action row.

## When to use

- A list, table or panel has no data: `EmptyState`, with the action that fixes it.
- A request failed and the user can retry: `ErrorState`.
- A region is loading and you know roughly its shape: `LoadingState` or `Skeleton`.

## When not to use

- Inline pending work (a button saving, a refresh): use [`Spinner`](https://docs.nasaqui.com/components/spinner).
- A transient message or confirmation: use a toast.
- A status of one item: use [`Status`](https://docs.nasaqui.com/components/status) or [`Badge`](https://docs.nasaqui.com/components/badge).
- Decorative empty art on populated screens: leave an empty cell instead.

## Import

```tsx
import {
  EmptyState, ErrorState, LoadingState, Skeleton, type LoadingStateProps,
} from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Button, EmptyState } from "@fadymondy/nasaq/web";

export function NoIssues() {
  return (
    <EmptyState
      title="لا توجد مهام بعد"
      description="تظهر هنا المهام التي تنشئها أو تُسند إليك."
      actions={<Button variant="primary">مهمة جديدة</Button>}
    />
  );
}
```

## Anatomy

```
EmptyState        data-slot="empty-state"      dashed frame, role none
ErrorState        data-slot="error-state"      role="alert"
├─ icon tile      40px, rounded-control, lucide icon (aria-hidden); red text in ErrorState
├─ title          <p class="text-label">
├─ description    <p class="text-body-sm text-muted-foreground">   (max-w-sm column)
├─ children       anything extra
└─ actions        wrapping, centred row

LoadingState      data-slot="loading-state"    role="status", aria-live="polite"
├─ sr-only label
├─ skeleton × rows       data-shape="rows" (rows), "grid" (loading-card), "timeline" (loading-event)
│                         (or spinner + label when rows is 0)
└─ caption               visible text under the skeleton, when set

Skeleton          data-slot="skeleton"         aria-hidden
```

## API

### `EmptyState` and `ErrorState`

Both accept the same props (the exported `StateProps` interface):
`Omit<ComponentProps<"div">, "title">` plus:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `ReactNode` | required | Headline. |
| `icon?` | `LucideIcon` | `Inbox` (`EmptyState`), `CircleAlert` (`ErrorState`) | Glyph in the icon tile. |
| `description?` | `ReactNode` | none | Supporting text under the title. |
| `actions?` | `ReactNode` | none | Usually one primary `Button` and at most one secondary. |
| `hatch?` | `boolean` | `false` | Hatched ground (grid expression); off automatically in the native expression. |
| `children?` | `ReactNode` | none | Extra content between the text and the actions. |
| `className?` | `string` | none | Merged onto the frame. |

`ErrorState` also sets `role="alert"`.

### `LoadingState`

`LoadingStateProps extends ComponentProps<"div">`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `string` | `"Loading…"` / `"جارٍ التحميل…"` by provider locale | Announced to assistive tech; also shown next to the spinner when `rows` is `0`. |
| `rows?` | `number` | `3` | Number of skeleton items (rows, cards or events). With `0`, a centred spinner and label are shown instead. |
| `shape?` | `"rows" \| "grid" \| "timeline"` | `"rows"` | The layout being previewed: list rows, a grid of cards, or a timeline of events. |
| `columns?` | `1 \| 2 \| 3 \| 4` | `3` | Grid columns from the `sm` breakpoint up; one column below it. |
| `caption?` | `ReactNode` | none | Visible text under the skeleton ("Fetching the last 30 days…"). Replaces the `sr-only` label as the announced text. |
| `className?` | `string` | none | Merged onto the wrapper. |

### `Skeleton`

`({ className, ...props }: ComponentProps<"div">)`. A `bg-secondary` block with `rounded-[4px]` that pulses
only when motion is allowed (`motion-safe:animate-pulse`). Give it a size with `className`.

## Examples

### Grid and timeline

```tsx
<LoadingState shape="grid" columns={3} rows={6} />
<LoadingState shape="timeline" rows={4} caption="Fetching the audit trail…" />
```

### Error with retry

```tsx
import { Button, ErrorState } from "@fadymondy/nasaq/web";

export function LoadFailed({ retry }: { retry: () => void }) {
  return (
    <ErrorState
      title="تعذّر تحميل المهام"
      description="لم يستجب الخادم."
      actions={<Button onClick={retry}>حاول مرة أخرى</Button>}
    />
  );
}
```

### Loading a list

```tsx
import { LoadingState } from "@fadymondy/nasaq/web";

export function IssuesLoading() {
  return <LoadingState rows={4} label="جارٍ التحميل…" className="max-w-md" />;
}
```

### Custom skeleton

```tsx
import { Skeleton } from "@fadymondy/nasaq/web";

export function CardSkeleton() {
  return (
    <div className="flex flex-col gap-2">
      <Skeleton className="h-4 w-40" />
      <Skeleton className="h-3 w-64" />
    </div>
  );
}
```

### Choosing the state in a data region

```tsx
import { Button, EmptyState, ErrorState, LoadingState } from "@fadymondy/nasaq/web";

type Query = { isLoading: boolean; isError: boolean; data?: string[]; refetch: () => void };

export function Issues({ q }: { q: Query }) {
  if (q.isLoading) return <LoadingState rows={4} label="جارٍ التحميل…" />;
  if (q.isError) return <ErrorState title="تعذّر التحميل" actions={<Button onClick={q.refetch}>إعادة المحاولة</Button>} />;
  if (!q.data?.length) return <EmptyState title="لا توجد مهام" actions={<Button variant="primary">مهمة جديدة</Button>} />;
  return <ul>{q.data.map((i) => <li key={i}>{i}</li>)}</ul>;
}
```

## Accessibility

None of these are focusable; keyboard access is through the `actions` you pass.

| Key | Action |
| --- | --- |
| `Tab` / `Enter` / `Space` | Reach and activate the buttons in `actions`. |

- `ErrorState` has `role="alert"`, so it is announced when it is mounted. It shows an icon and words, not colour alone.
- `LoadingState` has `role="status"` and `aria-live="polite"` with an `sr-only` label. The skeleton rows are `aria-hidden`.
- Icons are `aria-hidden`. The title and description carry the meaning.
- Localise `title`, `description`, action labels and `LoadingState`'s `label` (its default follows the provider locale).
- `Skeleton` is `aria-hidden`; pair custom skeletons with a `role="status"` label.

## RTL & i18n

- Content is centred, so it needs no mirroring. The action row wraps and follows `dir`.
- Arrows inside action buttons follow the button's own rules.
- Numbers inside copy: use `Num` from the numeric component.
- Built-in string: `LoadingState` `label` defaults to `"Loading…"`, or `"جارٍ التحميل…"` when the provider locale starts with `ar`.
- The `ErrorState` icon tile (`data-slot="state-icon"`) is coloured directly with `text-nq-danger-text`.

## Styling & tokens

- Frame: `border-dashed border-border`, `rounded-card`; icon tile `bg-card`, `rounded-control`; `ErrorState` icon `text-nq-danger-text`.
- Skeleton: `bg-secondary`. Row height `h-row`.
- Target with `[data-slot=empty-state]`, `[data-slot=error-state]`, `[data-slot=loading-state]`, `[data-slot=skeleton]`.
- Extend with `className`. Do not colour with raw hex.

## Do / Don't

- **Do** say what is missing and give the action that fixes it.
- **Do** prefer skeletons over spinners for regions; they preview the layout.
- **Do** give errors an icon and words.
- **Don't** add empty-state art or "welcome" filler on populated screens.
- **Don't** show more than one primary action.
- **Don't** rely on the default `"Loading…"` label in Arabic.

## Related

- [Spinner](https://docs.nasaqui.com/components/spinner) · [Table](https://docs.nasaqui.com/components/table) · [Card](https://docs.nasaqui.com/components/card) · [Status](https://docs.nasaqui.com/components/status)

## Lab

https://docs.nasaqui.com/?path=/docs/components-loading-states-states--docs

## Code

### React

```tsx
import { Button, EmptyState } from "@fadymondy/nasaq/web";

export function NoIssues() {
  return (
    <EmptyState
      title="لا توجد مهام بعد"
      description="تظهر هنا المهام التي تنشئها أو تُسند إليك."
      actions={<Button variant="primary">مهمة جديدة</Button>}
    />
  );
}
```

### shadcn

```tsx
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/states";

export function NoIssues() {
  return (
    <EmptyState
      title="لا توجد مهام بعد"
      description="تظهر هنا المهام التي تنشئها أو تُسند إليك."
      actions={<Button variant="primary">مهمة جديدة</Button>}
    />
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqButton, NqEmptyState, NqLoadingState } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqEmptyState title="لا توجد مهام بعد" description="تظهر هنا المهام التي تنشئها أو تُسند إليك.">
    <template #actions><NqButton variant="primary">مهمة جديدة</NqButton></template>
  </NqEmptyState>
  <NqLoadingState shape="grid" :rows="4" :columns="2" caption="Fetching the last 30 days…" />
  <NqLoadingState shape="timeline" :rows="3" />
</template>
```

### Blade

```blade
<x-nq::states title="لا توجد مهام بعد" description="تظهر هنا المهام التي تنشئها أو تُسند إليك.">
    <x-slot:actions>
        <x-nq::button variant="primary">مهمة جديدة</x-nq::button>
    </x-slot:actions>
</x-nq::states>

<x-nq::states.loading shape="grid" :rows="4" :columns="2" caption="Fetching the last 30 days…" />

<x-nq::states.loading shape="timeline" :rows="3" />
```

### HTML + Alpine

```html
<div data-slot="empty-state" class="flex flex-col items-center justify-center gap-3 border border-dashed border-border px-6 py-12 text-center rounded-card">
            <span data-slot="state-icon" class="inline-flex size-10 items-center justify-center rounded-control border border-border bg-card [&amp;_svg]:size-5 text-muted-foreground">
            <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/>
  <path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
</svg>        </span>
        <div class="flex max-w-sm flex-col gap-1">
        <p class="text-label text-foreground">لا توجد مهام بعد</p>
                    <p class="text-body-sm text-muted-foreground">تظهر هنا المهام التي تنشئها أو تُسند إليك.</p>
            </div>
            <div class="mt-1 flex flex-wrap items-center justify-center gap-2"><button data-slot="button"
     type="button"                         class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        مهمة جديدة</button></div>
    </div>
<div data-slot="loading-state" data-shape="grid" role="status" aria-live="polite" class="flex flex-col gap-2">
                <div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
                            <div data-slot="loading-card" class="flex flex-col gap-3 rounded-card border border-border p-4">
                    <div class="flex items-center gap-3">
                        <div data-slot="skeleton" aria-hidden="true" class="bg-secondary motion-safe:animate-pulse size-8 rounded-control"></div>
                        <div data-slot="skeleton" aria-hidden="true" style="inline-size: 62%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                    </div>
                    <div data-slot="skeleton" aria-hidden="true" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3 w-full"></div>
                    <div data-slot="skeleton" aria-hidden="true" style="inline-size: 74%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                </div>
                            <div data-slot="loading-card" class="flex flex-col gap-3 rounded-card border border-border p-4">
                    <div class="flex items-center gap-3">
                        <div data-slot="skeleton" aria-hidden="true" class="bg-secondary motion-safe:animate-pulse size-8 rounded-control"></div>
                        <div data-slot="skeleton" aria-hidden="true" style="inline-size: 44%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                    </div>
                    <div data-slot="skeleton" aria-hidden="true" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3 w-full"></div>
                    <div data-slot="skeleton" aria-hidden="true" style="inline-size: 58%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                </div>
                            <div data-slot="loading-card" class="flex flex-col gap-3 rounded-card border border-border p-4">
                    <div class="flex items-center gap-3">
                        <div data-slot="skeleton" aria-hidden="true" class="bg-secondary motion-safe:animate-pulse size-8 rounded-control"></div>
                        <div data-slot="skeleton" aria-hidden="true" style="inline-size: 54%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                    </div>
                    <div data-slot="skeleton" aria-hidden="true" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3 w-full"></div>
                    <div data-slot="skeleton" aria-hidden="true" style="inline-size: 82%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                </div>
                            <div data-slot="loading-card" class="flex flex-col gap-3 rounded-card border border-border p-4">
                    <div class="flex items-center gap-3">
                        <div data-slot="skeleton" aria-hidden="true" class="bg-secondary motion-safe:animate-pulse size-8 rounded-control"></div>
                        <div data-slot="skeleton" aria-hidden="true" style="inline-size: 38%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                    </div>
                    <div data-slot="skeleton" aria-hidden="true" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3 w-full"></div>
                    <div data-slot="skeleton" aria-hidden="true" style="inline-size: 64%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                </div>
                    </div>
                <p class="flex items-center gap-2 pt-1 text-caption text-muted-foreground">Fetching the last 30 days…</p>
    </div>
<div data-slot="loading-state" data-shape="timeline" role="status" aria-live="polite" class="flex flex-col gap-2">
    <span class="sr-only">Loading…</span>            <ol class="flex flex-col">
                            <li data-slot="loading-event" class="relative flex gap-3 pb-5 last:pb-0">
                    <span aria-hidden="true" class="absolute start-[9px] top-6 bottom-1 w-px bg-border"></span>                    <div data-slot="skeleton" aria-hidden="true" class="bg-secondary motion-safe:animate-pulse mt-0.5 size-5 shrink-0 rounded-full"></div>
                    <div class="flex min-w-0 flex-1 flex-col gap-2 pt-1">
                        <div data-slot="skeleton" aria-hidden="true" style="inline-size: 62%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                        <div data-slot="skeleton" aria-hidden="true" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-2.5 w-24"></div>
                    </div>
                </li>
                            <li data-slot="loading-event" class="relative flex gap-3 pb-5 last:pb-0">
                    <span aria-hidden="true" class="absolute start-[9px] top-6 bottom-1 w-px bg-border"></span>                    <div data-slot="skeleton" aria-hidden="true" class="bg-secondary motion-safe:animate-pulse mt-0.5 size-5 shrink-0 rounded-full"></div>
                    <div class="flex min-w-0 flex-1 flex-col gap-2 pt-1">
                        <div data-slot="skeleton" aria-hidden="true" style="inline-size: 44%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                        <div data-slot="skeleton" aria-hidden="true" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-2.5 w-24"></div>
                    </div>
                </li>
                            <li data-slot="loading-event" class="relative flex gap-3 pb-5 last:pb-0">
                                        <div data-slot="skeleton" aria-hidden="true" class="bg-secondary motion-safe:animate-pulse mt-0.5 size-5 shrink-0 rounded-full"></div>
                    <div class="flex min-w-0 flex-1 flex-col gap-2 pt-1">
                        <div data-slot="skeleton" aria-hidden="true" style="inline-size: 54%" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3"></div>
                        <div data-slot="skeleton" aria-hidden="true" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-2.5 w-24"></div>
                    </div>
                </li>
                    </ol>
        </div>
```
