Bootstrap User Permission Matrix — Free HTML CSS JS Snippet
Bootstrap User Permission Matrix · Dashboards · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="container py-5 d-flex justify-content-center">
<div class="card bsperm-card">
<div class="card-body p-3">
<h6 class="fw-bold mb-2">Role permissions</h6>
<div class="table-responsive">
<table class="table table-sm align-middle mb-0" id="bspermTable">
<thead>
<tr>
<th>Permission</th>
<th class="text-center">Viewer</th>
<th class="text-center">Editor</th>
<th class="text-center">Admin</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<p class="small text-muted mt-2 mb-0">Click a cell to toggle it. Admin always keeps every permission.</p>
</div>
</div>
</div>.bsperm-card { width: 420px; max-width: 100%; border: 1px solid #eceef1; border-radius: 14px; }
.bsperm-cell { cursor: pointer; font-size: 16px; }
.bsperm-cell.bsperm-locked { cursor: not-allowed; opacity: .6; }
.bsperm-on { color: #198754; }
.bsperm-off { color: #d1d5db; }const PERMISSIONS = ['View content', 'Edit content', 'Publish content', 'Manage billing', 'Manage users'];
const ROLES = ['viewer', 'editor', 'admin'];
// Admin is modeled as always-true and locked, rather than just pre-checked —
// this is what actually prevents someone from accidentally locking an admin
// out of a permission the role is supposed to always guarantee.
const grid = {
viewer: [true, false, false, false, false],
editor: [true, true, true, false, false],
admin: PERMISSIONS.map(() => true),
};
const tbody = document.querySelector('#bspermTable tbody');
function render() {
tbody.innerHTML = PERMISSIONS.map((perm, row) =>
'<tr><td>' + perm + '</td>' +
ROLES.map(role => {
const locked = role === 'admin';
const on = grid[role][row];
return '<td class="text-center bsperm-cell' + (locked ? ' bsperm-locked' : '') +
'" data-role="' + role + '" data-row="' + row + '">' +
'<span class="' + (on ? 'bsperm-on' : 'bsperm-off') + '">' + (on ? '\u2713' : '\u2014') + '</span></td>';
}).join('') + '</tr>'
).join('');
}
tbody.addEventListener('click', e => {
const cell = e.target.closest('.bsperm-cell');
if (!cell || cell.classList.contains('bsperm-locked')) return;
const role = cell.dataset.role;
const row = Number(cell.dataset.row);
grid[role][row] = !grid[role][row];
render();
});
render();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Bootstrap User Permission Matrix — HTML, CSS & JavaScript

The permission state lives in one plain grid object keyed by role, each holding a boolean array indexed the same way as PERMISSIONS — grid.editor[2] is unambiguously "does the editor role have the third permission," which is what makes rendering the entire table a matter of mapping over two small arrays rather than hand-writing markup for every one of the fifteen cells.
Admin's row isn't just pre-checked — every cell in grid.admin is genuinely fixed to true and separately marked locked in the render function, and the click handler explicitly bails out on a locked cell before touching grid at all. That distinction matters: a "pre-checked but still editable" admin row would let someone accidentally uncheck "Manage billing" for admins and lock the whole team out of billing, which a real permission matrix should treat as structurally impossible, not just discouraged.
Toggling any unlocked cell is a single, symmetric operation — grid[role][row] = !grid[role][row] — with no special-casing per permission or per role beyond the one admin-lock check, which is what keeps adding a sixth permission or a fourth role a matter of extending the two source arrays rather than writing new toggle logic.
Step by step
How to Use
- 1Load the snippetA 5x3 grid renders with Viewer, Editor, and Admin columns, each cell showing a checkmark or dash.
- 2Click a checkmark or dash in the Viewer or Editor columnIt toggles immediately between granted and not granted.
- 3Try clicking any cell in the Admin columnNothing happens — every Admin cell is genuinely locked to always-granted, not just pre-checked.
- 4Toggle several Editor permissions on and offEach cell updates independently, with no effect on Viewer or Admin.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
A pre-checked-but-editable cell can be accidentally unchecked by a stray click, silently removing a permission a role is supposed to always have — locking it structurally (both visually and in the click handler) makes that mistake impossible rather than just unlikely.
Yes — add it to the ROLES array and give it its own boolean array in grid keyed by that role name; render() and the click handler both iterate ROLES generically and need no changes.
Add its label to PERMISSIONS and a corresponding boolean to every role's array in grid, at the same index — the table automatically grows by one row.
Yes. Keep the grid object in component state, and toggle a cell by producing a new grid object with that one role/row value flipped (respecting your framework's immutability conventions) rather than mutating it directly.