Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
  1. Home
  2. Blog
  3. Build it: screens and apps
  4. Role Management UI in React: Members, Roles, Audit Log
Auf Deutsch lesen
  1. Blog
  2. Build it: screens and apps
  3. Role Management UI in React: Members, Roles, Audit Log
Build it: screens and apps
Build it: screens and apps

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

SR
Serban Rusu · Founder of Wingo UI
Nov 5, 2026 · 9 min read

On this page

0%
  1. What belongs on a user role management UI?
  2. Should you start from a block gallery or a full admin screen?
  3. How do you lay out the team page in Next.js?
  4. How should the members tab handle invitations and seats?
  5. How do you design a permissions matrix UI people can read?
    1. Why a save bar instead of saving each checkbox?
  6. What does the matrix become on a phone?
  7. What should the audit log record?
  8. Where do you enforce the permissions?
  9. Where do you go from here?
  10. FAQ
    1. What is a role management UI?
    2. Should a permissions matrix save every checkbox right away?
    3. How do I show a permissions matrix on a phone?
    4. Is hiding buttons enough to enforce roles?
    5. Do pending invitations count as seats?

TL;DR

A role management UI for a B2B SaaS needs three tabs: members and invitations counted against your seats, roles edited in a permissions matrix that saves through one save bar, and an audit log of who changed what. In React you can assemble it from the Wingo UI Users Admin Page block, pass Server Actions for every change, and validate the permission set again on the server. On phones the members become cards and the matrix shows one role at a time.

Published Nov 5, 2026

Every B2B product reaches the day a customer asks "can my accountant see invoices but not delete clients?" That question needs a role management UI: a team screen where an admin invites people, gives each one a role, decides what each role may do and can later see who changed what. Search for it and you find block galleries with a members table and an invite dialog, and Dribbble shots of a checkbox grid. Few explain how the parts behave together: seats, dependencies between permissions, unsaved changes, the audit trail and the phone layout. This tutorial builds that screen in Next.js from Wingo UI parts, with the code you need.

What belongs on a user role management UI?

Three tabs cover what admins actually do, in the order they do it:

  1. Members and invitations. The team with a role picker per row, status, last activity and two-step verification, plus pending invitations. A seat meter on top, because inviting is where plans run out.
  2. Roles and permissions. One card per role and a matrix of roles by permissions, grouped by module (Clients, Deals, Invoices, Reports, Settings and team). A save bar collects the edits.
  3. Audit log. Who did what, when, to which record, from which IP, with filters and a CSV export for the security questionnaire your customer will send.

The details decide whether admins trust the screen. The last administrator cannot be demoted, nobody removes the owner or themselves, and a failed request puts the old value back and says so. A user role management UI that shows success and then quietly loses the change is worse than none.

Should you start from a block gallery or a full admin screen?

Start from a block when roles are fixed, and from a full screen once customers can edit roles. Most results for a react user management template are blocks: a members table and an invite dialog. Facts from each public page, as of November 2026:

OptionWhat it includesPrice
shadcnblocks "User Table with Dialog Invite" (opens in a new tab)Members table (name, email, role, status), invite dialog with several addresses and a role selectFree
shadcn.io "Account Team Members" (opens in a new tab)Roles per person, pending invitations with resend and withdraw, a seat count, 2FA nudges, removal confirmationPro plan
Wingo UI Users Admin PageMembers and invitations with seats, a permissions matrix with save bar, an audit log, phone layoutsPro

If your product has three fixed roles and no customer will ever ask for custom ones, the free shadcnblocks table is enough: a role select per row is the whole feature. Once roles become editable you need the matrix, the draft and save model, and the history, and that is the screen this post builds. The Wingo parts are Pro; pricing starts at $8 a month.

How do you lay out the team page in Next.js?

Put the team screen in one route: a server page.tsx that loads data, a client view that wires the handlers, and a permission catalog both sides import. The page sits in the admin route group from the Next.js admin dashboard tutorial, which sets up the Admin Shell and the server/dal.ts module with requireAdmin(). Install the block (it pulls in the table, the matrix and the log):

bash
npx wingo-ui@latest add users-admin-page
text
app/(admin)/team/
page.tsx server: loads members, invites, roles, grants, audit rows
team-view.tsx client: UsersAdminPage + Server Actions
permissions.ts the permission catalog, shared by the view and the server
actions.ts invite, changeRole, removeMember, revokeInvite, savePermissions

