State

Empty

No projects yet

Create your first project to get started.

Loading

Loading projects…

Error (bordered)

Couldn't load projects

The server returned an error. Check your connection and try again.

Success (compact)

All done

Your changes have been saved.


  
/*
 * State — block
 *
 * A self-contained block for empty / loading / error / success states.
 * Used inside cards, sections, or panels when content is unavailable
 * or operation feedback should fill the surface. CSS-only.
 *
 *   <section class="state" data-state="empty">
 *     <div class="state__icon">📭</div>
 *     <h3 class="state__title">No projects yet</h3>
 *     <p class="state__description">Create your first project to get started.</p>
 *     <div class="state__action">
 *       <button class="button -primary">New project</button>
 *     </div>
 *   </section>
 */

@layer malevich.components {
  .state {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--space-gap-elements-m);

    padding-block: var(--space-inset-block-l);
    padding-inline: var(--space-inset-block-l);

    border-radius: var(--radius-card);
    background-color: var(--color-surface-canvas);
    color: var(--color-ink-strong);
  }

  .state__icon {
    inline-size: var(--size-control-xl);
    block-size: var(--size-control-xl);
    display: flex;
    align-items: center;
    justify-content: center;

    color: var(--color-ink-subtle);

    font-family: var(--font-display-statement-family);
    font-size: var(--font-display-statement-size);
    font-weight: var(--font-display-statement-weight);
  }

  .state__title {
    margin: 0;

    font-family: var(--font-heading-title-family);
    font-size: var(--font-heading-title-size);
    font-weight: var(--font-heading-title-weight);
    line-height: var(--font-heading-title-line-height);
    letter-spacing: var(--font-heading-title-letter-spacing);

    color: var(--color-ink-strong);
  }

  .state__description {
    margin: 0;
    max-inline-size: 48ch;

    font-family: var(--font-body-regular-family);
    font-size: var(--font-body-regular-size);
    font-weight: var(--font-body-regular-weight);
    line-height: var(--font-body-regular-line-height);
    letter-spacing: var(--font-body-regular-letter-spacing);

    color: var(--color-ink-regular);
  }

  .state__action {
    display: inline-flex;
    align-items: center;
    gap: var(--space-gap-elements-s);
    margin-block-start: var(--space-gap-elements-s);
  }

  /* Data-state hooks for icon color affordances. The icon's color
   * follows the data-state so authors can render a single icon and
   * have it tint correctly.
   */

  .state[data-state="empty"] .state__icon {
    color: var(--color-ink-subtle);
  }

  .state[data-state="loading"] .state__icon {
    color: var(--color-accent);
  }

  .state[data-state="error"] .state__icon {
    color: var(--color-danger);
  }

  .state[data-state="error"] .state__title {
    color: var(--color-danger);
  }

  .state[data-state="success"] .state__icon {
    color: var(--color-success);
  }

  .state[data-state="success"] .state__title {
    color: var(--color-success);
  }

  /* Variants */

  .state.-bordered {
    border: var(--border-width-hairline) solid var(--color-border-muted);
    background-color: var(--color-surface-raised);
  }

  .state.-compact {
    padding-block: var(--space-inset-block-m);
    padding-inline: var(--space-inset-block-m);
    gap: var(--space-gap-elements-s);
  }

  .state.-compact .state__title {
    font-family: var(--font-heading-section-family);
    font-size: var(--font-heading-section-size);
    font-weight: var(--font-heading-section-weight);
    line-height: var(--font-heading-section-line-height);
    letter-spacing: var(--font-heading-section-letter-spacing);
  }
}
This component is pure CSS — no JavaScript required.
# State — AGENTS.md

> Auto-generated from `state.docs.md` + the modifier applicability matrix.
> Edit those source files; this file regenerates on build.

## Identity

- **Component:** `state`
- **Layer:** blocks
- **Status:** stable
- **Last updated:** 2026-05-19

## Summary

- **Empty:** a list, table, or section has no items to display.

