Product Roadmap Board — HTML CSS JS Snippet

Product Roadmap Board · Dashboards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Three-column Planned / In Progress / Shipped layout
Colour-coded status dots matching legend (grey, indigo, green)
Four category tags: feature, bug, perf, ux with colour coding
Upvote toggle: vote is added/removed locally without mutating the source
Derived vote count (base + local vote) updates on every toggle
Quarter labels on upcoming items; ✓ Shipped badge on done items
Item count badge per column header
Data-driven rendering from a plain JavaScript columns array

About this UI Snippet

Product Roadmap Board — Planned, In Progress & Shipped with Upvotes

Screenshot of the Product Roadmap Board snippet rendered live

A public product roadmap board is a standard transparency feature for SaaS products and developer tools, and a frequently-searched UI component. Showing users what is coming, what is being built now, and what has shipped builds trust and reduces "when is X coming?" support requests. This snippet provides a complete three-column Kanban-style roadmap with category tags, target quarters, upvote buttons with live vote counts, and a shipped confirmation badge — the released items typically also appear in a changelog feed.

The three-column model

Planned / In Progress / Shipped is the canonical roadmap structure: planned items are committed but not yet started, in-progress items are actively being built, and shipped items are done and deployed. The columns use a CSS grid with three equal columns. Each column is a card with a coloured status dot in the header, an item count badge, and a scrollable list of feature cards.

Feature cards

Each item card shows a title, a category tag (feature, bug, perf, ux), a short description, an upvote button, and a quarter or shipped badge. The tag uses a colour-coded design: purple for features, red for bugs, blue for performance, and amber for UX — making it easy to scan by type across all three columns.

Upvote system

Each item has a vote button (△ hollow, ▲ filled when voted). Clicking toggles the voted state in a local votes map and re-renders. The displayed count is base votes + (voted ? 1 : 0), derived from the votes map rather than mutating the source array — a clean approach that makes the votes undoable. In production, votes would be persisted to a backend with user authentication to prevent duplicate voting.

Data-driven rendering

The entire board renders from a columns array of plain JavaScript objects. Adding a new feature card is adding an object to the correct column's items array; moving a card between columns is moving the object. In a real product this data would come from a backend, a headless CMS, or a product management tool like Linear, Productboard, or Canny via their public API.

Quarter and shipped labels

Planned and in-progress items show their target quarter (Q3 2026, Q4 2026). Shipped items show a green ✓ Shipped badge instead of a quarter, since the timeline is no longer relevant. This distinction is a small detail that makes the board feel genuinely useful rather than a cosmetic exercise.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to work out the derived-vote-count pattern by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the displayed vote count is computed as item.votes plus a lookup in a separate votes toggle map rather than incrementing item.votes directly, and how that separation makes an "undo vote" trivially correct. The same assistant can help optimize it, for example checking whether rebuilding the entire board's innerHTML on every single vote toggle is wasteful once the item count grows, or whether the same column-rendering logic could be extracted into one reusable function instead of being inlined in the map callback. It's also useful for extending the effect: ask it to add drag-and-drop so items can move between columns, persist votes to localStorage keyed by item id so a refresh doesn't reset them, or add a search/filter input that narrows the visible items across all three columns by keyword or tag. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a three-column product roadmap board (Planned, In Progress, Shipped) with upvoting in plain HTML, CSS, and vanilla JavaScript, rendered entirely from a data structure — no Kanban library.