Keep the permission catalog in a plain module without "use client". The matrix renders it, and the server validates against the same list, so the two cannot drift:

ts
// app/(admin)/team/permissions.ts
import type { PermissionGroup, PermissionRole } from "@/components/ui/permissions-matrix";
export const PERMISSION_GROUPS: PermissionGroup[] = [
{
id: "clients",
label: "Clients",
permissions: [
{ id: "clients.view", label: "View clients" },
{ id: "clients.edit", label: "Edit clients", requires: ["clients.view"] },
{ id: "clients.export", label: "Export clients", requires: ["clients.view"], dangerous: true },
],
},
{
id: "invoices",
label: "Invoices",
permissions: [
{ id: "invoices.view", label: "View invoices" },
{ id: "invoices.issue", label: "Issue invoices", requires: ["invoices.view"] },
{ id: "invoices.cancel", label: "Cancel invoices", requires: ["invoices.issue"], dangerous: true },
],
},
];
// the administrator holds every permission and cannot be edited
export const PERMISSION_ROLES: PermissionRole[] = [
{ id: "admin", name: "Administrator", locked: true },
{ id: "sales", name: "Sales" },
{ id: "accounting", name: "Accounting" },
{ id: "viewer", name: "Read only" },
];

Keep the administrator's id as admin: the block's last-administrator guard looks for that role.

The view is a thin client component that turns Server Action results into the promises the block expects. A rejected promise makes the block roll back and show the message:

tsx
// app/(admin)/team/team-view.tsx
"use client";
import { UsersAdminPage, type UsersAdminPageProps } from "@/components/blocks/users-admin-page";
import { PERMISSION_GROUPS, PERMISSION_ROLES } from "./permissions";
import { changeRole, invite, removeMember, revokeInvite, savePermissions } from "./actions";
type Props = Pick<
UsersAdminPageProps,
"companyName" | "members" | "invites" | "roles" | "defaultPermissions" | "auditEvents" | "seats" | "canManage"
>;
async function check(result: Promise<{ error?: string }>) {
const { error } = await result;
if (error) throw new Error(error);
}
export function TeamView(props: Props) {
return (
<UsersAdminPage
{...props}
permissionRoles={PERMISSION_ROLES}
permissionGroups={PERMISSION_GROUPS}
onInvite={(input) => check(invite(input))}
onRoleChange={(member, role) => check(changeRole(member.id, role))}
onRemove={(member) => check(removeMember(member.id))}
onRevokeInvite={(item) => check(revokeInvite(item.id))}
onPermissionsSave={(value) => check(savePermissions(value))}
/>
);
}

page.tsx loads the rows, computes canManage from the session and renders <TeamView />. With canManage={false} the page turns read-only and explains why, which is better than hiding the team from people who only need to see it.

How should the members tab handle invitations and seats?

Count pending invitations as used seats, because inviting is where plans run out. In the block an invitation holds a seat until it expires: the meter under the title shows used seats with a line like "2 seats held by invitations" and switches to its warning color at 80%. When members and invitations fill every seat, Invite people first offers to add seats (seatStep, five by default, through onAddSeats) and then opens the form.

A good invite team members UI accepts what people paste: the Members Table splits a list like Emma Carter <emma@northwind.app>; daniel@northwind.app into addresses, and lists the ones it refused under Not added with the reason (invalid, already a member, already invited). More addresses than free seats keeps Send disabled and says why.

Role changes in a row are optimistic: the picker shows the new role with a spinner, and a rejection rolls it back with the error. The block refuses to demote the last administrator before it calls you, but your changeRole action must refuse it too, because a Server Action can be called without the button.

Change a member's role, then switch to Roles and permissions and tick a few cells: the save bar counts your changes
$ npx wingo-ui@latest add users-admin-page
ProUsers Admin Page docs

How do you design a permissions matrix UI people can read?

A permissions matrix UI fails in two ways: too many rows to scan, and checkboxes that allow states your backend rejects. The Permissions Matrix handles both with a few rules worth copying even if you build your own.

  • Group by module, verbs inside. Rows are grouped (Clients, Invoices) with a group checkbox that shows a dash when only some are on. Reuse the same verbs across modules (view, edit, export) so a row reads the same in every group.
  • Encode dependencies. requires: ["invoices.view"] on Issue invoices means granting it also grants View, and revoking View clears Issue. The cells that changed on their own flash once and the change is announced to screen readers.
  • Mark the dangerous ones. dangerous: true adds a danger dot to exports and deletions, the permissions a security review asks about.
  • Lock the top role. A locked column has everything and explains why on hover, focus or long press, so nobody wonders why Administrator cannot be edited.
  • Show the diff. Pass the saved value as compareTo and cells that differ get a dot and a tint, with a count in the toolbar.

