Empty States — No-access / permission-required state
A polished permission-required empty state for pages a member can't open yet. It pairs a lock illustration with a clear headline, a plain-language explanation, and a requester-to-owner approval chain showing who must approve. A primary Request access button opens a confirm dialog and fires a Request sent toast, while a secondary Switch account action lets people retry under a different identity. A segmented switcher flips live between no-access, upgrade-required, and request-pending so you can preview every state.
MCP
الكود
:root {
--brand: #5b5bf0;
--brand-d: #4646d6;
--brand-700: #3a3ab8;
--brand-50: #eef0ff;
--accent: #00b4a6;
--accent-soft: #d8f5f2;
--ink: #101322;
--ink-2: #3a4060;
--muted: #6c7393;
--bg: #f6f7fb;
--white: #ffffff;
--surface: #ffffff;
--line: rgba(16, 19, 34, 0.1);
--line-2: rgba(16, 19, 34, 0.16);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-2: 0 8px 24px rgba(16, 19, 34, 0.08);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.page {
max-width: 620px;
margin: 0 auto;
padding: 28px 20px 48px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* Topbar */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: var(--r-sm);
background: var(--brand-50);
color: var(--brand);
}
.brand-name { font-weight: 700; font-size: 0.95rem; letter-spacing: -0.01em; }
.topbar-account {
display: flex;
align-items: center;
gap: 9px;
padding: 5px 12px 5px 6px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--sh-1);
}
.topbar-name { font-size: 0.85rem; font-weight: 600; color: var(--ink-2); }
/* Avatars */
.avatar {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
background: var(--a, var(--brand));
color: #fff;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.02em;
flex: 0 0 auto;
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.35);
}
.avatar-sm { width: 26px; height: 26px; font-size: 0.66rem; }
/* Card */
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
padding: 10px;
overflow: hidden;
}
/* Segmented variant switch */
.variant-switch {
display: flex;
gap: 4px;
padding: 4px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.seg {
flex: 1;
appearance: none;
border: 0;
cursor: pointer;
font: inherit;
font-size: 0.82rem;
font-weight: 600;
color: var(--muted);
padding: 9px 8px;
border-radius: 10px;
background: transparent;
transition: background 0.18s, color 0.18s, box-shadow 0.18s;
}
.seg:hover { color: var(--ink-2); }
.seg[aria-selected="true"] {
background: var(--white);
color: var(--brand-700);
box-shadow: var(--sh-1);
}
.seg:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
/* State body */
.state {
text-align: center;
padding: 30px 28px 26px;
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
animation: fade 0.28s ease;
}
@keyframes fade {
from { opacity: 0; transform: translateY(6px); }
to { opacity: 1; transform: none; }
}
.illustration {
width: 108px;
height: 108px;
display: grid;
place-items: center;
border-radius: 50%;
margin-bottom: 2px;
position: relative;
}
.illustration::after {
content: "";
position: absolute;
inset: -10px;
border-radius: 50%;
border: 1px dashed var(--line-2);
opacity: 0.7;
}
.illustration svg { width: 52px; height: 52px; }
.illustration[data-tone="no-access"] { background: var(--brand-50); color: var(--brand); }
.illustration[data-tone="upgrade"] { background: #fff3e2; color: var(--warn); }
.illustration[data-tone="pending"] { background: var(--accent-soft); color: var(--accent); }
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
padding: 4px 11px;
border-radius: 999px;
background: var(--brand-50);
color: var(--brand-700);
}
.badge[data-tone="upgrade"] { background: #fff3e2; color: #9a6011; }
.badge[data-tone="pending"] { background: var(--accent-soft); color: #0a7a72; }
.state-title {
margin: 0;
font-size: 1.4rem;
font-weight: 800;
letter-spacing: -0.02em;
color: var(--ink);
}
.state-desc {
margin: 0;
max-width: 42ch;
color: var(--muted);
font-size: 0.94rem;
}
.state-desc strong { color: var(--ink-2); font-weight: 600; }
/* Approver chain */
.approver {
display: flex;
align-items: center;
gap: 16px;
margin: 6px 0 2px;
padding: 12px 16px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
}
.appr-side { display: flex; align-items: center; gap: 10px; }
.appr-meta { display: flex; flex-direction: column; text-align: left; }
.appr-name { font-size: 0.85rem; font-weight: 700; color: var(--ink); line-height: 1.25; }
.appr-role { font-size: 0.72rem; color: var(--muted); }
.appr-arrow { color: var(--line-2); display: grid; place-items: center; }
/* Actions */
.actions {
display: flex;
gap: 10px;
margin-top: 6px;
flex-wrap: wrap;
justify-content: center;
}
.btn {
appearance: none;
font: inherit;
font-weight: 600;
font-size: 0.9rem;
cursor: pointer;
border-radius: var(--r-sm);
padding: 11px 20px;
border: 1px solid transparent;
transition: transform 0.12s, box-shadow 0.18s, background 0.18s, color 0.18s, border-color 0.18s;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.btn:disabled { opacity: 0.55; cursor: not-allowed; }
.btn-primary {
background: var(--brand);
color: #fff;
box-shadow: var(--sh-1);
}
.btn-primary:hover:not(:disabled) { background: var(--brand-d); }
.btn-primary:active:not(:disabled) { background: var(--brand-700); }
.btn-ghost {
background: var(--white);
color: var(--ink-2);
border-color: var(--line-2);
}
.btn-ghost:hover { background: var(--bg); }
.hint { margin: 4px 0 0; font-size: 0.78rem; color: var(--muted); }
.foot { text-align: center; font-size: 0.76rem; color: var(--muted); margin: 0; }
/* Overlay + modal */
.overlay {
position: fixed;
inset: 0;
background: rgba(16, 19, 34, 0.46);
display: grid;
place-items: center;
padding: 20px;
z-index: 50;
animation: ovfade 0.18s ease;
}
@keyframes ovfade { from { opacity: 0; } to { opacity: 1; } }
.overlay[hidden] { display: none; }
.modal {
background: var(--white);
border-radius: var(--r-lg);
box-shadow: var(--sh-2);
width: 100%;
max-width: 400px;
padding: 26px 24px 22px;
text-align: center;
animation: pop 0.2s cubic-bezier(0.2, 0.9, 0.3, 1.2);
}
@keyframes pop {
from { opacity: 0; transform: scale(0.94) translateY(8px); }
to { opacity: 1; transform: none; }
}
.dlg-icon {
display: inline-grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: 50%;
background: var(--brand-50);
color: var(--brand);
margin-bottom: 12px;
}
.dlg-title { margin: 0 0 8px; font-size: 1.18rem; font-weight: 800; letter-spacing: -0.01em; }
.dlg-desc { margin: 0 0 20px; font-size: 0.9rem; color: var(--muted); }
.dlg-desc strong { color: var(--ink-2); font-weight: 600; }
.dlg-actions { display: flex; gap: 10px; justify-content: center; }
.dlg-actions .btn { flex: 1; }
/* Account list */
.acct-list {
list-style: none;
margin: 0 0 18px;
padding: 0;
display: flex;
flex-direction: column;
gap: 6px;
text-align: left;
}
.acct-row {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-md);
cursor: pointer;
font: inherit;
transition: border-color 0.16s, background 0.16s;
}
.acct-row:hover { background: var(--bg); border-color: var(--line-2); }
.acct-row:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.acct-meta { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.acct-name { font-size: 0.88rem; font-weight: 600; color: var(--ink); }
.acct-mail { font-size: 0.76rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.acct-check { color: var(--brand); font-weight: 800; opacity: 0; transition: opacity 0.16s; }
.acct-row[aria-selected="true"] { border-color: var(--brand); background: var(--brand-50); }
.acct-row[aria-selected="true"] .acct-check { opacity: 1; }
/* Toast */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 80;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 9px;
background: var(--ink);
color: #fff;
font-size: 0.86rem;
font-weight: 500;
padding: 11px 16px;
border-radius: var(--r-md);
box-shadow: var(--sh-2);
animation: toastIn 0.24s ease;
}
.toast.out { animation: toastOut 0.24s ease forwards; }
.toast .t-ico { display: grid; place-items: center; color: var(--accent); }
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes toastOut { to { opacity: 0; transform: translateY(8px); } }
@media (max-width: 520px) {
.page { padding: 18px 14px 36px; }
.topbar-name { display: none; }
.state { padding: 24px 16px 22px; }
.state-title { font-size: 1.18rem; }
.approver { flex-direction: column; gap: 10px; width: 100%; }
.appr-arrow { transform: rotate(90deg); }
.actions { flex-direction: column; width: 100%; }
.actions .btn { width: 100%; }
.seg { font-size: 0.76rem; padding: 9px 4px; }
}(function () {
"use strict";
var ICONS = {
lock:
'<svg viewBox="0 0 24 24" fill="none"><rect x="4.5" y="10.5" width="15" height="10" rx="2.5" fill="currentColor" opacity="0.16"/><rect x="4.5" y="10.5" width="15" height="10" rx="2.5" stroke="currentColor" stroke-width="1.8"/><path d="M8 10.5V8a4 4 0 018 0v2.5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><circle cx="12" cy="15.5" r="1.6" fill="currentColor"/></svg>',
upgrade:
'<svg viewBox="0 0 24 24" fill="none"><path d="M12 3l2.3 5.6L20 9.1l-4.4 3.8L17 19l-5-3.3L7 19l1.4-6.1L4 9.1l5.7-.5L12 3z" fill="currentColor" opacity="0.18"/><path d="M12 3l2.3 5.6L20 9.1l-4.4 3.8L17 19l-5-3.3L7 19l1.4-6.1L4 9.1l5.7-.5L12 3z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>',
pending:
'<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" fill="currentColor" opacity="0.14"/><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.8"/><path d="M12 7.5V12l3 2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>'
};
var VARIANTS = {
"no-access": {
tone: "no-access",
icon: "lock",
badge: "Restricted",
title: "You don’t have access to this page",
desc:
"The <strong>Q3 Revenue Analytics</strong> dashboard is restricted to members of the Finance team. Request access and the workspace owner will be notified to review it.",
primary: "Request access",
primaryDisabled: false,
hint: "Typical response time is under 2 hours during business days.",
showApprover: true,
action: "request"
},
upgrade: {
tone: "upgrade",
icon: "upgrade",
badge: "Pro feature",
title: "Upgrade to unlock this page",
desc:
"<strong>Revenue forecasting</strong> is part of the Northwind <strong>Business</strong> plan. Ask <strong>Devon Khoury</strong> to upgrade your workspace, or request it below.",
primary: "Request upgrade",
primaryDisabled: false,
hint: "Billing is managed by the workspace owner.",
showApprover: true,
action: "request"
},
pending: {
tone: "pending",
icon: "pending",
badge: "Awaiting review",
title: "Your access request is pending",
desc:
"You requested access to <strong>Q3 Revenue Analytics</strong> on Jun 9. <strong>Devon Khoury</strong> hasn’t responded yet — we’ll email you the moment they do.",
primary: "Request sent",
primaryDisabled: true,
hint: "Requested 12 minutes ago · You can switch accounts meanwhile.",
showApprover: true,
action: "request"
}
};
var stateEl = document.getElementById("state");
var illu = document.getElementById("illu");
var badge = document.getElementById("badge");
var titleEl = document.getElementById("state-title");
var descEl = document.getElementById("state-desc");
var approver = document.getElementById("approver");
var primaryBtn = document.getElementById("primaryBtn");
var switchBtn = document.getElementById("switchBtn");
var hint = document.getElementById("hint");
var segs = Array.prototype.slice.call(document.querySelectorAll(".seg"));
var overlay = document.getElementById("overlay");
var modal = document.getElementById("modal");
var cancelBtn = document.getElementById("cancelBtn");
var confirmBtn = document.getElementById("confirmBtn");
var acctOverlay = document.getElementById("acctOverlay");
var acctModal = document.getElementById("acctModal");
var acctCancel = document.getElementById("acctCancel");
var acctRows = Array.prototype.slice.call(document.querySelectorAll(".acct-row"));
var toastWrap = document.getElementById("toastWrap");
var current = "no-access";
var lastFocused = null;
/* ---------- toast ---------- */
function toast(msg) {
var t = document.createElement("div");
t.className = "toast";
t.setAttribute("role", "status");
t.innerHTML =
'<span class="t-ico" aria-hidden="true"><svg viewBox="0 0 24 24" width="18" height="18" fill="none"><path d="M5 12.5l4 4 10-10" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>' +
'<span></span>';
t.lastChild.textContent = msg;
toastWrap.appendChild(t);
setTimeout(function () {
t.classList.add("out");
setTimeout(function () {
if (t.parentNode) t.parentNode.removeChild(t);
}, 240);
}, 2600);
}
/* ---------- render variant ---------- */
function render(key) {
var v = VARIANTS[key];
if (!v) return;
current = key;
illu.dataset.tone = v.tone;
illu.innerHTML = ICONS[v.icon];
badge.dataset.tone = v.tone;
badge.textContent = v.badge;
titleEl.textContent = v.title;
descEl.innerHTML = v.desc;
hint.textContent = v.hint;
approver.style.display = v.showApprover ? "" : "none";
primaryBtn.textContent = v.primary;
primaryBtn.disabled = !!v.primaryDisabled;
// re-trigger entrance animation
stateEl.style.animation = "none";
void stateEl.offsetWidth;
stateEl.style.animation = "";
segs.forEach(function (s) {
var on = s.dataset.variant === key;
s.setAttribute("aria-selected", on ? "true" : "false");
});
}
segs.forEach(function (s, i) {
s.addEventListener("click", function () {
render(s.dataset.variant);
});
s.addEventListener("keydown", function (e) {
if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
e.preventDefault();
var dir = e.key === "ArrowRight" ? 1 : -1;
var next = (i + dir + segs.length) % segs.length;
segs[next].focus();
render(segs[next].dataset.variant);
}
});
});
/* ---------- overlay helpers ---------- */
function openOverlay(ov, firstFocus) {
lastFocused = document.activeElement;
ov.hidden = false;
(firstFocus || ov.querySelector("button")).focus();
}
function closeOverlay(ov) {
ov.hidden = true;
if (lastFocused && lastFocused.focus) lastFocused.focus();
}
function trapTab(e, container) {
if (e.key !== "Tab") return;
var f = container.querySelectorAll("button, [href], input, [tabindex]:not([tabindex='-1'])");
if (!f.length) return;
var first = f[0];
var last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
/* ---------- request access flow ---------- */
primaryBtn.addEventListener("click", function () {
if (primaryBtn.disabled) return;
if (current === "upgrade") {
// tailor the modal copy lightly
modal.querySelector("#dlg-title").textContent = "Request upgrade?";
modal.querySelector("#dlg-desc").innerHTML =
"We’ll ask <strong>Devon Khoury</strong> to upgrade the workspace to the <strong>Business</strong> plan so you can use revenue forecasting.";
confirmBtn.textContent = "Send request";
} else {
modal.querySelector("#dlg-title").textContent = "Request access?";
modal.querySelector("#dlg-desc").innerHTML =
"We’ll send <strong>Devon Khoury</strong> a request to grant you access to <strong>Q3 Revenue Analytics</strong>. You’ll be notified when they respond.";
confirmBtn.textContent = "Send request";
}
openOverlay(overlay, confirmBtn);
});
cancelBtn.addEventListener("click", function () {
closeOverlay(overlay);
});
confirmBtn.addEventListener("click", function () {
closeOverlay(overlay);
toast("Request sent");
// move into the pending state for a coherent flow
setTimeout(function () {
render("pending");
}, 250);
});
overlay.addEventListener("mousedown", function (e) {
if (e.target === overlay) closeOverlay(overlay);
});
overlay.addEventListener("keydown", function (e) {
trapTab(e, modal);
});
/* ---------- switch account flow ---------- */
switchBtn.addEventListener("click", function () {
openOverlay(acctOverlay, acctOverlay.querySelector(".acct-row[aria-selected='true']") || acctOverlay.querySelector("button"));
});
acctCancel.addEventListener("click", function () {
closeOverlay(acctOverlay);
});
acctOverlay.addEventListener("mousedown", function (e) {
if (e.target === acctOverlay) closeOverlay(acctOverlay);
});
acctOverlay.addEventListener("keydown", function (e) {
trapTab(e, acctModal);
});
acctRows.forEach(function (row) {
row.addEventListener("click", function () {
acctRows.forEach(function (r) {
r.setAttribute("aria-selected", "false");
});
row.setAttribute("aria-selected", "true");
// update topbar identity
var name = row.dataset.name;
var initials = row.dataset.initials;
var color = row.dataset.color;
var tbAvatar = document.querySelector(".topbar-account .avatar");
var tbName = document.querySelector(".topbar-name");
if (tbAvatar) {
tbAvatar.textContent = initials;
tbAvatar.style.setProperty("--a", color);
}
if (tbName) tbName.textContent = name;
// update requester in the approver chain
var reqAvatar = approver.querySelector(".appr-side .avatar");
var reqName = approver.querySelector(".appr-side .appr-name");
if (reqAvatar) {
reqAvatar.textContent = initials;
reqAvatar.style.setProperty("--a", color);
}
if (reqName) reqName.textContent = name;
closeOverlay(acctOverlay);
toast("Switched to " + name);
});
});
/* ---------- Esc closes overlays ---------- */
document.addEventListener("keydown", function (e) {
if (e.key !== "Escape") return;
if (!overlay.hidden) closeOverlay(overlay);
else if (!acctOverlay.hidden) closeOverlay(acctOverlay);
});
render("no-access");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Permission-required state</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<header class="topbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none"><path d="M4 7l8-4 8 4v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7z" fill="currentColor"/></svg>
</span>
<span class="brand-name">Northwind Workspace</span>
</div>
<div class="topbar-account" role="group" aria-label="Current account">
<span class="avatar avatar-sm" aria-hidden="true" style="--a:#5b5bf0">MR</span>
<span class="topbar-name">Maya Reyes</span>
</div>
</header>
<section class="card" aria-labelledby="state-title">
<div class="variant-switch" role="tablist" aria-label="Choose state">
<button class="seg" role="tab" aria-selected="true" data-variant="no-access">No access</button>
<button class="seg" role="tab" aria-selected="false" data-variant="upgrade">Upgrade required</button>
<button class="seg" role="tab" aria-selected="false" data-variant="pending">Request pending</button>
</div>
<div class="state" id="state" data-variant="no-access">
<div class="illustration" id="illu" aria-hidden="true">
<!-- swapped by JS -->
</div>
<span class="badge" id="badge">Restricted</span>
<h1 class="state-title" id="state-title">You don’t have access to this page</h1>
<p class="state-desc" id="state-desc">
The <strong>Q3 Revenue Analytics</strong> dashboard is restricted to members of the
Finance team. Request access and the workspace owner will be notified to review it.
</p>
<div class="approver" id="approver">
<div class="appr-side">
<span class="avatar" style="--a:#5b5bf0" aria-hidden="true">MR</span>
<div class="appr-meta">
<span class="appr-name">Maya Reyes</span>
<span class="appr-role">You · Requester</span>
</div>
</div>
<div class="appr-arrow" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none"><path d="M5 12h14m-5-5l5 5-5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div class="appr-side">
<span class="avatar" style="--a:#00b4a6" aria-hidden="true">DK</span>
<div class="appr-meta">
<span class="appr-name">Devon Khoury</span>
<span class="appr-role">Owner · Approves</span>
</div>
</div>
</div>
<div class="actions">
<button class="btn btn-primary" id="primaryBtn" type="button">Request access</button>
<button class="btn btn-ghost" id="switchBtn" type="button">Switch account</button>
</div>
<p class="hint" id="hint">Typical response time is under 2 hours during business days.</p>
</div>
</section>
<p class="foot">Northwind Workspace · A fictional product demo</p>
</main>
<!-- Request access confirm dialog -->
<div class="overlay" id="overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="dlg-title" aria-describedby="dlg-desc" id="modal">
<span class="dlg-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none"><path d="M12 3l1.9 5.7H20l-4.9 3.6 1.9 5.7L12 14.4 7 18l1.9-5.7L4 8.7h6.1L12 3z" fill="currentColor"/></svg>
</span>
<h2 class="dlg-title" id="dlg-title">Request access?</h2>
<p class="dlg-desc" id="dlg-desc">
We’ll send <strong>Devon Khoury</strong> a request to grant you access to
<strong>Q3 Revenue Analytics</strong>. You’ll be notified when they respond.
</p>
<div class="dlg-actions">
<button class="btn btn-ghost" id="cancelBtn" type="button">Cancel</button>
<button class="btn btn-primary" id="confirmBtn" type="button">Send request</button>
</div>
</div>
</div>
<!-- Switch account dialog -->
<div class="overlay" id="acctOverlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="acct-title" id="acctModal">
<h2 class="dlg-title" id="acct-title">Switch account</h2>
<p class="dlg-desc">Choose an account to continue with.</p>
<ul class="acct-list" role="listbox" aria-label="Accounts">
<li><button class="acct-row" role="option" data-name="Maya Reyes" data-initials="MR" data-color="#5b5bf0" aria-selected="true">
<span class="avatar" style="--a:#5b5bf0" aria-hidden="true">MR</span>
<span class="acct-meta"><span class="acct-name">Maya Reyes</span><span class="acct-mail">[email protected]</span></span>
<span class="acct-check" aria-hidden="true">✓</span>
</button></li>
<li><button class="acct-row" role="option" data-name="Maya (Personal)" data-initials="MP" data-color="#00b4a6" aria-selected="false">
<span class="avatar" style="--a:#00b4a6" aria-hidden="true">MP</span>
<span class="acct-meta"><span class="acct-name">Maya (Personal)</span><span class="acct-mail">[email protected]</span></span>
<span class="acct-check" aria-hidden="true">✓</span>
</button></li>
<li><button class="acct-row" role="option" data-name="Reyes Consulting" data-initials="RC" data-color="#d98a2b" aria-selected="false">
<span class="avatar" style="--a:#d98a2b" aria-hidden="true">RC</span>
<span class="acct-meta"><span class="acct-name">Reyes Consulting</span><span class="acct-mail">[email protected]</span></span>
<span class="acct-check" aria-hidden="true">✓</span>
</button></li>
</ul>
<div class="dlg-actions">
<button class="btn btn-ghost" id="acctCancel" type="button">Close</button>
</div>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>No-access / permission-required state
A focused screen for the moment a member lands on a page they can’t open. A soft lock illustration, a direct headline (“You don’t have access to this page”), and a short explanation set expectations, while an approval chain shows the requester’s avatar pointing to the workspace owner who has to approve. A primary Request access button and a secondary Switch account action give the user two clear ways forward.
The demo ships three variants behind a segmented switcher: no-access (request access from the owner), upgrade-required (a Pro/Business feature gated by plan), and request-pending (the request is in flight, primary button disabled). Selecting a variant re-renders the illustration, badge tone, copy, and hint with a subtle entrance animation, and the arrow keys move between tabs.
Interactions are vanilla JS. Request access opens an aria-modal confirm dialog; confirming closes it, shows a Request sent toast, and transitions the page into the pending state for a coherent flow. Switch account opens an account picker that updates both the top-bar identity and the requester in the approval chain. Both overlays trap focus, close on Esc or backdrop click, and restore focus to their trigger. The whole thing is responsive down to ~360px, with AA-contrast colors and focus-visible rings throughout.