Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Tables & Lists

Permissions Matrix

Tables & Lists

Permissions Matrix React component for Tailwind CSS v4

Members tableTable
Live props
Content
mode
defaultSearch
searchPlaceholder
Appearance
density
tone
color
showDescriptions
showCounts
State
readOnly
Behavior
searchable
enforceDependencies
collapsibleGroups
Layout
stickyHeader
maxHeightAuto
mobileLayout
layout
Code
import { PermissionsMatrix } from "@/components/ui/permissions-matrix"

<PermissionsMatrix />
  1. Home
  2. Components
  3. Tables & Lists
  4. Permissions Matrix
Pro

Permissions Matrix React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React permissions matrix of roles by permissions, grouped by module, with dependencies, unsaved-change dots and a per-role list on phones.

Tick a cell to grant a permission to a role; the group row's box grants or clears the whole module and shows a dash while only some are on. Granting a permission that needs others (Issue invoices needs View invoices) ticks them too and the cells that changed on their own flash once; revoking a requirement clears what depends on it, and both are announced. Cells that differ from the saved state get a dot, a faint tint and a count in the toolbar. The Owner column is locked (hover, focus or long-press a cell for why), and a role name cut by a narrow column shows whole on hover or a long press. Type in the search field to keep only the matching rows, with the letters marked and the number of matches at the end of the field; a matching module name keeps the whole module. Click a module name (or press Enter or Space on it) to fold it, during a search too (the next query opens it again); Expand all / Collapse all does every module. The grid is one Tab stop: arrows move between cells, module rows and labels included, Home / End jump to the first / last role, Ctrl or Cmd + Home / End to the corners, PageUp / PageDown move eight rows, Space toggles the focused box or opens the level select. The head sticks while you scroll and the label column stays pinned while wide grids scroll sideways, each gaining a hairline shadow only while something slides under it. Below 768px the grid becomes one accordion card per role with a switch per permission and a switch per module (or role chips over one list with mobileLayout="role-picker"); level selects open as bottom sheets.

Permissions Matrix is a Pro React component in the React table and list components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add permissions-matrix, together with Accordion, Badge, Button, Checkbox, Empty State and 11 more and the npm packages motion and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add permissions-matrix

Permissions Matrix is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import PermissionsMatrix from @/components/ui/permissions-matrix.

import { useState } from "react"
import { FileText, Users } from "lucide-react"
import {
  PermissionsMatrix,
  countPermissionChanges,
  type PermissionGroup,
  type PermissionRole,
  type PermissionsValue,
} from "@/components/ui/permissions-matrix"

const roles: PermissionRole[] = [
  { id: "owner", name: "Owner", locked: true, memberCount: 1 },
  { id: "sales", name: "Sales", memberCount: 3 },
  { id: "accounting", name: "Accounting", memberCount: 1 },
]

const groups: PermissionGroup[] = [
  {
    id: "clients",
    label: "Clients",
    icon: <Users />,
    permissions: [
      { id: "clients.view", label: "View clients" },
      { id: "clients.delete", label: "Delete clients", requires: ["clients.view"], dangerous: true },
    ],
  },
  {
    id: "invoices",
    label: "Invoices",
    icon: <FileText />,
    permissions: [
      { id: "invoices.view", label: "View invoices" },
      { id: "invoices.issue", label: "Issue invoices", requires: ["invoices.view"] },
    ],
  },
]

const saved: PermissionsValue = { sales: ["clients.view"], accounting: ["clients.view", "invoices.view", "invoices.issue"] }

// 1 change, 3 changes
const changesLabel = (n: number) => (n === 1 ? "1 change" : `${n} changes`)

export function RolePermissions() {
  const [value, setValue] = useState<PermissionsValue>(saved)
  const changes = countPermissionChanges(value, saved, { ignoreRoles: ["owner"] })
  return (
    <div className="flex flex-col gap-3">
      <PermissionsMatrix
        roles={roles}
        groups={groups}
        value={value}
        onValueChange={setValue}
        compareTo={saved}
        searchPlaceholder="Search permissions"
        labels={{ changes: changesLabel, permission: "Permission", caption: "Permissions by role" }}
      />
      {changes > 0 && <p className="text-sm text-muted-foreground">{changesLabel(changes)} to save</p>}
    </div>
  )
}