Requirements:
- A nested data structure: an array of column objects (each with an id, a label, and a color), where each column object contains its own array of item objects (each with an id, a title, a category tag like feature/bug/perf/ux, a description, a base vote count, and a target quarter string).
- Render the entire board by mapping over the columns array to produce one card-styled column per entry, and within each column mapping over that column's items to produce one card per item, with a colored status dot and item count badge in each column's header.
- Each item card must show its title, a color-coded category tag (a distinct background/text color per tag type), its description, an upvote button, and either a target-quarter label or, if the item is in the Shipped column specifically, a green "Shipped" checkmark badge instead of the quarter.
- Implement voting so that a separate object or Set tracks which item ids the current user has voted for (the toggle map), and the displayed vote count for every item is computed at render time as that item's base vote count plus one if its id is present in the toggle map — never mutate the base vote count in the source data directly.
- Clicking an item's vote button must toggle that item's id in the toggle map (add it if absent, remove it if present) and re-render the whole board so the count and the button's filled/outlined arrow icon update immediately.
- Make sure adding a new item is as simple as pushing one more object into the correct column's items array with no other code changes required, since the whole board renders generically from the data.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Browse the roadmapScan the three columns for planned, in-progress, and shipped items. Category tags and quarter labels give context for each item.
  2. 2
    Upvote featuresClick △ on any item to cast your vote. The count increases and the button fills. Click again to remove your vote.
  3. 3
    Add your own itemsAdd an object to any column's items array: { id, title, tag, desc, votes, quarter }. Shipped column items show a "✓ Shipped" badge.
  4. 4
    Move items between columnsCut and paste an item object from one column's items array to another — the column it is in determines its status dot and badge.
  5. 5
    Load from an APIReplace the columns array with a fetch() to your roadmap endpoint (Canny, Productboard, Linear, or your own API) and call render() in the then callback.
  6. 6
    Export for your frameworkClick "React" for a component with votes in useState and columns as props. Click "Vue" for a Vue 3 SFC with a reactive votes map.

Real-world uses

Common Use Cases

Public SaaS product roadmap page
Publish the roadmap at /roadmap on your product site. Load entries from a headless CMS or your own API so the product team can update it without a deploy. The upvote system gives users a voice in prioritisation and gives the product team quantified demand data — how many users want dark mode vs CSV import.
Embed in an in-app feedback hub
Mount the roadmap inside the product itself, visible from a "Roadmap" link in the sidebar or settings menu. Users who just hit a limitation can see if it is already planned or in progress, reducing "where is X?" support requests. The shipped column doubles as an in-app changelog.
Internal sprint board for product and engineering
Adapt the three columns to Now / Next / Later or This Sprint / Next Sprint / Backlog for an internal planning board. Add drag-and-drop to move cards between columns, and replace the vote button with an effort/impact score for a lightweight prioritisation matrix.
Sync with Linear, Canny, or Productboard
Fetch items from Linear's GraphQL API (filtering by project and status), Canny's REST API, or Productboard's features endpoint and map the response to the columns array shape. Persist upvotes to the same backend so votes aggregate across all visitors rather than resetting on refresh.
Stakeholder and investor transparency page
Use the roadmap board as a lightweight external-facing product brief for investors, enterprise buyers, and integration partners. The Shipped column demonstrates execution cadence, In Progress shows current focus, and Planned shows strategic direction — a persuasive combination in sales and due-diligence contexts.
Study Kanban-style data-driven UI rendering
The board renders entirely from a nested data array: columns containing items. The upvote system demonstrates a clean derived-state pattern (base + local vote) rather than mutating source data. These patterns — column-based rendering, toggle state without mutation, data-driven re-render — transfer directly to task managers, CRMs, and project tools.
Related: Version History Timeline
See the Version History Timeline for a related dashboards pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

The items array stores a base vote count that never changes. A separate votes plain object acts as a toggle map: votes[id] = true when voted. In the renderer, count = item.votes + (votes[id] ? 1 : 0) derives the displayed value from both sources. This means undoing a vote (deleting votes[id]) automatically returns the displayed count to the original, and the source data stays clean — a pattern that makes syncing to a backend much simpler because you only send the toggle event, not a raw count.

On the client side, store voted item ids in localStorage so votes persist across page reloads. On the backend, record a vote as a row in a votes table with (userId, itemId, createdAt) and a unique constraint on (userId, itemId) to enforce one vote per user per item. The vote endpoint increments a denormalized vote_count on the item row on insert and decrements on delete, returning the new total to the client.

For Linear: query the GraphQL API for issues in a specific project, filtering by state. Map "Backlog" to planned, "In Progress" to progress, and "Done" to shipped. Include the title, description, priority/votes, and target date. For Canny: GET /v1/posts with board_id and category filters. Both APIs return JSON that maps cleanly to the columns[].items shape. Refresh on a server-side schedule and cache so the public page does not hammer the external API.

Accept columns as a prop (or fetch in useEffect). Keep a votes Set in useState for voted item ids. Toggle votes: setVotes(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }). Render each column and item from the data, deriving the vote count as item.votes + (votes.has(item.id) ? 1 : 0). The shipped check and quarter label render conditionally based on the column id. For the Tailwind version, click "Tailwind" to get the same markup with utility classes instead of a scoped stylesheet.