Permissions matrix
ProRoles across, permissions down, a level in every cell: no access, read, write or admin. Granting a permission adds what it requires, removing one takes away what depends on it, and roles and groups change in bulk.
npx prodular-ui add permissions-matrixPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
Edit roles
Owner is locked. Give Approver write on Refunds and it also gets write on Invoices; remove Bills from Accountant and Payouts goes with it. Changed cells are marked until you save.
| Permission | Owner1 member | Admin2 members | Accountant4 members | Approver3 members | AuditorCustom1 member |
|---|---|---|---|---|---|
| Invoices and customers | |||||
| Invoices | Admin | ||||
| Customers | Admin | ||||
| Refunds and credit notes | Write | ||||
| Payments | |||||
| Bills | Admin | ||||
| Payouts | Write | ||||
| Bank accounts | Admin | ||||
| Workspace | |||||
| Members and roles | Admin | ||||
| AI and agents | Admin | ||||
| Audit log | Read |
"use client"
import * as React from "react"
import { PermissionsMatrix, type PermissionGroup, type PermissionRole, type PermissionValue,} from "@/components/ui/permissions-matrix"
const roles: PermissionRole[] = [ { id: "owner", label: "Owner", members: 1, locked: true }, { id: "admin", label: "Admin", members: 2 }, { id: "accountant", label: "Accountant", members: 4 }, { id: "approver", label: "Approver", members: 3 }, { id: "auditor", label: "Auditor", members: 1, custom: true },]
const groups: PermissionGroup[] = [ { id: "invoicing", label: "Invoices and customers", permissions: [ { id: "invoices", label: "Invoices", description: "Draft, send and edit invoices." }, { id: "customers", label: "Customers", description: "Customer records, contacts and payment terms." }, { id: "refunds", label: "Refunds and credit notes", levels: ["none", "write"], requires: [{ permission: "invoices", level: "write" }] }, ], }, { id: "payments", label: "Payments", permissions: [ { id: "bills", label: "Bills", description: "Vendor bills and their approval." }, { id: "payouts", label: "Payouts", levels: ["none", "read", "write"], requires: [{ permission: "bills", level: "read" }] }, { id: "bank", label: "Bank accounts", levels: ["none", "read", "admin"] }, ], }, { id: "workspace", label: "Workspace", permissions: [ { id: "members", label: "Members and roles", levels: ["none", "read", "admin"] }, { id: "ai", label: "AI and agents", description: "Models, agent permissions and spend limits." }, { id: "audit", label: "Audit log", levels: ["none", "read"] }, ], },]
const saved: PermissionValue = { admin: { invoices: "admin", customers: "admin", refunds: "write", bills: "admin", payouts: "write", bank: "admin", members: "admin", ai: "admin", audit: "read" }, accountant: { invoices: "write", customers: "write", refunds: "write", bills: "read", payouts: "write", bank: "read", members: "read", ai: "read", audit: "none" }, approver: { invoices: "read", customers: "read", refunds: "none", bills: "write", payouts: "read", bank: "none", members: "read", ai: "none", audit: "none" }, auditor: { invoices: "read", customers: "read", refunds: "none", bills: "read", payouts: "read", bank: "read", members: "read", ai: "read", audit: "read" },}
export default function PermissionsMatrixRoles() { const [value, setValue] = React.useState(saved) const [base, setBase] = React.useState(saved) return ( <PermissionsMatrix groups={groups} roles={roles} value={value} onValueChange={setValue} initialValue={base} onSave={async (v) => { await new Promise((r) => setTimeout(r, 600)) setBase(v) }} /> )}Read only
readOnly shows the same matrix without controls, for people who can see roles but not change them.
| Permission | Member | Guest |
|---|---|---|
| Projects | ||
| View projects | Read | Read |
| Edit tasks | Write | – |
| Share outside the workspace | – | – |
"use client"
import { PermissionsMatrix, type PermissionGroup, type PermissionRole } from "@/components/ui/permissions-matrix"
const roles: PermissionRole[] = [ { id: "member", label: "Member" }, { id: "guest", label: "Guest" },]
const groups: PermissionGroup[] = [ { id: "projects", label: "Projects", permissions: [ { id: "view", label: "View projects", levels: ["none", "read"] }, { id: "edit", label: "Edit tasks", levels: ["none", "write"], requires: [{ permission: "view", level: "read" }] }, { id: "share", label: "Share outside the workspace", levels: ["none", "write"] }, ], },]
export default function PermissionsMatrixReadOnly() { return ( <PermissionsMatrix groups={groups} roles={roles} readOnly value={{ member: { view: "read", edit: "write", share: "none" }, guest: { view: "read", edit: "none", share: "none" } }} className="max-w-xl" /> )}Anatomy
<PermissionsMatrix groups={groups} roles={roles} value={value} onValueChange={setValue} initialValue={saved} onSave={save}/>Guidance
Use it when
- Role based access for a workspace or an organization: the settings page where admins define what each role can do.
- levels per permission when not every level makes sense, like an audit log that is only none or read.
- requires for real dependencies, so a role can never hold a permission that would not work without another.
Avoid
- Assigning roles to people. Use the members list with a role select.
- More than about eight roles. Add a role filter or split the matrix by product area.
- Per-record sharing. Use a share dialog on the record.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Describe permissions as groups of { id, label, description?, levels?, requires? } and roles as { id, label, members?, locked?, custom? }.
- Keep value as { [roleId]: { [permissionId]: level } } and pass onValueChange. Missing entries mean none.
- Pass the saved value as initialValue and an async onSave to get change markers and the Save bar; throw from onSave to show an error.
- Mark built-in owner roles locked: they show every permission at its highest level and cannot be edited.
- Use setPermission(value, groups, role, permission, level) on the server too, so dependency rules hold however permissions change.
- Cells are a grid: arrow keys move, Enter opens the level menu. Every cell's accessible name includes the permission, the role and the level.
API
PermissionsMatrix
| Prop | Type | Default | Description |
|---|---|---|---|
| groups | PermissionGroup[] | Groups of permissions with levels and requirements. | |
| roles | PermissionRole[] | Columns, in order. | |
| value / onValueChange | PermissionValue / (value) => void | Levels by role and permission. | |
| readOnly | boolean | true without onValueChange | Hides the controls. |
| initialValue / onSave | PermissionValue / (value) => Promise<void> | Change markers, Discard and Save. |
setPermission
| Prop | Type | Default | Description |
|---|---|---|---|
| (value, groups, role, permission, level) | { value, changes } | Applies one change with its dependencies. |
diffPermissions
| Prop | Type | Default | Description |
|---|---|---|---|
| (before, after, roles, groups) | { role, permission, from, to }[] | Every changed cell, for review or an audit entry. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add permissions-matrix# or, with the registry in components.jsonnpx shadcn@latest add @prodular/permissions-matrix