Props

  • roles*
    PermissionRole[]

    The columns: { id, name, description?, locked? (every permission, not editable), tone? / color? (a dot and the granted bar), memberCount? }. Zero roles shows an empty state.

  • groups*
    PermissionGroup[]

    The rows, by module: { id, label, description?, icon?, permissions: { id, label, description?, requires?: string[], dangerous? }[] }.

  • value
    PermissionsValue

    Controlled grants. Boolean mode: Record<roleId, string[]>. Levels mode: Record<roleId, Record<permissionId, level>>. Ids that are not in groups are kept untouched; locked roles are ignored (always everything).

  • defaultValuedefault "{}"
    PermissionsValue

    The grants to start with while uncontrolled.

  • onValueChange
    (value: PermissionsValue, change: { roleId: string; permissionIds: string[]; granted: boolean | string }) => void

    Every change with the next value. permissionIds lists every cell that changed, dependencies included (the one you pressed first). The grid resolves them with resolvePermissionDependencies (see Parts), which you can reuse to validate a change on the server.

  • compareTo
    PermissionsValue

    The saved state. Cells that differ get data-changed, a dot (with a screen reader "changed") and a faint tint, and the toolbar shows the count. countPermissionChanges (see Parts) gives the same count for a save bar, on the server too.

  • modedefault "boolean"
    PermissionsMatrixMode = "boolean" | "levels"

    levels: every cell is a compact ghost select of levels (None / View / Edit / Full), the module row sets all at once.

  • levelsdefault "[{ value: "none", label: "None" }, { value: "view", label: "View" }, { value: "edit", label: "Edit" }, { value: "full", label: "Full" }]"
    PermissionLevel[]

    { value, label, short? } for levels mode; the cell shows short or the label. The first level means not granted, the second is what a requirement is raised to. An unknown value in the data shows as is.

  • defaultSearchdefault ""
    string

    The search to start with, uncontrolled. Pair search / onSearchChange to drive it.

  • search
    string

    Controlled search text.

  • onSearchChange
    (query: string) => void

    Called as the search text changes.

  • searchPlaceholderdefault "Search permissions"
    string

    The search field's placeholder and accessible name.

  • labels
    Partial<PermissionsMatrixLabels>

    Every visible and announced string, English by default (see Parts): caption, permission, changes(n), changed, locked, requires(names), dangerous, granted(permission, role), revoked(permission, role), levelSet(permission, role, level), alsoGranted(names), alsoRevoked(names), expandAll, collapseAll, noResults, noResultsDescription, clearSearch, results(n) (the match count in the search field, announced), noRoles, noRolesDescription, members(n), count(granted, total), group(label, role), mixed, yes, no, role.

  • densitydefault "default"
    "compact" | "default"

    Rows of 40 or 48px; the compact row grows to 44px on touch.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The checkbox, switch, role chip and changed-dot color.

  • colordefault ""
    string

    Any CSS color; replaces the tone. The check turns black or white by lightness.

  • showDescriptionsdefault true
    boolean

    Permission descriptions under their labels (two lines at most).

  • showCountsdefault true
    boolean

    The granted count ("12/18") and a small bar under every role; on phones the count badge of each role and the count beside each module switch.

  • readOnlydefault false
    boolean

    Cells become check and dash icons (levels show their name); module rows show a count. All roles locked means read-only too.

  • searchabledefault true
    boolean

    A search field that keeps only the matching rows (modules with no match hide) and marks the matches.

  • enforceDependenciesdefault true
    boolean

    Granting adds what a permission requires, revoking a requirement revokes its dependents; the cells that changed on their own flash and the change is announced.

  • collapsibleGroupsdefault true
    boolean

    Module rows fold their permissions away; adds Expand all / Collapse all to the toolbar.

  • defaultCollapsedGroupsdefault "[]"
    string[]

    Module ids folded at first.

  • stickyHeaderdefault true
    boolean

    The role row sticks to the top of the scroller (or of the page at --topbar-offset when maxHeight is 0 and the grid fits sideways).

  • maxHeightdefault 0
    number

    Caps the grid's height in px so it scrolls inside; 0 grows with the page.

  • mobileLayoutdefault "accordion"
    "accordion" | "role-picker"

    Below 768px: one accordion card per role with switches, or a row of role chips over one list. The grid never scrolls sideways on a phone.

  • layoutdefault "auto"
    "auto" | "grid" | "list"

    auto: the grid from 768px, the phone list below. grid or list force one, e.g. list in a narrow side panel.

  • onRoleClick
    (role: PermissionRole) => void

    Turns the role headers into buttons (edit the role); they join the grid's arrow navigation.

  • classNames
    Partial<Record<"toolbar" | "search" | "changes" | "expandAll" | "panel" | "scroller" | "table" | "head" | "role" | "group" | "groupToggle" | "row" | "permission" | "cell" | "mobile" | "mobileRole" | "empty", string>>

    Extra classes for the inner parts.

  • className
    string

    Extra classes for the root; the remaining props spread onto it.

