Features / Bento
Hover-to-preview feature list
A list on the left swaps the panel on the right
A list on the left swaps the panel on the right
Build a feature explorer with Tailwind CSS in two columns. The left column is a list of five rows; each row has a small square glyph tile, a medium-weight title, a one-line description and a chevron, and the active row gets a neutral-50 background with a neutral-900 left rule. The right column is a rounded-2xl bordered panel with a soft neutral-50 header strip showing a fake breadcrumb and three window dots, and a body that shows the active feature — a large glyph, a heading, a paragraph, and a two-column list of small facts with muted labels above medium values. Below the panel, a caption in muted small text. Then add JavaScript that switches the active row on both hover and click, swaps the panel content from a data map, and applies a short opacity transition to the panel body so the change reads as a fade rather than a jump. Stack the columns below lg with the panel underneath. <section class="mx-auto max-w-6xl px-6 py-20">
<div class="mb-12 max-w-xl">
<h2 class="text-3xl font-semibold tracking-tight text-neutral-900">One workspace, five jobs done properly</h2>
<p class="mt-3 text-neutral-500">Hover anything on the left to see what it looks like in the product.</p>
</div>
<div class="grid gap-8 lg:grid-cols-[minmax(0,380px)_1fr]">
<div id="featureList" class="space-y-1">
<button data-key="rules" class="feature-row flex w-full items-center gap-3.5 rounded-xl border-l-2 border-transparent px-4 py-3.5 text-left transition">
<span class="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-neutral-100 text-neutral-700"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="m13 2-8 12h7l-1 8 8-12h-7l1-8Z"/></svg></span>
<span class="min-w-0 flex-1">
<span class="block text-sm font-medium text-neutral-900">Rules engine</span>
<span class="block truncate text-xs text-neutral-500">Decisions in under 50ms</span>
</span>
<span class="text-neutral-300">›</span>
</button>
<button data-key="audit" class="feature-row flex w-full items-center gap-3.5 rounded-xl border-l-2 border-transparent px-4 py-3.5 text-left transition">
<span class="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-neutral-100 text-neutral-700"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><rect x="3" y="5" width="18" height="4" rx="1"/><path d="M5 9v10h14V9M10 13h4"/></svg></span>
<span class="min-w-0 flex-1">
<span class="block text-sm font-medium text-neutral-900">Audit trail</span>
<span class="block truncate text-xs text-neutral-500">Signed, searchable, exportable</span>
</span>
<span class="text-neutral-300">›</span>
</button>
<button data-key="alerts" class="feature-row flex w-full items-center gap-3.5 rounded-xl border-l-2 border-transparent px-4 py-3.5 text-left transition">
<span class="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-neutral-100 text-neutral-700"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m4 7 8 6 8-6"/></svg></span>
<span class="min-w-0 flex-1">
<span class="block text-sm font-medium text-neutral-900">Alerting</span>
<span class="block truncate text-xs text-neutral-500">Route by severity and owner</span>
</span>
<span class="text-neutral-300">›</span>
</button>
<button data-key="access" class="feature-row flex w-full items-center gap-3.5 rounded-xl border-l-2 border-transparent px-4 py-3.5 text-left transition">
<span class="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-neutral-100 text-neutral-700"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="M12 3 4.5 6v5.5c0 4.7 3.2 7.7 7.5 9.5 4.3-1.8 7.5-4.8 7.5-9.5V6L12 3Z"/><path d="m9 12 2 2 4-4"/></svg></span>
<span class="min-w-0 flex-1">
<span class="block text-sm font-medium text-neutral-900">Access control</span>
<span class="block truncate text-xs text-neutral-500">Roles that map to your org</span>
</span>
<span class="text-neutral-300">›</span>
</button>
<button data-key="reports" class="feature-row flex w-full items-center gap-3.5 rounded-xl border-l-2 border-transparent px-4 py-3.5 text-left transition">
<span class="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-neutral-100 text-neutral-700"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="M4 20V10m5 10V4m6 16v-7m5 7V7"/></svg></span>
<span class="min-w-0 flex-1">
<span class="block text-sm font-medium text-neutral-900">Reporting</span>
<span class="block truncate text-xs text-neutral-500">Scheduled to inboxes</span>
</span>
<span class="text-neutral-300">›</span>
</button>
</div>
<div>
<div class="overflow-hidden rounded-2xl border border-neutral-200 bg-white">
<div class="flex items-center gap-3 border-b border-neutral-200 bg-neutral-50 px-5 py-3">
<span class="flex gap-1.5">
<span class="h-2.5 w-2.5 rounded-full bg-neutral-300"></span>
<span class="h-2.5 w-2.5 rounded-full bg-neutral-300"></span>
<span class="h-2.5 w-2.5 rounded-full bg-neutral-300"></span>
</span>
<span id="panelCrumb" class="font-mono text-xs text-neutral-500">workspace / rules</span>
</div>
<div id="panelBody" class="p-8 transition-opacity duration-200">
<span id="panelIcon" class="grid h-12 w-12 place-items-center rounded-xl bg-neutral-900 text-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="inline-block h-[1em] w-[1em] shrink-0 align-[-0.125em] transition-transform"><path d="m13 2-8 12h7l-1 8 8-12h-7l1-8Z"/></svg></span>
<h3 id="panelTitle" class="mt-5 text-xl font-semibold text-neutral-900">Rules engine</h3>
<p id="panelText" class="mt-2 max-w-lg leading-relaxed text-neutral-500">Expressions compile to a decision tree, so a policy with four hundred branches still answers inside a request.</p>
<dl id="panelFacts" class="mt-7 grid max-w-md grid-cols-2 gap-5 border-t border-neutral-200 pt-6 text-sm">
<div><dt class="text-xs uppercase tracking-widest text-neutral-400">Median latency</dt><dd class="mt-1 font-medium text-neutral-900">18 ms</dd></div>
<div><dt class="text-xs uppercase tracking-widest text-neutral-400">Evaluated daily</dt><dd class="mt-1 font-medium text-neutral-900">1.4 billion</dd></div>
</dl>
</div>
</div>
<p class="mt-3 text-xs text-neutral-400">Figures from the shared cluster, measured over the last 30 days.</p>
</div>
</div>
</section> var DATA = {
rules: {
crumb: "workspace / rules",
icon: "⚡",
title: "Rules engine",
text: "Expressions compile to a decision tree, so a policy with four hundred branches still answers inside a request.",
facts: [["Median latency", "18 ms"], ["Evaluated daily", "1.4 billion"]],
},
audit: {
crumb: "workspace / audit",
icon: "▤",
title: "Audit trail",
text: "Every decision is written once, signed, and kept queryable for as long as your retention policy says.",
facts: [["Retention", "7 years"], ["Export formats", "CSV, Parquet"]],
},
alerts: {
crumb: "workspace / alerts",
icon: "✉",
title: "Alerting",
text: "Severity, owner and schedule decide where a notification lands — and who it wakes at three in the morning.",
facts: [["Delivery targets", "9"], ["Median notify", "4.2 s"]],
},
access: {
crumb: "workspace / access",
icon: "⛨",
title: "Access control",
text: "Roles map onto the groups you already maintain in your identity provider, synced on every login.",
facts: [["Providers", "Okta, Entra, Google"], ["Sync", "SCIM 2.0"]],
},
reports: {
crumb: "workspace / reports",
icon: "◈",
title: "Reporting",
text: "Build a view once and have it land in the right inboxes every Monday, with the filters already applied.",
facts: [["Schedules", "Hourly to monthly"], ["Formats", "PDF, CSV, link"]],
},
};
var rows = Array.prototype.slice.call(document.querySelectorAll(".feature-row"));
var body = document.getElementById("panelBody");
var crumb = document.getElementById("panelCrumb");
var icon = document.getElementById("panelIcon");
var title = document.getElementById("panelTitle");
var text = document.getElementById("panelText");
var facts = document.getElementById("panelFacts");
var active = "";
function paint(key) {
if (key === active) return;
active = key;
var d = DATA[key];
rows.forEach(function (r) {
var on = r.dataset.key === key;
r.classList.toggle("bg-neutral-50", on);
r.classList.toggle("border-neutral-900", on);
r.classList.toggle("border-transparent", !on);
});
// Fade out, swap while invisible, fade back in.
body.classList.add("opacity-0");
setTimeout(function () {
crumb.textContent = d.crumb;
icon.textContent = d.icon;
title.textContent = d.title;
text.textContent = d.text;
facts.innerHTML = d.facts
.map(function (f) {
return '<div><dt class="text-xs uppercase tracking-widest text-neutral-400">' + f[0] + '</dt><dd class="mt-1 font-medium text-neutral-900">' + f[1] + "</dd></div>";
})
.join("");
body.classList.remove("opacity-0");
}, 160);
}
rows.forEach(function (row) {
row.addEventListener("mouseenter", function () {
paint(row.dataset.key);
});
row.addEventListener("click", function () {
paint(row.dataset.key);
});
row.addEventListener("focus", function () {
paint(row.dataset.key);
});
});
paint("rules");