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.
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.
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.
Preferences Grid
cell-type="toggle" with editable gives a type × channel on/off grid — e.g.
a notification-preferences matrix.
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 |
On paper the column and row labels wrap between words, so a matrix needs less of the sheet's width than of the screen's, and its cells keep their size. A sheet does not scroll, so a matrix still wider than the sheet prints the columns that fit. A landscape sheet holds more of them: give the page that prints the matrix @page { size: landscape; }.
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.
// 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:
{{-- 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"
/>
// 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 |
|---|---|---|---|
emptyText |
string |
Nothing here yet |
Sentence shown when rows or columns is empty. The empty slot replaces it entirely. |
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|null | null |
Accessible name for the matrix table, and the switch that makes the scroll wrapper a screen-reader landmark. The table itself is always named — it falls back to 'Status matrix'; the wrapper's role="region" waits for your name. See Accessibility |
name |
string|null | null |
Hidden-input name for form submission. Without an id the matrix's id derives from it; a second matrix with the same name on the page gets -2 |
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 matrix is a
<table>withscope="col"/scope="row"headers, and it is always named —ariaLabelwhen you set one, "Status matrix" otherwise. A table with no accessible name is worse than a duplicated region, so this fallback stays. role="grid"is added only forcell-type="tristate"andcell-type="toggle". The grid role is a promise of a keyboard model — one tab stop into the widget, arrows from there — and only those two cell types render something focusable to hold it. Astatusorheatmatrix is a data table with nothing to operate, so it stays a plain table and your reader keeps their own table-navigation mode, which announces the row and column header as they move. A grid role there would replace that with an application-widget model and hand them no way in.- The scroll wrapper carries
tabindex="0"and a focus ring in every configuration, so it is reachable and operable by keyboard (WCAG 2.1.1). - The wrapper becomes a screen-reader landmark only when you set
ariaLabel. It then also getsrole="region"under that name. Without one it stays a plain focusable scroller —role="region"plus a name is a landmark, and three matrices on a page all falling back to "Status matrix" would be three landmarks announcing the same thing, which axe reports aslandmark-unique. - 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 exposerole="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.
- An editable
tristatematrix with its legend says the number of unsaved changes through arole="status"live region each time it changes, from the first change on. The region is in the page before that change, because one that appears together with its text is announced unreliably; the count in the legend is the same text for the eye. - An
aria-labelledbyof yours names the matrix and its scroll region, asaria-labeldoes.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Enter / leave the grid (one stop), then reach the scroll region |
Arrow keys |
Move focus between interactive cells |
Home / End |
First / last cell of the current row |
Ctrl+Home / Ctrl+End |
First / last cell of the whole grid |
Enter / Space |
Cycle a tristate cell or flip a toggle |
This table describes cell-type="tristate" and cell-type="toggle". A status or heat matrix
has no focusable cells and no grid role: Tab reaches its scroll region and stops there, and the
cells are read with your screen reader's own table navigation.
Navigation works whether or not the matrix is editable — only Enter / Space are gated on it.
A read-only tristate or toggle matrix marks its cells aria-disabled="true", so they stay
focusable and readable while announcing that there is nothing to activate.
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 |
| Toggle on | --color-wk-success |
| Toggle off (a ring, at 3:1 against the page) | --color-wk-border-strong |
| 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/:
'components' => [
'status-matrix' => [
'cell-type' => 'tristate',
'legend' => true,
],
],