## Variants

| Variant   | Class                | Use for                                |
|-----------|----------------------|----------------------------------------|
| Default   | `.state`             | Canvas-tone background, generous pad   |
| Bordered  | `.state.-bordered`   | Raised background + hairline border    |
| Compact   | `.state.-compact`    | Smaller padding + title size           |

`data-state` attribute drives icon and title colors:

| Value      | Effect                                       |
|------------|----------------------------------------------|
| `empty`    | Icon: ink-subtle. Title: ink-strong.         |
| `loading`  | Icon: accent. Title: ink-strong.             |
| `error`    | Icon: danger. Title: danger.                 |
| `success`  | Icon: success. Title: success.               |

## Modifier applicability

Per ADR-0007. Modifiers attach via `data-{category}="value"` attributes.

| Category | Accepts |
|---|---|
| `background` | `solid` |
| `surface` | `flat`, `elevated` |
| `effect` | — |
| `shader` | — |
| `rhythm` | `compact`, `regular` |
| `motion` | any value |
| `behavior` | — |

## Anatomy

```html
<section class="state" data-state="empty">
  <div class="state__icon">📭</div>
  <h3 class="state__title">No projects yet</h3>
  <p class="state__description">Create your first project to get started.</p>
  <div class="state__action">
    <button class="button -primary">New project</button>
  </div>
</section>

<section class="state" data-state="loading">
  <div class="state__icon">
    <span class="spinner -l" role="status" aria-label="Loading"></span>
  </div>
  <h3 class="state__title">Loading projects…</h3>
</section>

<section class="state -bordered" data-state="error">
  <div class="state__icon">⚠</div>
  <h3 class="state__title">Couldn't load</h3>
  <p class="state__description">
    The server returned an error. Check your connection and try again.
  </p>
  <div class="state__action">
    <button class="button -primary">Retry</button>
    <button class="button -ghost">Cancel</button>
  </div>
</section>

<section class="state -compact" data-state="success">
  <div class="state__icon">✓</div>
  <h3 class="state__title">All done</h3>
  <p class="state__description">Your changes have been saved.</p>
</section>
```

Every part is optional except the wrapper. Authors compose only what
the state needs.

## Tokens consumed

### From semantic tier
- `--color-surface-canvas` — default background
- `--color-surface-raised` — bordered variant
- `--color-ink-strong`, `--color-ink-regular`, `--color-ink-subtle`
- `--color-accent` — loading state icon
- `--color-danger` — error state icon + title
- `--color-success` — success state icon + title
- `--color-border-muted` — bordered variant
- `--space-inset-block-l` / `-m` — padding
- `--space-gap-elements-m` / `-s`
- `--size-control-xl` — icon area
- `--radius-card`
- `--border-width-hairline`
- `--font-heading-title-*`, `--font-heading-section-*`, `--font-body-regular-*`,
  `--font-display-statement-*`

## Accessibility contract

- Wrap in `<section>` so screen readers can navigate to it as a
  landmark.
- The title (`.state__title`) is a real heading element — use
  `<h2>`–`<h4>` matching the document outline.
- For `data-state="loading"`, the inner Spinner or status indicator
  carries `role="status"` and `aria-label`.
- For `data-state="error"`, consider an `aria-live="polite"` parent
  region so error appearance is announced.
- The icon glyph (emoji or SVG) is presentational. If using an emoji,
  add `aria-hidden="true"` to the `.state__icon`; if using an SVG,
  add `aria-hidden="true"` directly on the SVG element.

## Guidance

### Do

- Use `data-state` so the visual treatment matches the message.
- Keep titles short — 3-6 words. The description carries detail.
- Always provide an action for error states.

### Don't

- Don't render multiple `<h3>`s inside one State block.
- Don't use State for content that loads in pieces. Use Skeleton for
  layout-aware placeholders.
- Don't fake an error state with an empty list. Distinguish "no data
  yet" (empty) from "fetch failed" (error).