---
title: Status Matrix
description: 2D grid of typed status cells (tristate / toggle / status / heat) with sticky headers
visibility: guest
draft: false
---

# Status Matrix

One grid engine for every rows × columns status surface: a **role-permission
matrix** (role × capability → allow / deny / inherit), a **heat grid** (cohort ×
period → value), a **controls matrix** (control × framework → met / at-risk /
failing), or a **preferences grid** (type × channel → on / off). Pick the
`cell-type` and the matrix renders the right interactive (or read-only) cell —
on sticky row and column headers, inside a keyboard-reachable scroll region.

Every encoding is conveyed in **text or shape**, never color alone: tristate
cells use distinct check / cross / dash glyphs, heat cells always print their
value, and status cells render a labeled badge.

## Role Permission Matrix

`cell-type="tristate"` with `editable` cycles each cell `inherit → allow → deny`
on click or Enter/Space. Edited cells get a "changed" ring and the legend
counts unsaved changes — ideal for an IAM grant editor.

:::preview{title="Editable role × capability grants"}
<x-wirekit::status-matrix
    cell-type="tristate"
    editable
    corner-label="Role"
    aria-label="Role permissions"
    :rows="[
        ['key' => 'owner', 'label' => 'Owner'],
        ['key' => 'admin', 'label' => 'Admin'],
        ['key' => 'member', 'label' => 'Member'],
        ['key' => 'guest', 'label' => 'Guest'],
    ]"
    :columns="[
        ['key' => 'billing', 'label' => 'Billing'],
        ['key' => 'members', 'label' => 'Members'],
        ['key' => 'projects', 'label' => 'Projects'],
        ['key' => 'settings', 'label' => 'Settings'],
    ]"
    :cells="[
        'owner:billing' => 'allow', 'owner:members' => 'allow', 'owner:projects' => 'allow', 'owner:settings' => 'allow',
        'admin:billing' => 'deny', 'admin:members' => 'allow', 'admin:projects' => 'allow', 'admin:settings' => 'allow',
        'member:projects' => 'allow', 'member:members' => 'inherit',
        'guest:projects' => 'deny',
    ]"
/>
:::

## Heat Grid

`cell-type="heat"` color-mixes each cell's background from the surface toward
the scale color by its normalized value — and always prints the value, so the
grid stays legible for colorblind readers.

:::preview{title="Retention cohort heat grid"}
<x-wirekit::status-matrix
    cell-type="heat"
    corner-label="Cohort"
    aria-label="Weekly retention"
    heat-unit="%"
    :heat-min="0"
    :heat-max="100"
    :rows="[
        ['key' => 'jan', 'label' => 'January'],
        ['key' => 'feb', 'label' => 'February'],
        ['key' => 'mar', 'label' => 'March'],
    ]"
    :columns="[
        ['key' => 'w0', 'label' => 'Week 0'],
        ['key' => 'w1', 'label' => 'Week 1'],
        ['key' => 'w2', 'label' => 'Week 2'],
        ['key' => 'w3', 'label' => 'Week 3'],
    ]"
    :cells="[
        'jan:w0' => 100, 'jan:w1' => 68, 'jan:w2' => 54, 'jan:w3' => 41,
        'feb:w0' => 100, 'feb:w1' => 72, 'feb:w2' => 60, 'feb:w3' => 48,
        'mar:w0' => 100, 'mar:w1' => 75, 'mar:w2' => 63, 'mar:w3' => 52,
    ]"
/>
:::

## Controls Matrix

`cell-type="status"` renders each value as an intent badge — common status
words (`met`, `at-risk`, `failing`) map to success / warning / danger
automatically. Perfect for a compliance control × framework grid.

:::preview{title="Compliance controls × frameworks"}
<x-wirekit::status-matrix
    cell-type="status"
    corner-label="Control"
    aria-label="Compliance controls"
    :rows="[
        ['key' => 'mfa', 'label' => 'MFA enforced'],
        ['key' => 'encryption', 'label' => 'Encryption at rest'],
        ['key' => 'backups', 'label' => 'Tested backups'],
    ]"
    :columns="[
        ['key' => 'soc2', 'label' => 'SOC 2'],
        ['key' => 'iso', 'label' => 'ISO 27001'],
        ['key' => 'hipaa', 'label' => 'HIPAA'],
    ]"
    :cells="[
        'mfa:soc2' => 'met', 'mfa:iso' => 'met', 'mfa:hipaa' => 'met',
        'encryption:soc2' => 'met', 'encryption:iso' => 'met', 'encryption:hipaa' => 'at-risk',
        'backups:soc2' => 'at-risk', 'backups:iso' => 'failing', 'backups:hipaa' => 'failing',
    ]"
/>
:::

## Preferences Grid

`cell-type="toggle"` with `editable` gives a type × channel on/off grid — e.g.
a notification-preferences matrix.

:::preview{title="Notification preferences (type × channel)"}
<x-wirekit::status-matrix
    cell-type="toggle"
    editable
    corner-label="Notify me about"
    aria-label="Notification preferences"
    name="prefs"
    :rows="[
        ['key' => 'mentions', 'label' => 'Mentions'],
        ['key' => 'comments', 'label' => 'Comments'],
        ['key' => 'digest', 'label' => 'Weekly digest'],
    ]"
    :columns="[
        ['key' => 'email', 'label' => 'Email'],
        ['key' => 'push', 'label' => 'Push'],
        ['key' => 'sms', 'label' => 'SMS'],
    ]"
    :cells="[
        'mentions:email' => true, 'mentions:push' => true,
        'comments:email' => true,
        'digest:email' => true,
    ]"
