Permission Matrix
Role and permission assignment with server-owned locked and inherited states.
Preview
@props([
'action' => '#',
'method' => 'PATCH',
'title' => 'Role permissions',
'description' => 'Choose the permissions that each role receives.',
'roles' => null,
'permissions' => null,
'values' => null,
'locked' => null,
'inherited' => null,
'statusMessage' => null,
'submitLabel' => 'Save permissions',
])
@php
$roles ??= [
['key' => 'viewer', 'name' => 'Viewer', 'description' => 'Read-only access'],
['key' => 'editor', 'name' => 'Editor', 'description' => 'Creates and updates content'],
['key' => 'admin', 'name' => 'Admin', 'description' => 'Manages the workspace'],
];
$permissions ??= [
['group' => 'Projects', 'key' => 'projects.view', 'name' => 'View projects', 'description' => 'Read project details and activity.'],
['group' => 'Projects', 'key' => 'projects.update', 'name' => 'Update projects', 'description' => 'Change project content and status.'],
['group' => 'People', 'key' => 'members.invite', 'name' => 'Invite members', 'description' => 'Send workspace invitations.'],
['group' => 'Administration', 'key' => 'billing.manage', 'name' => 'Manage billing', 'description' => 'Change plans and payment details.'],
];
$values ??= [
'viewer' => ['projects.view'],
'editor' => ['projects.view', 'projects.update'],
'admin' => ['projects.view', 'projects.update', 'members.invite', 'billing.manage'],
];
$locked ??= ['admin' => ['billing.manage']];
$inherited ??= ['editor' => ['projects.view'], 'admin' => ['projects.view']];
@endphp
<x-ui.form.guard
as="section"
data-slot="permission-matrix"
data-surface="admin"
{{ $attributes->merge(['class' => 'bg-background text-foreground']) }}
>
<x-ui.form
:action="$action"
:method="$method"
:summary="__('Please correct the permission errors below.')"
data-persisted-form
x-data="{
toggleRole(role, checked) {
this.$root.querySelectorAll('[data-permission-control]').forEach((control) => {
if (control.dataset.role === role && ! control.disabled) control.checked = checked
})
this.$dispatch('brok:form-dirty')
}
}"
>
<x-ui.card>
<x-ui.card.header>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<x-ui.card.title>{{ __($title) }}</x-ui.card.title>
<x-ui.card.description class="mt-2">{{ __($description) }}</x-ui.card.description>
</div>
<x-ui.badge variant="outline">{{ __('Server-authoritative') }}</x-ui.badge>
</div>
@if ($statusMessage)
<x-ui.alert variant="success" role="status" class="mt-4">{{ __($statusMessage) }}</x-ui.alert>
@endif
</x-ui.card.header>
<x-ui.card.content class="!p-0">
<div class="overflow-x-auto">
<table class="w-full min-w-[48rem] border-collapse text-sm">
<caption class="sr-only">{{ __('Permission assignments by role') }}</caption>
<thead>
<tr class="border-y border-border bg-muted/50">
<th scope="col" class="px-6 py-4 text-start font-medium text-muted-foreground">{{ __('Permission') }}</th>
@foreach ($roles as $role)
@php
$roleKey = (string) $role['key'];
@endphp
<th scope="col" class="min-w-40 px-4 py-4 text-start align-top">
<div class="flex items-start gap-2">
<x-ui.checkbox
:aria-label="__('Select all available permissions for :role', ['role' => __($role['name'])])"
:data-role="$roleKey"
@change="toggleRole($event.target.dataset.role, $event.target.checked)"
/>
<span>
<span class="block font-medium text-foreground">{{ __($role['name']) }}</span>
<span class="mt-0.5 block text-xs font-normal text-muted-foreground">{{ __($role['description']) }}</span>
</span>
</div>
</th>
@endforeach
</tr>
</thead>
<tbody>
@foreach ($permissions as $index => $permission)
@if ($index === 0 || $permissions[$index - 1]['group'] !== $permission['group'])
<tr>
<th colspan="{{ count($roles) + 1 }}" scope="colgroup" class="border-b border-border bg-muted/30 px-6 py-2 text-start text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{{ __($permission['group']) }}
</th>
</tr>
@endif
<tr class="border-b border-border last:border-b-0">
<th scope="row" class="px-6 py-4 text-start font-normal">
<span class="block font-medium text-foreground">{{ __($permission['name']) }}</span>
<span class="mt-2 block text-xs text-muted-foreground">{{ __($permission['description']) }}</span>
</th>
@foreach ($roles as $role)
@php
$roleKey = (string) $role['key'];
$permissionKey = (string) $permission['key'];
$selected = in_array($permissionKey, old("permissions.$roleKey", $values[$roleKey] ?? []), true);
$isLocked = in_array($permissionKey, $locked[$roleKey] ?? [], true);
$isInherited = in_array($permissionKey, $inherited[$roleKey] ?? [], true);
$isDisabled = $isLocked || $isInherited;
@endphp
<td class="px-4 py-4 align-middle">
<div class="flex items-center gap-2">
<x-ui.checkbox
name="permissions[{{ $roleKey }}][]"
:value="$permissionKey"
:checked="$selected"
:disabled="$isDisabled"
data-permission-control
:data-role="$roleKey"
:aria-label="__(':permission for :role', ['permission' => __($permission['name']), 'role' => __($role['name'])])"
/>
@if ($isLocked)
<x-ui.badge variant="outline" size="sm">{{ __('Locked') }}</x-ui.badge>
@elseif ($isInherited)
<x-ui.badge variant="secondary" size="sm">{{ __('Inherited') }}</x-ui.badge>
@endif
</div>
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
</x-ui.card.content>
<x-ui.card.footer class="flex flex-col gap-4 border-t border-border sm:flex-row sm:items-center sm:justify-between">
<p class="text-xs text-muted-foreground">{{ __('Locked and inherited values must be preserved by the server.') }}</p>
<x-ui.button type="submit" x-bind:disabled="submitting" x-bind:aria-busy="submitting">
<span x-show="! submitting">{{ __($submitLabel) }}</span>
<span x-cloak x-show="submitting">{{ __('Saving…') }}</span>
</x-ui.button>
</x-ui.card.footer>
</x-ui.card>
</x-ui.form>
</x-ui.form.guard>
Installation
php artisan ui:add blocks/permission-matrix-roles
Registry contract
Install confidence
php artisan ui:add blocks/permission-matrix-roles
writes only the generated targets below. The CLI validates each file hash before writing and
prompts before replacing local changes unless --force is used.
- Version
- 1.1.4
- License
- open
- Stability
- stable
- Contract
- v2
- Foundation
- ≥ 1.0.0
| Type | Generated target |
|---|---|
| blade | resources/views/blocks/permission-matrix-roles.blade.php |
Registry dependencies
Package dependencies
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: what the block is, the install command, how to render it, its props and the rules. Copy it, or open a prompt about this block in an assistant.
# Brok UI block: Permission Matrix (`permission-matrix-roles`)
A server-ready role and permission matrix with grouped permissions, bulk role selection, locked and inherited states, validation, progress, and unsaved-change protection.
Brok UI is a Laravel Blade registry. `ui:add` copies this block into the app as plain Blade the app owns; it composes installed `<brok:*>` primitives and semantic design tokens.
## Install
```bash
php artisan ui:add blocks/permission-matrix-roles
```
## Render it
```blade
<x-blocks.permission-matrix-roles />
```
## Props
- `action` (string, default `#`) — Permission update endpoint.
- `method` (string, default `PATCH`) — Update request method.
- `title` (string, default `Role permissions`) — Matrix heading.
- `description` (string, default `Choose the permissions that each role receives.`) — Matrix explanation.
- `roles` (array|null, default `null`) — Stable role keys, names, and descriptions.
- `permissions` (array|null, default `null`) — Grouped stable permission keys and labels.
- `values` (array|null, default `null`) — Selected permission keys by role key.
- `locked` (array|null, default `null`) — Server-owned permission keys by role.
- `inherited` (array|null, default `null`) — Inherited permission keys by role.
- `status-message` (string|null, default `null`) — Success feedback after redirect.
- `submit-label` (string, default `Save permissions`) — Save action label.
## Use when
- Use to assign a known permission allowlist to a small set of application roles.
## Avoid when
- Use a policy editor when access depends on conditions, resource attributes, or a large number of roles.
## Rules
- Render the installed block with `<x-blocks.permission-matrix-roles />` and pass data through its props; edit the copied file only for structural changes.
- Keep the semantic design tokens (`bg-background`, `text-muted-foreground`); never swap in raw colour utilities.
- Keep the `data-slot` attributes and the logical (start/end) spacing so the markup still mirrors under `dir="rtl"`.
## Links
- Docs: https://brokui.dev/blocks/permission-matrix-roles
- Registry JSON (files, props, contract): https://brokui.dev/r/open/blocks/permission-matrix-roles.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Guidance
Use when
- Use to assign a known permission allowlist to a small set of application roles.
Avoid when
- Use a policy editor when access depends on conditions, resource attributes, or a large number of roles.
Anti-patterns
- Do not replace semantic props with conflicting utility classes.
- Do not remove labels, focus styles, or state attributes.
- Anatomy
- Theming hooks
Usage
Render the block as a component, passing data where useful:
<x-blocks.permission-matrix-roles />
Built from primitives
This block composes installed <brok:*> primitives and semantic design
tokens only — it does not reimplement any primitive. Re-theme it (light, dark, admin, customer) by
editing CSS variables; flip the preview to RTL to confirm it mirrors.
Source
The exact, editable file ui:add writes
into your app. The preview above renders this same source — there are no preview-only blocks.
@props([
'action' => '#',
'method' => 'PATCH',
'title' => 'Role permissions',
'description' => 'Choose the permissions that each role receives.',
'roles' => null,
'permissions' => null,
'values' => null,
'locked' => null,
'inherited' => null,
'statusMessage' => null,
'submitLabel' => 'Save permissions',
])
@php
$roles ??= [
['key' => 'viewer', 'name' => 'Viewer', 'description' => 'Read-only access'],
['key' => 'editor', 'name' => 'Editor', 'description' => 'Creates and updates content'],
['key' => 'admin', 'name' => 'Admin', 'description' => 'Manages the workspace'],
];
$permissions ??= [
['group' => 'Projects', 'key' => 'projects.view', 'name' => 'View projects', 'description' => 'Read project details and activity.'],
['group' => 'Projects', 'key' => 'projects.update', 'name' => 'Update projects', 'description' => 'Change project content and status.'],
['group' => 'People', 'key' => 'members.invite', 'name' => 'Invite members', 'description' => 'Send workspace invitations.'],
['group' => 'Administration', 'key' => 'billing.manage', 'name' => 'Manage billing', 'description' => 'Change plans and payment details.'],
];
$values ??= [
'viewer' => ['projects.view'],
'editor' => ['projects.view', 'projects.update'],
'admin' => ['projects.view', 'projects.update', 'members.invite', 'billing.manage'],
];
$locked ??= ['admin' => ['billing.manage']];
$inherited ??= ['editor' => ['projects.view'], 'admin' => ['projects.view']];
@endphp
<x-ui.form.guard
as="section"
data-slot="permission-matrix"
data-surface="admin"
{{ $attributes->merge(['class' => 'bg-background text-foreground']) }}
>
<x-ui.form
:action="$action"
:method="$method"
:summary="__('Please correct the permission errors below.')"
data-persisted-form
x-data="{
toggleRole(role, checked) {
this.$root.querySelectorAll('[data-permission-control]').forEach((control) => {
if (control.dataset.role === role && ! control.disabled) control.checked = checked
})
this.$dispatch('brok:form-dirty')
}
}"
>
<x-ui.card>
<x-ui.card.header>
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<x-ui.card.title>{{ __($title) }}</x-ui.card.title>
<x-ui.card.description class="mt-2">{{ __($description) }}</x-ui.card.description>
</div>
<x-ui.badge variant="outline">{{ __('Server-authoritative') }}</x-ui.badge>
</div>
@if ($statusMessage)
<x-ui.alert variant="success" role="status" class="mt-4">{{ __($statusMessage) }}</x-ui.alert>
@endif
</x-ui.card.header>
<x-ui.card.content class="!p-0">
<div class="overflow-x-auto">
<table class="w-full min-w-[48rem] border-collapse text-sm">
<caption class="sr-only">{{ __('Permission assignments by role') }}</caption>
<thead>
<tr class="border-y border-border bg-muted/50">
<th scope="col" class="px-6 py-4 text-start font-medium text-muted-foreground">{{ __('Permission') }}</th>
@foreach ($roles as $role)
@php
$roleKey = (string) $role['key'];
@endphp
<th scope="col" class="min-w-40 px-4 py-4 text-start align-top">
<div class="flex items-start gap-2">
<x-ui.checkbox
:aria-label="__('Select all available permissions for :role', ['role' => __($role['name'])])"
:data-role="$roleKey"
@change="toggleRole($event.target.dataset.role, $event.target.checked)"
/>
<span>
<span class="block font-medium text-foreground">{{ __($role['name']) }}</span>
<span class="mt-0.5 block text-xs font-normal text-muted-foreground">{{ __($role['description']) }}</span>
</span>
</div>
</th>
@endforeach
</tr>
</thead>
<tbody>
@foreach ($permissions as $index => $permission)
@if ($index === 0 || $permissions[$index - 1]['group'] !== $permission['group'])
<tr>
<th colspan="{{ count($roles) + 1 }}" scope="colgroup" class="border-b border-border bg-muted/30 px-6 py-2 text-start text-xs font-semibold uppercase tracking-wide text-muted-foreground">
{{ __($permission['group']) }}
</th>
</tr>
@endif
<tr class="border-b border-border last:border-b-0">
<th scope="row" class="px-6 py-4 text-start font-normal">
<span class="block font-medium text-foreground">{{ __($permission['name']) }}</span>
<span class="mt-2 block text-xs text-muted-foreground">{{ __($permission['description']) }}</span>
</th>
@foreach ($roles as $role)
@php
$roleKey = (string) $role['key'];
$permissionKey = (string) $permission['key'];
$selected = in_array($permissionKey, old("permissions.$roleKey", $values[$roleKey] ?? []), true);
$isLocked = in_array($permissionKey, $locked[$roleKey] ?? [], true);
$isInherited = in_array($permissionKey, $inherited[$roleKey] ?? [], true);
$isDisabled = $isLocked || $isInherited;
@endphp
<td class="px-4 py-4 align-middle">
<div class="flex items-center gap-2">
<x-ui.checkbox
name="permissions[{{ $roleKey }}][]"
:value="$permissionKey"
:checked="$selected"
:disabled="$isDisabled"
data-permission-control
:data-role="$roleKey"
:aria-label="__(':permission for :role', ['permission' => __($permission['name']), 'role' => __($role['name'])])"
/>
@if ($isLocked)
<x-ui.badge variant="outline" size="sm">{{ __('Locked') }}</x-ui.badge>
@elseif ($isInherited)
<x-ui.badge variant="secondary" size="sm">{{ __('Inherited') }}</x-ui.badge>
@endif
</div>
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
</div>
</x-ui.card.content>
<x-ui.card.footer class="flex flex-col gap-4 border-t border-border sm:flex-row sm:items-center sm:justify-between">
<p class="text-xs text-muted-foreground">{{ __('Locked and inherited values must be preserved by the server.') }}</p>
<x-ui.button type="submit" x-bind:disabled="submitting" x-bind:aria-busy="submitting">
<span x-show="! submitting">{{ __($submitLabel) }}</span>
<span x-cloak x-show="submitting">{{ __('Saving…') }}</span>
</x-ui.button>
</x-ui.card.footer>
</x-ui.card>
</x-ui.form>
</x-ui.form.guard>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.1.4 - Status
- Stable
- License
-
open - Deprecation
- Not deprecated
- Contract
-
v2 - Foundation
-
≥ 1.0.0