User Role Permission Card — Role Presets Driving Conditional Checkbox State
User Role Card with Conditional Permission Checkboxes · Cards · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="demo">
<div class="perm-card">
<div class="perm-head">
<div class="avatar">RP</div>
<div>
<span class="perm-name">Rahul Patel</span>
<span class="perm-email">[email protected]</span>
</div>
</div>
<label class="perm-field">
<span>Role</span>
<select id="roleSelect">
<option value="viewer">Viewer</option>
<option value="editor" selected>Editor</option>
<option value="admin">Admin</option>
<option value="custom">Custom</option>
</select>
</label>
<div class="perm-list" id="permList">
<label class="perm-row"><input type="checkbox" data-perm="view" checked disabled /><span>View content</span></label>
<label class="perm-row"><input type="checkbox" data-perm="comment" checked /><span>Comment</span></label>
<label class="perm-row"><input type="checkbox" data-perm="edit" checked /><span>Edit content</span></label>
<label class="perm-row"><input type="checkbox" data-perm="publish" /><span>Publish changes</span></label>
<label class="perm-row"><input type="checkbox" data-perm="invite" /><span>Invite members</span></label>
<label class="perm-row"><input type="checkbox" data-perm="billing" /><span>Manage billing</span></label>
</div>
<p class="perm-note" id="permNote">Editors can edit and comment, but can't publish or manage the team.</p>
</div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f8fafc; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; }
.perm-card { width: 320px; max-width: 100%; background: #fff; border: 1px solid #e2e8f0; border-radius: 16px; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.perm-head { display: flex; align-items: center; gap: 10px; }
.avatar { width: 38px; height: 38px; border-radius: 50%; background: #6366f1; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 700; flex-shrink: 0; }
.perm-head div:last-child { display: flex; flex-direction: column; }
.perm-name { font-size: 13px; font-weight: 700; color: #111827; }
.perm-email { font-size: 11px; color: #94a3b8; }
.perm-field { display: flex; flex-direction: column; gap: 6px; }
.perm-field span { font-size: 11.5px; font-weight: 600; color: #64748b; }
.perm-field select { padding: 9px 11px; border: 1.5px solid #e2e8f0; border-radius: 9px; font-size: 12.5px; font-family: inherit; color: #111827; background: #fff; }
.perm-field select:focus-visible { outline: none; border-color: #6366f1; box-shadow: 0 0 0 3px rgba(99,102,241,0.15); }
.perm-list { display: flex; flex-direction: column; gap: 9px; border-top: 1px solid #f1f5f9; padding-top: 12px; }
.perm-row { display: flex; align-items: center; gap: 9px; font-size: 12.5px; color: #334155; font-weight: 600; cursor: pointer; }
.perm-row input { width: 16px; height: 16px; accent-color: #6366f1; cursor: pointer; flex-shrink: 0; }
.perm-row input:disabled { cursor: not-allowed; }
.perm-row input:disabled ~ span { color: #94a3b8; }
.perm-note { font-size: 11.5px; color: #94a3b8; line-height: 1.5; background: #f8fafc; border-radius: 9px; padding: 9px 11px; }const roleSelect = document.getElementById('roleSelect');
const permList = document.getElementById('permList');
const permNote = document.getElementById('permNote');
const checkboxes = Array.from(permList.querySelectorAll('input[data-perm]'));
const rolePresets = {
viewer: { perms: ['view'], note: "Viewers can only view content — they can't comment, edit, or manage the team." },
editor: { perms: ['view', 'comment', 'edit'], note: "Editors can edit and comment, but can't publish or manage the team." },
admin: { perms: ['view', 'comment', 'edit', 'publish', 'invite', 'billing'], note: 'Admins have full access, including publishing, inviting members, and billing.' },
custom: { perms: null, note: 'Custom role — check exactly the permissions this person should have.' },
};
function applyPreset(role) {
const preset = rolePresets[role];
permNote.textContent = preset.note;
checkboxes.forEach((cb) => {
const perm = cb.dataset.perm;
if (perm === 'view') return; // always on, never editable
if (role === 'custom') {
cb.disabled = false;
} else {
cb.checked = preset.perms.includes(perm);
cb.disabled = true;
}
});
}
roleSelect.addEventListener('change', () => applyPreset(roleSelect.value));
// If someone manually unchecks/checks a box while on a preset role... they can't,
// since non-custom roles disable the checkboxes. But if a user starts on Custom
// and manually builds a permission set that happens to exactly match a preset,
// we leave the role as Custom rather than guessing — presets are only applied
// top-down (role -> checkboxes), never inferred bottom-up (checkboxes -> role).
applyPreset(roleSelect.value);Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), 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
User Role Permission Card — Presets That Actually Drive Checkbox State

Role-based access control UIs often show a role dropdown next to a permissions checklist that don't actually interact — picking "Admin" doesn't visibly change what's checked below it, leaving the user to manually reconcile the two. This card fixes that: selecting a role actively sets and locks the matching permission checkboxes, and only switching to "Custom" hands manual control back to the user.
Role presets are data, not scattered if/else logic
rolePresets is a single object mapping each role name to its exact permission list and a plain-language explanation — { perms: ['view', 'comment', 'edit'], note: '...' }. applyPreset(role) reads from this one source of truth rather than branching through role-specific conditionals inline, so adding a new role or adjusting what "Editor" includes means editing one object entry, not hunting through scattered logic.
Checkboxes are genuinely locked under a preset role, not just pre-checked
For any role other than Custom, every checkbox (besides the always-on "View content") gets both its checked state *and* its disabled attribute set together — cb.checked = preset.perms.includes(perm); cb.disabled = true;. The disabled state matters as much as the checked state: without it, a user could manually toggle a box while "Editor" is still selected in the dropdown, creating a permission set that silently no longer matches what the role name claims to grant. Locking the checkboxes is what keeps the displayed role and the actual granted permissions honest with each other.
Custom mode is the deliberate escape hatch, and only Custom
Selecting "Custom" removes the disabled attribute from every checkbox, handing control back to the user to build an arbitrary permission set. Critically, the reverse direction is intentionally *not* implemented — if a user manually builds a custom permission set that happens to exactly match, say, the Editor preset, the role dropdown is deliberately left on "Custom" rather than the code trying to guess and silently switch it back to "Editor." Inferring a role from a checkbox state is a much easier place to get subtly wrong than applying a role's known preset downward, so this snippet only ever goes one direction.
The always-on "View content" permission
The view checkbox is permanently checked and disabled regardless of role, modeling the realistic constraint that a role can't actually exist in most systems without at least view access — the checkbox reflects that baseline explicitly rather than letting a user (even in Custom mode) construct a role that can, say, comment but not view.
Step by step
How to Use
- 1Change the role dropdownWatch the permission checkboxes and note text update together, with checkboxes locked for every role except Custom.
- 2Select Custom to edit permissions manuallyEvery checkbox except "View content" becomes editable, letting you build an arbitrary permission set.
- 3Edit the rolePresets object to change what each role grantsUpdate the perms array and note string for any role in the JS panel — the UI updates automatically from that single source.
- 4Add a new roleAdd a new <option> to the select and a matching entry in rolePresets with its own perms array and note.
- 5Add or remove permission typesAdd a new .perm-row checkbox with a unique data-perm value, and include that value in whichever roles' perms arrays should grant it.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Disabling them prevents a user from manually toggling a permission while a named role is still shown as selected, which would create a mismatch between what the role name implies and what's actually granted. Locking the checkboxes under a preset keeps the two honestly in sync.
The role dropdown stays on "Custom" — the code deliberately never tries to infer a role name from a checkbox combination and switch the dropdown automatically, since that kind of reverse inference is much easier to get subtly wrong than the one-directional role-to-checkboxes flow this snippet implements.
No — it's permanently checked and disabled regardless of which role is selected, including Custom, modeling the common real-world constraint that a role can't meaningfully exist without at least baseline view access.
Add a new <option value="contributor"> to the role <select>, and add a matching "contributor" entry to the rolePresets object in JavaScript with its own perms array and explanatory note string — the rest of the logic picks it up automatically.
Yes — at any point you can read the current permission set with checkboxes.filter(cb => cb.checked).map(cb => cb.dataset.perm), which works identically whether the state came from a preset role or manual Custom selection.
No — switching away from Custom to a named role immediately overwrites every checkbox's state (except View) with that role's exact preset, discarding whatever custom combination was previously set. Switching back to Custom afterward starts from that role's preset as the new baseline, not the earlier custom set.