/>
:::

## Cell Types

| `cell-type` | Editable | Cell renders | Typical use |
| --- | --- | --- | --- |
| `tristate` | yes | inherit / allow / deny glyph | role-permission matrix |
| `toggle` | yes | on / off dot | preferences (type × channel) |
| `status` | no | intent badge | compliance / controls grid |
| `heat` | no | color-scaled value | retention / metric heatmap |

### Data Shapes

`rows` and `columns` are each `[{key, label}]`. `cells` is a value map keyed by
`"rowKey:colKey"` — or a nested `[rowKey => [colKey => value]]` map; both are
accepted.

```php
// 1. Flat key shape
$cells = ['admin:billing' => 'allow', 'admin:users' => 'deny'];

// 2. Nested shape (equivalent)
$cells = ['admin' => ['billing' => 'allow', 'users' => 'deny']];
```

### Status Value → Intent

`status` cells map these words automatically (case-insensitive); any other value
renders neutral:

| Intent | Words |
| --- | --- |
| success | met · pass · ok · active · compliant · done · on |
| warning | at-risk · warning · pending · partial · review |
| danger | failing · fail · error · inactive · breach · off |

## The Emitted Value (Editable)

When `editable`, every cell change emits the normalized map two ways — a
bubbling `cell-change` event and a JSON hidden input — so `wire:model` and forms
both bridge:

```blade
{{-- 1. Bind the JSON bridge to a Livewire property --}}
<x-wirekit::status-matrix
    cell-type="tristate"
    editable
    :rows="$roles"
    :columns="$capabilities"
    :cells="$grants"
    wire:model.live="grantsJson"
/>
```

```php
// 2. Decode + persist the edited grants
public string $grantsJson = '{}';

public function save(): void
{
    $grants = json_decode($this->grantsJson, true) ?: [];
    // 3. $grants is ['admin:billing' => 'deny', ...] — persist as you like
    foreach ($grants as $key => $value) {
        [$role, $capability] = explode(':', $key);
        Grant::updateOrCreate(compact('role', 'capability'), ['value' => $value]);
    }
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `rows` | array | `[]` | Row axis `[{key, label}]` |
| `columns` | array | `[]` | Column axis `[{key, label}]` |
| `cells` | array | `[]` | Value map (`"row:col" => value` or nested) |
| `cellType` | string | `'status'` | `tristate` · `toggle` · `status` · `heat` |
| `editable` | bool | `false` | Make `tristate` / `toggle` interactive |
| `cornerLabel` | string | `''` | Top-left header cell (the row-axis name) |
| `ariaLabel` | string | `'Status matrix'` | Accessible name for the grid + scroll region |
| `name` | string\|null | `null` | Hidden-input name for form submission |
| `heatFrom` | string | `var(--color-wk-warning)` | Heat ramp low end (cold) — a color/token reference |
| `heatTo` | string | `var(--color-wk-danger)` | Heat ramp high end (hot) — a color/token reference |
| `heatMin` | numeric | `0` | Heat scale lower bound |
| `heatMax` | numeric | `100` | Heat scale upper bound |
| `heatUnit` | string | `''` | Suffix on heat value labels (e.g. `%`) |
| `legend` | bool | `true` | Render the cell-type legend |
| `scope` | string\|null | `null` | Scoped personalization name |

## Accessibility

- The grid is a `role="grid"` with `scope="col"` / `scope="row"` headers; the
  scroll wrapper is a labeled `role="region"` with `tabindex="0"` and a focus
  ring, so it is reachable and operable by keyboard (WCAG 2.1.1).
- Every cell's state is in **text or shape**, never color alone: tristate cells
  carry the state word in their `aria-label` ("Admin, Billing: Denied") and use
  distinct glyphs; toggle cells expose `role="switch"` + `aria-checked`; heat
  cells always print their value; status cells render a labeled badge.
- Interactive cells use a single tab stop (roving focus) into the grid; arrow
  keys then move between cells.

## Keyboard Interaction

| Key | Action |
| --- | --- |
| `Tab` | Enter / leave the grid (one stop), then reach the scroll region |
| `Arrow keys` | Move focus between interactive cells |
| `Enter` / `Space` | Cycle a tristate cell or flip a toggle |

## Design Tokens

| Element | Token |
| --- | --- |
| Sticky header surface | `--color-wk-bg-elevated` |
| Grid border | `--color-wk-border` |
| Allow glyph | `--color-wk-success` |
| Deny glyph | `--color-wk-danger` |
| Inherit glyph | `--color-wk-text-subtle` |
| Changed-cell ring | `--color-wk-warning` |
| Heat ramp (cold → hot) | `--color-wk-warning` → `--color-wk-danger` (override via `heatFrom` / `heatTo`) |
| Focus ring | `--color-wk-ring` |

## Customization

Override the cell-type default and legend visibility via `config/wirekit.php`:

```php
'components' => [
    'status-matrix' => [
        'cell-type' => 'tristate',
        'legend' => true,
    ],
],
```

## Further Reading

- [WAI-ARIA Authoring Practices — Grid](https://www.w3.org/WAI/ARIA/apg/patterns/grid/)
- [WCAG 1.4.1 — Use of Color](https://www.w3.org/WAI/WCAG21/Understanding/use-of-color.html)
