Skip to content
WANCRAFT

F.205Forms & Inputs

Permission Matrix

A roles × permissions grid with tri-state select-all headers, collapsible groups, locked cells and tracked changes.

NewReactTailwindadvanced

Overview

Permission Matrix edits who can do what in a real <table>: roles across the top, permissions down the side, grouped into collapsible sections. Every column header, row header and group row carries a tri-state checkbox that grants or revokes the whole set (mixed when only some are on). Inherited or policy-locked cells stay focusable but cannot change, and explain why in a tooltip and an accessible description. Cells changed since load are tinted and dotted, the toolbar counts them ("4 changes") and Reset changes restores the baseline. The header row and permission column stay pinned while the grid scrolls in both directions, and the whole matrix is a single tab stop with APG grid navigation: arrows move, Space toggles, Home / End jump. The value is controlled as granted permission ids per role.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/permission-matrix.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/permission-matrix.json

Method 02 · Copy and paste

  1. 1. No extra dependencies — React and Tailwind CSS only.
  2. 2. Copy the source from the Code tab into components/ui/permission-matrix.tsx.
  3. 3. Import it and adjust props.
Dependencies
None
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { PermissionMatrix, type PermissionGroup, type PermissionRole, type PermissionValue } from "@/components/ui/permission-matrix";
import { useState } from "react";

export function PermissionMatrixExample() {
  const roles: PermissionRole[] = [
    { id: "admin", name: "Admin" },
    { id: "editor", name: "Editor" },
    { id: "viewer", name: "Viewer" },
  ];
  const groups: PermissionGroup[] = [
    {
      id: "content",
      label: "Content",
      permissions: [
        { id: "pages.view", label: "View pages" },
        { id: "pages.edit", label: "Edit pages" },
        { id: "pages.delete", label: "Delete pages" },
      ],
    },
  ];
  const [value, setValue] = useState<PermissionValue>({
    admin: ["pages.view", "pages.edit", "pages.delete"],
    editor: ["pages.view", "pages.edit"],
    viewer: ["pages.view"],
  });

  return (
    <div className="w-full max-w-3xl">
      <PermissionMatrix
        roles={roles}
        groups={groups}
        value={value}
        onChange={setValue}
        locked={[{ role: "viewer", permission: "pages.delete", reason: "Viewers are read-only." }]}
      />
    </div>
  );
}

03Props

PropTypeDefaultDescription
rolesPermissionRole[]—Required. Columns: { id, name, description? }.
groupsPermissionGroup[]—Required. Row groups: { id, label, permissions: { id, label, description? }[] }.
value / defaultValueRecord<string, string[]>—Granted permission ids per role id — controlled, or initial when uncontrolled.
onChange(value: PermissionValue) => void—Called with the next grants after a toggle, a select-all or a reset.
lockedPermissionLock[][]{ role, permission, reason, granted? } — cells that cannot change; the reason shows in a tooltip.
baselinePermissionValue—What changes are counted against. Defaults to the value at mount.
labelstring"Permissions"Title and accessible name of the grid.
collapsiblebooleantrueGroup rows have a disclosure button that hides their permissions.
defaultCollapsedstring[][]Group ids collapsed at mount.
groupTogglesbooleantrueTri-state checkboxes on group rows for each role.
highlightChangesbooleantrueTint changed cells and show the change count and Reset changes.
density"comfortable" | "compact""comfortable"Row height; compact hides permission descriptions.
maxHeightnumber420Scroll area max height in px (0 = none). Header row and first column are sticky.
accentstring"#FF7448"Checked fill, change markers and focus rings.

04Features

  • 01Real <table> with sticky header row and sticky permission column while scrolling both ways
  • 02Tri-state select-all checkboxes on every role column, permission row and (optionally) group row
  • 03Collapsible permission groups with counts
  • 04Locked / inherited cells stay readable and focusable, with the reason in a tooltip
  • 05Cells changed since load are tinted and dotted; "4 changes" summary with Reset changes
  • 06APG grid keyboard model with one tab stop; select-alls skip locked cells
input · clickinput · keyboardinput · focusinput · hover

05Accessibility

  • The table has role="grid" and is labelled by its title; it is a single tab stop with a roving tabindex. Arrow keys move between checkboxes, Home / End jump to the ends of a row and Ctrl + Home / End to the first and last cell.
  • Every box is a button with role="checkbox" and aria-checked ("mixed" for partly granted sets); Space toggles. Names say what they cover, e.g. "Publish for Editor", "All permissions for Admin", "Publish for all roles".
  • Locked cells use aria-disabled (so they stay focusable) and aria-describedby pointing at "Locked: <reason>"; the same reason appears in a tooltip on hover or focus, and Escape hides it without leaving the grid.
  • Changed cells add ", changed" to their name; grants, revokes and resets are announced in a polite live region.
  • Group rows use a disclosure button with aria-expanded; headers use scope="col", scope="row" and scope="rowgroup".

06Limitations

  • All rows render (no virtualisation); for hundreds of permissions add search or paging around it.
  • Locks are static per cell — inheritance rules are resolved by you before passing them in.