Level Matrix
Set a level for each row and column pair, with pending changes and bulk editing, such as access by role and resource.
import { LevelMatrix, PermissionChange, PermissionResource, PermissionRole } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use a level matrix when access is decided per role and per resource: who can read billing, who can deploy, who can manage members. Each cell is an access level (none, read, write, admin) drawn as three segments lit to it, so a column or a row reads at a glance. For one person's settings, a list of switches reads better.
import { LevelMatrix, type PermissionValue } from '@youj/react'
const [value, setValue] = useState<PermissionValue>(saved)
<LevelMatrix
label="Workspace permissions"
roles={[
{ id: 'owner', name: 'Owner', locked: true },
{ id: 'editor', name: 'Editor', detail: '8 members' },
]}
resources={[
{ id: 'docs', name: 'Docs' },
{ id: 'billing', name: 'Billing' },
]}
value={value}
baseline={saved}
onValueChange={setValue}
/>A value maps a role's id to its levels by resource id; a missing cell is none. A locked role (an owner) can't change and sits out of bulk selection.
Description
Three roles across four resources, with an Apply button that counts the pending changes.
Against baseline (by default, the value it opened with), a changed cell keeps the level it had in view, struck through, and draws the difference in its segments: gained segments light in the success tone, lost ones stay as a hollow danger outline. diffPermissions(baseline, value, roles, resources) lists the changes in grid order, for a summary or a review before you save.
const changes = diffPermissions(saved, value, roles, resources)
// [{ role, resource, from: 'read', to: 'write' }, …]A row header's box selects the role's cells, a column header's box selects the resource for every role, and the corner selects everything. The bar that opens sets every selected cell to one level.
Code
level-matrix-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsroles, resources, value… (7)
Props
| Prop | Type | |
|---|---|---|
rolesRequired | readonly PermissionRole[] | – |
resourcesRequired | readonly PermissionResource[] | – |
value | PermissionValue | – |
defaultValue | PermissionValuedefault {} | {} |
onValueChange | (value: PermissionValue) => void | – |
baseline | PermissionValueThe saved levels: cells that differ from it show their pending gain or loss. Default: the value it opened with. | – |
label | stringAccessible name of the grid. Default: the labels' "Permissions". | – |
Other props pass through: extends ComponentProps<'div'>. | ||
MotionA cell changes, A bulk set, the bulk bar · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| A cell changesits segments light or empty one after another | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| A bulk setthe same, as a wave across the grid from the top-start corner | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| The bulk baropens and closes with the selection | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
Levels change in place.
Reduced motion
Levels change in place, without the segment stagger or the wave.
AccessibilityKeyboard, ARIA, announcements
Accessibility
- It's an ARIA grid, named by
label(or the labels' "Permissions") and multiselectable, with column and row headers and a keyboard hint as its description. - One tab stop, then the arrow keys move between cells, the header boxes included; Home and End go to a row's ends, and Ctrl+Home and Ctrl+End to the grid's. Space or Enter steps a cell up a level, and Shift steps it down.
- Every change is announced: "Editor, Billing: Write", or "6 permissions set to Read". A pending cell adds "changed from Read" to its name, selected cells are
aria-selected, and a locked role's cells are read-only. - Its built-in text comes from the labels, so
<YoujProvider labels>translates it.