For roles that need more than yes or no, mode="levels" turns each cell into a None / View / Edit / Full select.

Grant Issue invoices to a role: View invoices turns on with it, and the changed cells get a dot
$ npx wingo-ui@latest add permissions-matrix
ProPermissions Matrix docs

Why a save bar instead of saving each checkbox?

Because one permission change affects every member with that role. Collecting edits in a draft lets an admin set up a role completely, see the count ("3 unsaved changes") and save once, or discard. The block uses countPermissionChanges(draft, saved, { ignoreRoles }) for that count and a save bar from Settings Layout that slides up with Discard and Save, plus mod+s while it shows. Switching tabs with unsaved changes asks first. A single member's role, by contrast, saves per click: it is one clear decision about one person.

What does the matrix become on a phone?

It shows one role at a time. At 390px a grid of five roles by twenty permissions does not fit, and a sideways scroll hides the role names while you tick, so in its default layout the matrix does not scroll sideways on a phone. Below 768px it offers two layouts through mobileLayout: "accordion" (one card per role with a switch per permission and per module) or "role-picker" (role chips over one list). The Users Admin Page uses the role picker, because admins usually edit one role at a time.

Never stack two bottom bars on one edge. The save bar floats above the edge and respects the safe area, and onDirtyChange tells your shell when to hide its phone tab bar while changes are pending. The members tab follows the same rule as any responsive table: rows become cards with a full-width role picker, and the invite dialog and menus open as bottom sheets.

What should the audit log record?

Record every change an admin makes on this screen, and the security events around it: invitations sent and accepted, role changes, removals, role permission edits, failed sign-ins and exports. Each Audit Log event carries actor, action, target, severity, ip, device and a changes list with before and after values, which the row expands into a diff.

Write those rows in the same transaction as the change. The block adds this page's changes to the top of the log as instant feedback, but the source of truth is your table. The block's audit tab filters and exports the auditEvents you pass, in the browser, so load a bounded window (the last 90 days, say). For a log too large for that, render the Audit Log yourself with mode="server", onQueryChange, hasMore and onLoadMore, and give it an onExport that builds the CSV on the server instead of from the rows on screen.

Filter the log by user or action, expand a row for its before and after values, then export the rows as CSV
$ npx wingo-ui@latest add audit-log
ProAudit Log docs

Where do you enforce the permissions?

On the server, every time. The matrix stops impossible states in the browser, and savePermissions checks them again against the same catalog:

ts
// app/(admin)/team/actions.ts
"use server";
import type { PermissionsValue } from "@/components/ui/permissions-matrix";
import { requireAdmin } from "@/server/dal";
import { replaceRolePermissions } from "@/server/team";
import { PERMISSION_GROUPS, PERMISSION_ROLES } from "./permissions";
const REQUIRES = new Map(
PERMISSION_GROUPS.flatMap((group) => group.permissions.map((p) => [p.id, p.requires ?? []] as const)),
);
const EDITABLE = new Set(PERMISSION_ROLES.filter((role) => !role.locked).map((role) => role.id));
export async function savePermissions(value: PermissionsValue): Promise<{ error?: string }> {
const admin = await requireAdmin();
const grants: Record<string, string[]> = {};
for (const [roleId, granted] of Object.entries(value)) {
if (!EDITABLE.has(roleId)) continue; // locked roles are never written
if (!Array.isArray(granted)) return { error: "Unexpected permission format" };
const set = new Set(granted);
for (const id of granted) {
const needs = REQUIRES.get(id);
if (!needs) return { error: `Unknown permission ${id}` };
const missing = needs.find((need) => !set.has(need));
if (missing) return { error: `${id} needs ${missing}` };
}
grants[roleId] = granted;
}
// checks each role id belongs to the workspace, then writes the grants and one role.updated audit row per changed role, in one transaction
return replaceRolePermissions(admin.workspaceId, admin.userId, grants);
}

The helpers resolvePermissionDependencies and countPermissionChanges are exported too, but from a client module, so the server reads the plain catalog instead. Your data layer then checks a permission per request (can(user, "invoices.cancel")), not a role name, so a renamed or new role works without code changes.

Where do you go from here?

