Features / Bento
Public roadmap columns
Shipped, in progress and under consideration
Shipped, in progress and under consideration
Build a public roadmap section with Tailwind CSS as three columns. Each column has a header row with a coloured dot, a title — Shipped, In progress, Exploring — and a muted count, then a stack of cards. Every card is a bordered rounded-xl tile with a small category label, a title in medium weight, a two-line description, and a footer row holding either a shipped date with a check glyph, a thin progress bar with a percentage, or an upvote button with a count. Give the shipped column faintly muted text so attention lands on the live work. Under the columns, a bordered strip inviting people to suggest something, with a one-line pitch and a dark button. Then add JavaScript that makes the upvote buttons toggle: incrementing the count and filling the button dark on first click, reverting on a second, and keeping a running total in the exploring column header. <section class="mx-auto max-w-6xl px-6 py-16">
<div class="mb-10 flex flex-wrap items-end justify-between gap-4">
<div>
<h2 class="text-3xl font-semibold tracking-tight text-neutral-900">What we are building</h2>
<p class="mt-2 text-neutral-500">Updated every Friday. Dates are intentions, not promises.</p>
</div>
<a href="#" class="text-sm font-medium text-neutral-900 underline-offset-4 hover:underline">Subscribe to changes <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="M14 4h6v6M20 4l-9 9"/><path d="M18 13v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6"/></svg></a>
</div>
<div class="grid gap-5 lg:grid-cols-3">
<div>
<div class="flex items-center gap-2 pb-4">
<span class="h-2.5 w-2.5 rounded-full bg-green-500"></span>
<p class="text-sm font-semibold text-neutral-900">Shipped</p>
<span class="text-xs text-neutral-400">last 30 days · 3</span>
</div>
<div class="space-y-3">
<article class="rounded-xl border border-neutral-200 bg-white p-4 opacity-80">
<p class="text-[11px] font-medium uppercase tracking-widest text-neutral-400">Performance</p>
<p class="mt-1.5 text-sm font-medium text-neutral-900">Query caching on every plan</p>
<p class="mt-1 text-sm leading-relaxed text-neutral-500">Identical queries hit a warm cache, keyed by the row-level rules.</p>
<p class="mt-3 flex items-center gap-1.5 border-t border-neutral-100 pt-3 text-xs text-green-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="m5 12.5 4.25 4.25L19 7"/></svg> 02 Aug</p>
</article>
<article class="rounded-xl border border-neutral-200 bg-white p-4 opacity-80">
<p class="text-[11px] font-medium uppercase tracking-widest text-neutral-400">Integrations</p>
<p class="mt-1.5 text-sm font-medium text-neutral-900">Databricks source</p>
<p class="mt-1 text-sm leading-relaxed text-neutral-500">Unity Catalog aware, with column-level lineage on import.</p>
<p class="mt-3 flex items-center gap-1.5 border-t border-neutral-100 pt-3 text-xs text-green-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="m5 12.5 4.25 4.25L19 7"/></svg> 24 Jul</p>
</article>
<article class="rounded-xl border border-neutral-200 bg-white p-4 opacity-80">
<p class="text-[11px] font-medium uppercase tracking-widest text-neutral-400">Admin</p>
<p class="mt-1.5 text-sm font-medium text-neutral-900">SCIM deprovisioning</p>
<p class="mt-1 text-sm leading-relaxed text-neutral-500">Removing someone from your directory now revokes access in minutes.</p>
<p class="mt-3 flex items-center gap-1.5 border-t border-neutral-100 pt-3 text-xs text-green-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="m5 12.5 4.25 4.25L19 7"/></svg> 11 Jul</p>
</article>
</div>
</div>
<div>
<div class="flex items-center gap-2 pb-4">
<span class="h-2.5 w-2.5 rounded-full bg-amber-400"></span>
<p class="text-sm font-semibold text-neutral-900">In progress</p>
<span class="text-xs text-neutral-400">3</span>
</div>
<div class="space-y-3">
<article class="rounded-xl border border-neutral-200 bg-white p-4">
<p class="text-[11px] font-medium uppercase tracking-widest text-neutral-400">Reporting</p>
<p class="mt-1.5 text-sm font-medium text-neutral-900">Cross-source joins</p>
<p class="mt-1 text-sm leading-relaxed text-neutral-500">Join a warehouse table to a live API source without staging it first.</p>
<div class="mt-3 border-t border-neutral-100 pt-3">
<div class="h-1.5 overflow-hidden rounded-full bg-neutral-100"><div class="h-full w-[70%] rounded-full bg-neutral-900"></div></div>
<p class="mt-1.5 text-xs text-neutral-400">70% · beta in September</p>
</div>
</article>
<article class="rounded-xl border border-neutral-200 bg-white p-4">
<p class="text-[11px] font-medium uppercase tracking-widest text-neutral-400">Mobile</p>
<p class="mt-1.5 text-sm font-medium text-neutral-900">Offline approvals</p>
<p class="mt-1 text-sm leading-relaxed text-neutral-500">Queue decisions on the phone and sync them when signal returns.</p>
<div class="mt-3 border-t border-neutral-100 pt-3">
<div class="h-1.5 overflow-hidden rounded-full bg-neutral-100"><div class="h-full w-[45%] rounded-full bg-neutral-900"></div></div>
<p class="mt-1.5 text-xs text-neutral-400">45% · in internal testing</p>
</div>
</article>
<article class="rounded-xl border border-neutral-200 bg-white p-4">
<p class="text-[11px] font-medium uppercase tracking-widest text-neutral-400">Platform</p>
<p class="mt-1.5 text-sm font-medium text-neutral-900">Regional failover</p>
<p class="mt-1 text-sm leading-relaxed text-neutral-500">Automatic promotion of a standby region, with no key rotation.</p>
<div class="mt-3 border-t border-neutral-100 pt-3">
<div class="h-1.5 overflow-hidden rounded-full bg-neutral-100"><div class="h-full w-[20%] rounded-full bg-neutral-900"></div></div>
<p class="mt-1.5 text-xs text-neutral-400">20% · design review done</p>
</div>
</article>
</div>
</div>
<div>
<div class="flex items-center gap-2 pb-4">
<span class="h-2.5 w-2.5 rounded-full bg-neutral-300"></span>
<p class="text-sm font-semibold text-neutral-900">Exploring</p>
<span id="voteTotal" class="text-xs text-neutral-400">612 votes</span>
</div>
<div class="space-y-3">
<article class="rounded-xl border border-neutral-200 bg-white p-4">
<p class="text-[11px] font-medium uppercase tracking-widest text-neutral-400">Reporting</p>
<p class="mt-1.5 text-sm font-medium text-neutral-900">Notebook-style analysis</p>
<p class="mt-1 text-sm leading-relaxed text-neutral-500">Cells that mix SQL, prose and charts in one shareable document.</p>
<div class="mt-3 border-t border-neutral-100 pt-3">
<button class="vote flex items-center gap-1.5 rounded-lg border border-neutral-200 px-2.5 py-1 text-xs text-neutral-600 transition hover:bg-neutral-50"><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 16V4m0 0L7 9m5-5 5 5"/><path d="M5 14v5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-5"/></svg> <span class="count">284</span></button>
</div>
</article>
<article class="rounded-xl border border-neutral-200 bg-white p-4">
<p class="text-[11px] font-medium uppercase tracking-widest text-neutral-400">Automations</p>
<p class="mt-1.5 text-sm font-medium text-neutral-900">Conditional branching</p>
<p class="mt-1 text-sm leading-relaxed text-neutral-500">If-this-then-that paths inside a workflow, with a dry-run mode.</p>
<div class="mt-3 border-t border-neutral-100 pt-3">
<button class="vote flex items-center gap-1.5 rounded-lg border border-neutral-200 px-2.5 py-1 text-xs text-neutral-600 transition hover:bg-neutral-50"><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 16V4m0 0L7 9m5-5 5 5"/><path d="M5 14v5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-5"/></svg> <span class="count">198</span></button>
</div>
</article>
<article class="rounded-xl border border-neutral-200 bg-white p-4">
<p class="text-[11px] font-medium uppercase tracking-widest text-neutral-400">Admin</p>
<p class="mt-1.5 text-sm font-medium text-neutral-900">Per-environment budgets</p>
<p class="mt-1 text-sm leading-relaxed text-neutral-500">Hard spend caps for staging, so a runaway job cannot bill you.</p>
<div class="mt-3 border-t border-neutral-100 pt-3">
<button class="vote flex items-center gap-1.5 rounded-lg border border-neutral-200 px-2.5 py-1 text-xs text-neutral-600 transition hover:bg-neutral-50"><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 16V4m0 0L7 9m5-5 5 5"/><path d="M5 14v5a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-5"/></svg> <span class="count">130</span></button>
</div>
</article>
</div>
</div>
</div>
<div class="mt-8 flex flex-wrap items-center justify-between gap-4 rounded-2xl border border-neutral-200 bg-neutral-50 px-7 py-6">
<div>
<p class="text-sm font-semibold text-neutral-900">Missing something?</p>
<p class="mt-1 text-sm text-neutral-500">Requests with the most votes get read at every planning session. We reply to all of them.</p>
</div>
<button class="rounded-xl bg-neutral-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-neutral-800">Suggest a feature</button>
</div>
</section> var votes = Array.prototype.slice.call(document.querySelectorAll(".vote"));
var totalEl = document.getElementById("voteTotal");
function total() {
return votes.reduce(function (sum, button) {
return sum + Number(button.querySelector(".count").textContent);
}, 0);
}
votes.forEach(function (button) {
var count = button.querySelector(".count");
button.addEventListener("click", function () {
var voted = button.dataset.voted === "true";
button.dataset.voted = voted ? "false" : "true";
count.textContent = Number(count.textContent) + (voted ? -1 : 1);
button.classList.toggle("bg-neutral-900", !voted);
button.classList.toggle("text-white", !voted);
button.classList.toggle("border-neutral-900", !voted);
button.classList.toggle("text-neutral-600", voted);
totalEl.textContent = total() + " votes";
});
});
totalEl.textContent = total() + " votes";