Parts

  • resolvePermissionDependencies

    function

    resolvePermissionDependencies(groups, value, roleId, permissionId | ids, checked, { mode?, levels?, enforce? }) grants or revokes for one role and follows requires both ways. Returns { value, permissionIds, alsoGranted, alsoRevoked }; pure, so it can validate a change on the server.

  • countPermissionChanges

    function

    countPermissionChanges(value, compareTo, { emptyLevel?, ignoreRoles? }) counts the (role, permission) cells that differ, for a save bar or a server check.

  • DEFAULT_PERMISSIONS_MATRIX_LABELS

    constant

    Every English string of the grid, the toolbar and the phone layouts, to spread under a translation.

  • DEFAULT_PERMISSION_LEVELS

    constant

    The levels used in levels mode when you pass none: None, View, Edit and Full ("none", "view", "edit", "full").

  • PermissionGroup

    type

    A module row group: { id, label, description?, icon?, permissions }, each permission { id, label, description?, requires?, dangerous? }.

  • PermissionRole

    type

    A column: { id, name, description?, locked?, tone?, color?, memberCount? }; a locked role has everything and cannot be edited.

  • PermissionsValue

    type

    The value: role id to granted permission ids (boolean mode), or role id to permission id to level (levels mode).

Examples

Levels per module

mode="levels": each cell is a compact select (None, View, Edit, Full).

Read-only

An auditor's view: checks and dashes, nothing to press.

Search with highlights

Searching "invoice" keeps the matching rows and marks the letters.

Dependencies

Granting Void invoices also grants Issue and View; the change is announced.

On a phone

One accordion item per role with switches, or role chips over one list.

Compact, 10 roles

The label column stays pinned while the roles scroll sideways; the head sticks inside a 420px box.

Dependencies

motionlucide-reactaccordionbadgebuttoncheckboxempty-statesearch-inputselectswitchtooltipuse-controllable-stateuse-media-queryuse-scroll-edgestext-matchmotiontonesui-config

Files

  • components/ui/permissions-matrix.tsx

FAQ

Is the React Permissions Matrix component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Permissions Matrix?

Run npx wingo-ui@latest add permissions-matrix. It also adds the Wingo UI items it builds on (Accordion, Badge, Button, Checkbox, Empty State and 11 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/permissions-matrix.

How does Permissions Matrix behave on phones?

Below 768px the grid becomes one accordion card per role with a switch per permission and a switch per module (or role chips over one list with mobileLayout="role-picker"); level selects open as bottom sheets.

See also

Members tableSettings LayoutCheckboxAccordionTableTree View

Used in

Users Admin Page

More React table and list components

Task ListVirtual ListKanbanData TableDeal PipelineAudit Log
All 11 React table and list components

Articles using this component

  • Role Management UI in React: Members, Roles, Audit Log

    Build it: screens and apps · Nov 5, 2026 · 9 min read

PreviousMembers tableNext Table
4 changes
Permissions by role
Permission
View clientsThe client list, profiles and history
Create clientsAlso needs View clientsAdd new clients with a verified tax ID
Edit clientsAlso needs View clientsContact details, addresses, account owner
Delete clientsSensitive permissionAlso needs View clientsPermanently, with all their history
Export clientsAlso needs View clientsDownload the list as CSV or Excel, changed
View invoicesIssued invoices, payments and due dates
Issue invoicesAlso needs View invoicesSeries INV, with tax calculated automatically
Void invoicesSensitive permissionAlso needs Issue invoicesReverse an issued invoice
Sync to QuickBooksAlso needs Issue invoicesPush invoices to the accounting app
View quotesSent quotes and their status
Create quotesAlso needs View quotesFrom the product and service catalog
Approve discounts over 10%Also needs View quotesOtherwise the quote waits for approval, changed
View reportsSales, payments, pipeline, changed, changed
Export reportsAlso needs View reportsPDF and Excel
Invite membersSend invitations by email
Change rolesSensitive permissionIncluding their own role
Company settingsTax ID, address, bank account, invoice series
Billing and subscriptionThe Acme CRM plan and payment method

Clients5/5
Invoices4/4
Quotes3/3
Reports2/2
Team and settings3/4

4 changes
Changes apply to members the next time they sign in.

Click a role name to edit it · enforceDependencies=true · stickyHeader=true