If you only need members, invitations and a role per person, start with the Members Table. For the whole team screen, open the Users Admin Page demo: try a role change, a matrix edit and the audit filters, then install it with npx wingo-ui@latest add users-admin-page. Every demo and its props are public without an account; the source of these four parts needs Pro. The rest of the admin lives in the Build it category, including the settings page tutorial that uses the same save bar.

Components in this post

  • Users Admin Page

    A React users admin page: team members and invitations with seats, roles with a permissions matrix and a save bar, and the audit log.

    Pro
  • Permissions Matrix

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

    Pro
  • Members table

    A React members table for B2B team settings: a role select per row, status, activity, 2FA, pending invitations, seats and an invite dialog.

    Pro
  • Audit Log

    A React audit log table: who did what, when and to which record, with filters, day groups, before / after diffs and a CSV export.

    Pro

FAQ

What is a role management UI?

It is the admin screen where a workspace owner sees who has access, invites people, assigns each person a role and decides what each role may do. Most B2B products also show the history of those changes in an audit log on the same screen.

Should a permissions matrix save every checkbox right away?

Usually not. Permission changes affect many people at once, so collect them in a draft, mark the changed cells and save the batch from a save bar, with Discard next to it. Saving per click works for a single member's role, which is one clear decision.

How do I show a permissions matrix on a phone?

Do not scroll a grid of five roles sideways at 390px. Show one role at a time, either as an accordion card per role or as role chips over one list of switches, and keep the save bar pinned above the bottom edge.

Is hiding buttons enough to enforce roles?

No. The interface only explains the rules. Every Server Action or API route has to check the caller's role and validate the submitted permissions, because anyone can send the request without your buttons.

Do pending invitations count as seats?

They should, until they expire or are revoked. Otherwise an admin can send more invitations than the plan allows and the last people to accept hit an error.

Try it in your project

Users Admin Page

Pro

A React users admin page: team members and invitations with seats, roles with a permissions matrix and a save bar, and the audit log.

75 components free, no account needed. Pro from $8 a month.

Open Users Admin Page liveSee pricing
  • RBAC
  • Admin
  • Next.js
  • B2B SaaS
  • Permissions

Share

SR

About the author

Serban Rusu

Founder of Wingo UI

Serban Rusu founded Wingo UI. He builds its component library, CLI and MCP server, and writes about React interfaces for phones and AI coding agents.

More from Serban
Keep reading

Related posts

All posts
Build it: screens and apps

Nextjs Admin Dashboard Tutorial: 5 Screens, Phone-Ready

A nextjs admin dashboard tutorial for the App Router: app shell, KPI home, server-side data tables, settings and team pages, each one usable on a phone.

SRSerban Rusu·Oct 9, 2026·23 min read
Component guides

Audit Log UI in React: Filters, Diffs and CSV Export

Build an audit log UI in React that people can use: who did what to which record, day groups, filters, before and after diffs, CSV export and the event shape.

SRSerban Rusu·Nov 6, 2026·10 min read
Component guides

React Diff Viewer in React 19: Split, Unified, Word Diffs

How a React diff viewer works, which packages still support React 19, and how to review AI edits, contracts and configs with split views and word highlights.

SRSerban Rusu·Oct 16, 2026·9 min read
Newsletter

Get new posts by email

New posts, product news and occasional offers from Wingo UI. We count clicks to keep it relevant.

No spam. Unsubscribe at any time.

WingoUI

Animated, configurable, mobile-first React components and blocks. Copy the source, make it yours, and let your coding agent build with it.

Product

  • Components
  • Templates
  • Pricing
  • AI agents
  • For teams
  • Theme generator

Resources

  • Docs
  • Contact
  • Programs
  • Blog
  • Changelog
  • Changelog RSS
  • llms.txt

Compare

  • Wingo UI vs paid kits
  • Wingo UI vs shadcn/ui
  • shadcn/ui alternatives
  • MUI alternatives
  • Tailwind Plus alternatives
  • Magic UI alternatives
  • Free components

Legal

  • Terms
  • Privacy
  • Refunds
  • License
  • Imprint

Component categories

  • Buttons & Actions
  • Inputs
  • Forms
  • Navigation
  • Overlays
  • Feedback
  • Data Display
  • Tables & Lists
  • Charts & Stats
  • Layout
  • Media
  • AI Kit
  • Text & Effects
  • Mobile
  • Commerce
  • Marketing Sections
  • Blocks
  • Hooks & Utilities

© 2026 Wingo UI

Wingo UI