Paywall — Inline feature-locked gate (pro badge)
An inline feature-gate pattern for a fictional Northwind workspace, where premium controls in a settings panel and editor toolbar carry a PRO pill and a small lock icon. Clicking a locked row or tool opens a positioned popover that explains it is a Pro feature and offers an upgrade. A demo upgrade flips the plan badge, unlocks every gated row with a live toggle, clears the toolbar locks, and updates the side-by-side plan cards with an annual-billing price toggle.
MCP
Code
: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);
--sh-3: 0 16px 48px rgba(16, 19, 34, 0.16);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 32px 20px 56px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
button {
font-family: inherit;
}
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: var(--r-sm);
}
code {
font-family: ui-monospace, "SF Mono", Menlo, monospace;
font-size: 0.85em;
background: var(--brand-50);
color: var(--brand-700);
padding: 1px 5px;
border-radius: 5px;
}
/* ===== Shell ===== */
.shell {
max-width: 980px;
margin: 0 auto;
}
/* ===== Topbar ===== */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 14px 18px;
box-shadow: var(--sh-1);
margin-bottom: 20px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand__mark {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: var(--r-md);
background: linear-gradient(150deg, var(--brand), var(--brand-700));
color: #fff;
box-shadow: var(--sh-1);
}
.brand__text {
display: flex;
flex-direction: column;
line-height: 1.25;
}
.brand__text strong {
font-size: 15px;
font-weight: 800;
letter-spacing: -0.01em;
}
.brand__sub {
font-size: 12px;
color: var(--muted);
}
.topbar__right {
display: flex;
align-items: center;
gap: 12px;
}
.plan-badge {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
padding: 6px 11px;
border-radius: 999px;
transition: background 0.25s, color 0.25s, border-color 0.25s;
}
.plan-badge__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
transition: background 0.25s;
}
.plan-badge[data-plan="pro"] {
color: var(--brand-700);
background: var(--brand-50);
border-color: rgba(91, 91, 240, 0.25);
}
.plan-badge[data-plan="pro"] .plan-badge__dot {
background: var(--brand);
box-shadow: 0 0 0 3px rgba(91, 91, 240, 0.18);
}
/* ===== Buttons ===== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
font-size: 13.5px;
font-weight: 600;
border-radius: var(--r-sm);
border: 1px solid transparent;
padding: 9px 15px;
cursor: pointer;
transition: transform 0.12s ease, background 0.18s, box-shadow 0.18s,
border-color 0.18s;
}
.btn:active {
transform: translateY(1px);
}
.btn--sm {
padding: 7px 12px;
font-size: 12.5px;
}
.btn--primary {
background: var(--brand);
color: #fff;
box-shadow: var(--sh-1);
}
.btn--primary:hover {
background: var(--brand-d);
box-shadow: var(--sh-2);
}
.btn--ghost {
background: var(--surface);
color: var(--ink-2);
border-color: var(--line-2);
}
.btn--ghost:hover {
background: var(--bg);
border-color: var(--ink-2);
}
.link {
background: none;
border: 0;
padding: 0;
font: inherit;
font-weight: 600;
color: var(--brand);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
.link:hover {
color: var(--brand-d);
}
/* ===== Layout ===== */
.layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 20px;
align-items: start;
}
/* ===== Panel ===== */
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 22px;
box-shadow: var(--sh-2);
}
.panel__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
margin-bottom: 18px;
}
.panel h1 {
margin: 0 0 4px;
font-size: 19px;
font-weight: 800;
letter-spacing: -0.02em;
}
.panel__lede {
margin: 0;
font-size: 13.5px;
color: var(--muted);
max-width: 46ch;
}
.upsell-chip {
flex-shrink: 0;
font-size: 12px;
font-weight: 600;
color: var(--warn);
background: rgba(217, 138, 43, 0.12);
border: 1px solid rgba(217, 138, 43, 0.28);
padding: 5px 10px;
border-radius: 999px;
white-space: nowrap;
}
/* ===== Toolbar ===== */
.toolbar {
display: flex;
align-items: center;
gap: 6px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 7px;
margin-bottom: 20px;
}
.tool {
position: relative;
display: grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: var(--r-sm);
border: 1px solid transparent;
background: transparent;
color: var(--ink-2);
cursor: pointer;
transition: background 0.16s, color 0.16s, box-shadow 0.16s;
}
.tool:hover {
background: var(--white);
color: var(--ink);
box-shadow: var(--sh-1);
}
.toolbar__sep {
width: 1px;
height: 22px;
background: var(--line-2);
margin: 0 3px;
}
.tool--locked {
color: var(--muted);
cursor: pointer;
}
.tool--locked:hover {
background: var(--brand-50);
color: var(--brand-700);
box-shadow: none;
}
.tool__lock {
position: absolute;
right: -3px;
bottom: -3px;
display: grid;
place-items: center;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--brand);
color: #fff;
border: 2px solid var(--bg);
}
/* ===== Rows ===== */
.rows {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 14px 16px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--surface);
transition: border-color 0.2s, background 0.2s, box-shadow 0.2s;
}
.row:hover {
border-color: var(--line-2);
}
.row__main {
display: flex;
align-items: center;
gap: 13px;
min-width: 0;
}
.row__icon {
display: grid;
place-items: center;
width: 34px;
height: 34px;
flex-shrink: 0;
border-radius: var(--r-sm);
background: var(--brand-50);
color: var(--brand-700);
}
.row__title {
margin: 0;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.row__desc {
margin: 2px 0 0;
font-size: 12.5px;
color: var(--muted);
}
/* locked row visuals */
.row--locked {
background: linear-gradient(
180deg,
rgba(246, 247, 251, 0.4),
var(--surface)
);
}
.row--locked .row__icon {
background: var(--bg);
color: var(--muted);
}
.row--locked .row__title {
color: var(--ink-2);
}
/* unlocked state (after upgrade) */
.row--unlocked {
border-color: rgba(47, 158, 111, 0.4);
background: linear-gradient(180deg, rgba(47, 158, 111, 0.06), var(--surface));
animation: pop 0.45s cubic-bezier(0.2, 0.9, 0.3, 1.4);
}
.row--unlocked .row__icon {
background: rgba(47, 158, 111, 0.12);
color: var(--ok);
}
.row--unlocked .row__title {
color: var(--ink);
}
@keyframes pop {
0% {
transform: scale(0.985);
}
60% {
transform: scale(1.012);
}
100% {
transform: scale(1);
}
}
/* PRO pill + lock */
.pro-pill {
font-size: 9.5px;
font-weight: 800;
letter-spacing: 0.07em;
color: #fff;
background: linear-gradient(135deg, var(--brand), var(--brand-700));
padding: 2px 7px;
border-radius: 999px;
line-height: 1;
box-shadow: 0 1px 3px rgba(91, 91, 240, 0.4);
}
.row__cta {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
font-weight: 600;
color: var(--brand-700);
background: var(--brand-50);
border: 1px solid rgba(91, 91, 240, 0.22);
border-radius: var(--r-sm);
padding: 7px 12px;
cursor: pointer;
transition: background 0.16s, border-color 0.16s, transform 0.12s;
}
.row__cta:hover {
background: #e3e6ff;
border-color: rgba(91, 91, 240, 0.4);
}
.row__cta:active {
transform: translateY(1px);
}
.row__lock {
display: grid;
place-items: center;
color: var(--brand);
}
.switch__success {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12.5px;
font-weight: 600;
color: var(--ok);
}
.panel__foot {
margin: 20px 0 0;
font-size: 13px;
color: var(--muted);
}
/* ===== Switch ===== */
.switch {
position: relative;
display: inline-flex;
flex-shrink: 0;
cursor: pointer;
}
.switch input {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
margin: 0;
cursor: pointer;
}
.switch__track {
width: 40px;
height: 23px;
border-radius: 999px;
background: var(--line-2);
position: relative;
transition: background 0.22s;
}
.switch__track::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 19px;
height: 19px;
border-radius: 50%;
background: #fff;
box-shadow: var(--sh-1);
transition: transform 0.22s cubic-bezier(0.2, 0.8, 0.3, 1.2);
}
.switch input:checked + .switch__track {
background: var(--brand);
}
.switch input:checked + .switch__track::after {
transform: translateX(17px);
}
.switch input:focus-visible + .switch__track {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
/* ===== Aside / plan cards ===== */
.aside {
display: flex;
flex-direction: column;
gap: 16px;
position: sticky;
top: 24px;
}
.plan-card {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px;
box-shadow: var(--sh-1);
}
.plan-card--pro {
border: 1.5px solid var(--brand);
box-shadow: var(--sh-2);
background: linear-gradient(180deg, var(--brand-50), var(--surface) 38%);
}
.plan-card__ribbon {
position: absolute;
top: -11px;
left: 50%;
transform: translateX(-50%);
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #fff;
background: linear-gradient(135deg, var(--brand), var(--brand-700));
padding: 4px 12px;
border-radius: 999px;
white-space: nowrap;
box-shadow: var(--sh-1);
}
.plan-card__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.plan-card__head h2 {
margin: 0;
font-size: 16px;
font-weight: 800;
}
.plan-card__price {
display: inline-flex;
align-items: baseline;
gap: 2px;
}
.plan-card__price .amt {
font-size: 22px;
font-weight: 800;
letter-spacing: -0.02em;
}
.plan-card__price .per {
font-size: 12px;
color: var(--muted);
font-weight: 500;
}
.plan-card__note {
margin: 6px 0 12px;
font-size: 12.5px;
color: var(--muted);
}
.feat {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.feat li {
position: relative;
padding-left: 24px;
font-size: 13px;
color: var(--ink-2);
}
.feat li::before {
position: absolute;
left: 0;
top: 1px;
width: 16px;
height: 16px;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 10px;
font-weight: 700;
}
.feat__yes::before {
content: "✓";
color: var(--ok);
background: rgba(47, 158, 111, 0.14);
}
.feat__no {
color: var(--muted);
}
.feat__no::before {
content: "×";
color: var(--muted);
background: var(--bg);
}
.billing {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin: 14px 0 12px;
padding: 10px 12px;
border-radius: var(--r-sm);
background: rgba(255, 255, 255, 0.7);
border: 1px solid var(--line);
cursor: pointer;
}
.billing__label {
font-size: 12.5px;
font-weight: 600;
color: var(--ink-2);
}
.plan-card__cta {
width: 100%;
}
/* ===== Popover ===== */
.popover {
position: absolute;
z-index: 50;
width: 252px;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
padding: 16px;
box-shadow: var(--sh-3);
transform-origin: top center;
animation: popIn 0.16s ease;
}
.popover[hidden] {
display: none;
}
@keyframes popIn {
from {
opacity: 0;
transform: translateY(-4px) scale(0.97);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.popover__arrow {
position: absolute;
width: 12px;
height: 12px;
background: var(--surface);
border-left: 1px solid var(--line-2);
border-top: 1px solid var(--line-2);
transform: rotate(45deg);
top: -7px;
left: 24px;
}
.popover.is-below .popover__arrow {
top: auto;
bottom: -7px;
border-left: 0;
border-top: 0;
border-right: 1px solid var(--line-2);
border-bottom: 1px solid var(--line-2);
}
.popover__icon {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: var(--r-sm);
background: var(--brand-50);
color: var(--brand);
margin-bottom: 10px;
}
.popover h3 {
margin: 0 0 4px;
font-size: 14px;
font-weight: 700;
}
.popover__body {
margin: 0 0 14px;
font-size: 12.5px;
color: var(--muted);
}
.popover__actions {
display: flex;
gap: 8px;
}
.popover__actions .btn {
flex: 1;
}
/* ===== Toast ===== */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 14px);
background: var(--ink);
color: #fff;
font-size: 13px;
font-weight: 500;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--sh-3);
z-index: 80;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.toast[hidden] {
display: none;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
.toast__check {
color: var(--accent);
}
/* ===== Responsive ===== */
@media (max-width: 820px) {
.layout {
grid-template-columns: 1fr;
}
.aside {
position: static;
flex-direction: row;
flex-wrap: wrap;
}
.plan-card {
flex: 1 1 240px;
}
}
@media (max-width: 520px) {
body {
padding: 18px 12px 48px;
}
.topbar {
flex-direction: column;
align-items: stretch;
gap: 12px;
}
.topbar__right {
justify-content: space-between;
}
.panel {
padding: 16px;
}
.panel__head {
flex-direction: column;
}
.row {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.row__cta,
.switch {
align-self: stretch;
}
.row__cta {
justify-content: center;
}
.aside {
flex-direction: column;
}
.popover {
width: calc(100vw - 28px);
max-width: 300px;
}
}(function () {
"use strict";
var PRO_MONTHLY = 12;
var PRO_ANNUAL = 10; // 20% off, billed annually
var popover = document.getElementById("popover");
var popTitle = document.getElementById("popTitle");
var popBody = document.getElementById("popBody");
var popUpgrade = document.getElementById("popUpgrade");
var lockedCountEl = document.getElementById("lockedCount");
var planBadge = document.getElementById("planBadge");
var planLabel = document.getElementById("planLabel");
var toastEl = document.getElementById("toast");
var activeTrigger = null;
var toastTimer = null;
/* ---------- Toast ---------- */
function toast(msg) {
if (!toastEl) return;
toastEl.innerHTML =
'<span class="toast__check" aria-hidden="true">✓</span>' + escapeHtml(msg);
toastEl.hidden = false;
// force reflow so the transition runs
void toastEl.offsetWidth;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
setTimeout(function () {
toastEl.hidden = true;
}, 280);
}, 2600);
}
function escapeHtml(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
/* ---------- Popover positioning ---------- */
function openPopover(trigger) {
if (trigger.getAttribute("data-locked") !== "true") return;
var feature =
trigger.getAttribute("data-feature") || "this feature";
popBody.textContent =
"Upgrade to Northwind Pro to unlock “" + feature + ".”";
activeTrigger = trigger;
popover.hidden = false;
popover.classList.remove("is-below");
position(trigger);
trigger.setAttribute("aria-expanded", "true");
// focus first action for keyboard users
requestAnimationFrame(function () {
popUpgrade.focus();
});
document.addEventListener("keydown", onKeydown, true);
document.addEventListener("mousedown", onOutside, true);
window.addEventListener("resize", reposition, true);
window.addEventListener("scroll", reposition, true);
}
function position(trigger) {
var r = trigger.getBoundingClientRect();
var pw = popover.offsetWidth;
var ph = popover.offsetHeight;
var pad = 10;
var sx = window.scrollX;
var sy = window.scrollY;
// horizontal: align left edge with trigger, clamp to viewport
var left = r.left + sx;
var maxLeft = sx + document.documentElement.clientWidth - pw - pad;
if (left > maxLeft) left = maxLeft;
if (left < sx + pad) left = sx + pad;
// vertical: prefer below, flip above if it would overflow
var below = r.bottom + sy + pad;
var spaceBelow = window.innerHeight - r.bottom;
var top;
if (spaceBelow < ph + pad + 8 && r.top > ph + pad) {
top = r.top + sy - ph - pad;
popover.classList.add("is-below");
} else {
top = below;
popover.classList.remove("is-below");
}
popover.style.left = Math.round(left) + "px";
popover.style.top = Math.round(top) + "px";
// point the arrow at the trigger
var arrow = popover.querySelector(".popover__arrow");
if (arrow) {
var ax = r.left + sx - left + Math.min(r.width / 2, 28) - 6;
arrow.style.left = Math.max(12, Math.min(pw - 24, ax)) + "px";
}
}
function reposition() {
if (!popover.hidden && activeTrigger) position(activeTrigger);
}
function closePopover(returnFocus) {
if (popover.hidden) return;
popover.hidden = true;
document.removeEventListener("keydown", onKeydown, true);
document.removeEventListener("mousedown", onOutside, true);
window.removeEventListener("resize", reposition, true);
window.removeEventListener("scroll", reposition, true);
if (activeTrigger) {
activeTrigger.removeAttribute("aria-expanded");
if (returnFocus) activeTrigger.focus();
}
activeTrigger = null;
}
function onKeydown(e) {
if (e.key === "Escape") {
e.preventDefault();
closePopover(true);
}
}
function onOutside(e) {
if (popover.contains(e.target)) return;
if (activeTrigger && activeTrigger.contains(e.target)) return;
closePopover(false);
}
/* ---------- Wire up gated triggers ---------- */
function bindTriggers() {
var triggers = document.querySelectorAll('[data-locked="true"]');
triggers.forEach(function (t) {
t.addEventListener("click", function (e) {
e.preventDefault();
if (t === activeTrigger && !popover.hidden) {
closePopover(true);
} else {
openPopover(t);
}
});
});
}
/* ---------- Upgrade flow ---------- */
function upgrade() {
closePopover(false);
// 1. flip plan badge
planBadge.setAttribute("data-plan", "pro");
planLabel.textContent = "Pro plan";
// 2. unlock each gated ROW
var lockedRows = document.querySelectorAll(".row--locked");
lockedRows.forEach(function (row, i) {
setTimeout(function () {
unlockRow(row);
}, 90 * i);
});
// 3. unlock toolbar tools
document.querySelectorAll(".tool--locked").forEach(function (tool) {
tool.classList.remove("tool--locked");
tool.removeAttribute("data-locked");
tool.removeAttribute("aria-disabled");
var lock = tool.querySelector(".tool__lock");
if (lock) lock.remove();
});
// 4. update the locked-count chip
if (lockedCountEl) {
lockedCountEl.textContent = "All features unlocked";
lockedCountEl.style.color = "var(--ok)";
lockedCountEl.style.background = "rgba(47,158,111,0.12)";
lockedCountEl.style.borderColor = "rgba(47,158,111,0.3)";
}
// 5. swap the main upgrade button
var mainBtn = document.getElementById("upgradeMain");
if (mainBtn) {
mainBtn.textContent = "✓ You’re on Pro";
mainBtn.disabled = true;
mainBtn.classList.remove("btn--primary");
mainBtn.classList.add("btn--ghost");
mainBtn.style.cursor = "default";
}
toast("Welcome to Northwind Pro — everything’s unlocked.");
}
function unlockRow(row) {
row.classList.remove("row--locked");
row.classList.add("row--unlocked");
row.removeAttribute("data-locked");
// remove PRO pill
var pill = row.querySelector(".pro-pill");
if (pill) pill.remove();
// replace the "Locked" CTA with a live toggle (default on)
var cta = row.querySelector(".row__cta");
if (cta) {
var label = document.createElement("label");
label.className = "switch";
label.innerHTML =
'<input type="checkbox" checked>' +
'<span class="switch__track" aria-hidden="true"></span>' +
'<span class="sr-only">Toggle feature</span>';
cta.replaceWith(label);
}
}
/* ---------- Annual billing toggle ---------- */
function bindBilling() {
var annual = document.getElementById("annualToggle");
var price = document.getElementById("proPrice");
var per = document.getElementById("proPer");
if (!annual || !price) return;
annual.addEventListener("change", function () {
var amount = annual.checked ? PRO_ANNUAL : PRO_MONTHLY;
price.textContent = "$" + amount;
per.textContent = annual.checked ? "/seat · mo*" : "/seat · mo";
popUpgrade.textContent = "Upgrade — $" + amount + "/mo";
toast(
annual.checked
? "Annual billing — $" + amount + "/seat, saving 20%."
: "Switched to monthly billing."
);
});
}
/* ---------- Misc actions ---------- */
function bindMisc() {
if (popUpgrade) popUpgrade.addEventListener("click", upgrade);
var mainBtn = document.getElementById("upgradeMain");
if (mainBtn) mainBtn.addEventListener("click", upgrade);
document.querySelectorAll("[data-pop-close]").forEach(function (b) {
b.addEventListener("click", function () {
closePopover(true);
});
});
["seePlans", "seePlans2"].forEach(function (id) {
var el = document.getElementById(id);
if (el)
el.addEventListener("click", function () {
toast("Opening the full plan comparison…");
var pro = document.querySelector(".plan-card--pro");
if (pro) pro.scrollIntoView({ behavior: "smooth", block: "center" });
});
});
// safe interactive tools just acknowledge
document.querySelectorAll(".tool:not(.tool--locked)").forEach(function (t) {
t.addEventListener("click", function () {
var tip = t.getAttribute("data-tip");
if (tip) toast(tip + " applied.");
});
});
}
/* ---------- Init ---------- */
bindTriggers();
bindBilling();
bindMisc();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northwind — Workspace settings (Pro feature gate)</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="shell">
<!-- ============ Top bar ============ -->
<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 14.5 12 4l8 10.5-8 5.5-8-5.5Z"
fill="currentColor"
opacity=".9"
/>
<path d="m4 14.5 8 5.5 8-5.5-8-2.6-8 2.6Z" fill="#fff" opacity=".4" />
</svg>
</span>
<div class="brand__text">
<strong>Northwind</strong>
<span class="brand__sub">Workspace settings</span>
</div>
</div>
<div class="topbar__right">
<span class="plan-badge" id="planBadge" data-plan="starter">
<span class="plan-badge__dot" aria-hidden="true"></span>
<span id="planLabel">Starter plan</span>
</span>
<button class="btn btn--ghost" id="seePlans" type="button">
View plans
</button>
</div>
</header>
<div class="layout">
<!-- ============ Settings panel ============ -->
<section class="panel" aria-labelledby="panelTitle">
<div class="panel__head">
<div>
<h1 id="panelTitle">Editor & automation</h1>
<p class="panel__lede">
Configure how your team writes, ships, and automates. Some
controls are part of <strong>Northwind Pro</strong>.
</p>
</div>
<span class="upsell-chip" id="lockedCount" aria-live="polite"
>4 locked features</span
>
</div>
<div class="toolbar" role="group" aria-label="Document toolbar">
<button class="tool" type="button" data-tip="Bold">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path
d="M7 5h6a3.5 3.5 0 0 1 0 7H7V5Zm0 7h7a3.5 3.5 0 0 1 0 7H7v-7Z"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linejoin="round"
/>
</svg>
</button>
<button class="tool" type="button" data-tip="Italic">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path
d="M10 5h7M7 19h7M14 5l-4 14"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</button>
<button class="tool" type="button" data-tip="Link">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path
d="M9 15a4 4 0 0 1 0-6l2-2a4 4 0 0 1 6 6l-1 1M15 9a4 4 0 0 1 0 6l-2 2a4 4 0 0 1-6-6l1-1"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</button>
<span class="toolbar__sep" aria-hidden="true"></span>
<!-- Locked tool: AI rewrite -->
<button
class="tool tool--locked"
type="button"
data-locked="true"
data-feature="AI rewrite"
aria-disabled="true"
>
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path
d="m12 3 1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3ZM18 14l.9 2.1 2.1.9-2.1.9L18 20l-.9-2.1L15 17l2.1-.9L18 14Z"
fill="currentColor"
/>
</svg>
<span class="tool__lock" aria-hidden="true">
<svg viewBox="0 0 24 24" width="11" height="11">
<path
d="M7 10V8a5 5 0 0 1 10 0v2"
fill="none"
stroke="currentColor"
stroke-width="2.4"
/>
<rect
x="5"
y="10"
width="14"
height="10"
rx="2.4"
fill="currentColor"
/>
</svg>
</span>
</button>
<!-- Locked tool: Version history -->
<button
class="tool tool--locked"
type="button"
data-locked="true"
data-feature="Version history"
aria-disabled="true"
>
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path
d="M12 7v5l3 2M4 12a8 8 0 1 0 2.3-5.6M4 4v3h3"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span class="tool__lock" aria-hidden="true">
<svg viewBox="0 0 24 24" width="11" height="11">
<path
d="M7 10V8a5 5 0 0 1 10 0v2"
fill="none"
stroke="currentColor"
stroke-width="2.4"
/>
<rect
x="5"
y="10"
width="14"
height="10"
rx="2.4"
fill="currentColor"
/>
</svg>
</span>
</button>
</div>
<!-- ============ Setting rows ============ -->
<ul class="rows" role="list">
<li class="row">
<div class="row__main">
<span class="row__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18">
<path
d="M12 3a9 9 0 1 0 9 9M12 3v9h9"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</span>
<div>
<p class="row__title">Auto-save drafts</p>
<p class="row__desc">Persist edits locally every few seconds.</p>
</div>
</div>
<label class="switch">
<input type="checkbox" checked />
<span class="switch__track" aria-hidden="true"></span>
<span class="sr-only">Toggle auto-save drafts</span>
</label>
</li>
<li class="row">
<div class="row__main">
<span class="row__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18">
<path
d="M4 7h16M4 12h16M4 17h10"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</span>
<div>
<p class="row__title">Markdown shortcuts</p>
<p class="row__desc">Type <code>**</code> to bold, <code>#</code> for headings.</p>
</div>
</div>
<label class="switch">
<input type="checkbox" checked />
<span class="switch__track" aria-hidden="true"></span>
<span class="sr-only">Toggle markdown shortcuts</span>
</label>
</li>
<!-- LOCKED ROW: AI rewrite -->
<li class="row row--locked" data-locked="true" data-feature="AI rewrite suggestions">
<div class="row__main">
<span class="row__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18">
<path
d="m12 3 1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3Z"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linejoin="round"
/>
</svg>
</span>
<div>
<p class="row__title">
AI rewrite suggestions
<span class="pro-pill">PRO</span>
</p>
<p class="row__desc">Inline tone, clarity, and grammar fixes as you type.</p>
</div>
</div>
<button class="row__cta" type="button" data-gate aria-haspopup="dialog">
<span class="row__lock" aria-hidden="true">
<svg viewBox="0 0 24 24" width="13" height="13">
<path d="M7 10V8a5 5 0 0 1 10 0v2" fill="none" stroke="currentColor" stroke-width="2.4" />
<rect x="5" y="10" width="14" height="10" rx="2.4" fill="currentColor" />
</svg>
</span>
Locked
</button>
</li>
<!-- LOCKED ROW: Version history -->
<li class="row row--locked" data-locked="true" data-feature="Unlimited version history">
<div class="row__main">
<span class="row__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18">
<path
d="M12 7v5l3 2M4 12a8 8 0 1 0 2.3-5.6M4 4v3h3"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<div>
<p class="row__title">
Unlimited version history
<span class="pro-pill">PRO</span>
</p>
<p class="row__desc">Restore any revision — Starter keeps the last 7 days.</p>
</div>
</div>
<button class="row__cta" type="button" data-gate aria-haspopup="dialog">
<span class="row__lock" aria-hidden="true">
<svg viewBox="0 0 24 24" width="13" height="13">
<path d="M7 10V8a5 5 0 0 1 10 0v2" fill="none" stroke="currentColor" stroke-width="2.4" />
<rect x="5" y="10" width="14" height="10" rx="2.4" fill="currentColor" />
</svg>
</span>
Locked
</button>
</li>
<!-- LOCKED ROW: Custom export -->
<li class="row row--locked" data-locked="true" data-feature="Branded PDF export">
<div class="row__main">
<span class="row__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18">
<path
d="M12 3v10m0 0 4-4m-4 4-4-4M5 17v2a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-2"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<div>
<p class="row__title">
Branded PDF export
<span class="pro-pill">PRO</span>
</p>
<p class="row__desc">Export with your logo, colors, and custom footer.</p>
</div>
</div>
<button class="row__cta" type="button" data-gate aria-haspopup="dialog">
<span class="row__lock" aria-hidden="true">
<svg viewBox="0 0 24 24" width="13" height="13">
<path d="M7 10V8a5 5 0 0 1 10 0v2" fill="none" stroke="currentColor" stroke-width="2.4" />
<rect x="5" y="10" width="14" height="10" rx="2.4" fill="currentColor" />
</svg>
</span>
Locked
</button>
</li>
</ul>
<p class="panel__foot">
Need everything unlocked for the team?
<button class="link" id="seePlans2" type="button">Compare Northwind plans</button>.
</p>
</section>
<!-- ============ Plan aside ============ -->
<aside class="aside" aria-label="Plan comparison">
<article class="plan-card">
<header class="plan-card__head">
<h2>Starter</h2>
<span class="plan-card__price"
><span class="amt">$0</span><span class="per">/mo</span></span
>
</header>
<p class="plan-card__note">Your current plan — great for solo writers.</p>
<ul class="feat" role="list">
<li class="feat__yes">Unlimited documents</li>
<li class="feat__yes">Markdown shortcuts</li>
<li class="feat__no">AI rewrite suggestions</li>
<li class="feat__no">Branded PDF export</li>
</ul>
</article>
<article class="plan-card plan-card--pro">
<span class="plan-card__ribbon">Most popular</span>
<header class="plan-card__head">
<h2>Pro</h2>
<span class="plan-card__price"
><span class="amt" id="proPrice">$12</span
><span class="per" id="proPer">/seat · mo</span></span
>
</header>
<p class="plan-card__note">Everything in Starter, fully unlocked.</p>
<ul class="feat" role="list">
<li class="feat__yes">AI rewrite suggestions</li>
<li class="feat__yes">Unlimited version history</li>
<li class="feat__yes">Branded PDF export</li>
<li class="feat__yes">Priority support</li>
</ul>
<label class="billing">
<span class="billing__label">Bill annually · save 20%</span>
<span class="switch">
<input type="checkbox" id="annualToggle" />
<span class="switch__track" aria-hidden="true"></span>
</span>
</label>
<button class="btn btn--primary plan-card__cta" id="upgradeMain" type="button">
Upgrade to Pro
</button>
</article>
</aside>
</div>
</main>
<!-- ============ Gate popover (single, repositioned) ============ -->
<div
class="popover"
id="popover"
role="dialog"
aria-modal="false"
aria-labelledby="popTitle"
hidden
>
<span class="popover__arrow" aria-hidden="true"></span>
<div class="popover__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="18" height="18">
<path d="M7 10V8a5 5 0 0 1 10 0v2" fill="none" stroke="currentColor" stroke-width="2.4" />
<rect x="5" y="10" width="14" height="10" rx="2.4" fill="currentColor" />
</svg>
</div>
<h3 id="popTitle">This is a Pro feature</h3>
<p class="popover__body" id="popBody">
Upgrade to Northwind Pro to unlock this control.
</p>
<div class="popover__actions">
<button class="btn btn--ghost btn--sm" type="button" data-pop-close>Not now</button>
<button class="btn btn--primary btn--sm" type="button" id="popUpgrade">
Upgrade — $12/mo
</button>
</div>
</div>
<!-- ============ Toast ============ -->
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script src="script.js"></script>
</body>
</html>Inline feature-locked gate (pro badge)
A workspace settings screen for the fictional Northwind editor, showing how to gate premium
controls in place rather than behind a separate pricing page. Two editor-toolbar buttons and three
setting rows are marked with a bold PRO pill and a small lock SVG, while a header chip keeps a
running, aria-live count of how many features are still locked. Alongside the panel sit Starter
and Pro plan cards with real-feeling feature checklists, a highlighted Most popular ribbon, and a
working annual-billing toggle that recomputes the price.
Clicking any locked tool or row opens a single shared popover (role="dialog") that is positioned
next to the trigger — it flips above when there isn’t room below, clamps to the viewport, and points
its arrow at the control. Focus moves to the Upgrade button, Escape closes it and returns focus, and
an outside click dismisses it. The popover’s body names the specific feature being gated.
Choosing Upgrade runs a demo unlock: the plan badge switches from Starter to Pro, each gated row
loses its lock and PRO pill and gains a live toggle (staggered for a satisfying cascade), the toolbar
locks disappear, the locked-count chip turns into a success state, and the main upgrade button settles
into a confirmed state. A small toast() helper confirms every action. The layout reflows to a single
column and full-width controls down to 360px.
Illustrative UI only — fictional brand, plans, and prices.