Skip to content
Marketing block

Public Roadmap Columns

Shipped / in progress / planned in three columns, with status carried by icon as well as colour.

122 lineslucide-reactAdded 8 Aug 2026Updated 2 Sept 2026
  • roadmap
  • kanban
  • columns
  • status
  • planning

What's included

  • components/roadmap-columns.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add roadmap-columns

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Roadmap

What is done, what is being built, and what is next.

Shipped

2
  • Marketing sections

    Heroes, pricing, FAQ, footers.

  • Dark mode tokens

    Every block reads semantic colours.

In progress

2
  • Dashboard blocksQ3 2026

    Shells, tables, settings.

  • CLI installQ3 2026

    Add a block straight to your repo.

Planned

3
  • Full page layoutsQ4 2026

    Composed screens, not sections.

  • Starter templatesQ4 2026

    Multi-page projects, ready to deploy.

  • Vue and Svelte ports2027

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/roadmap-columns.tsx
/**
 * <RoadmapColumns> — a three-column public roadmap.
 *
 * Shipped / In progress / Planned, each column a list of cards. Status is
 * carried by an icon plus a colour, never colour alone, so the columns stay
 * distinguishable in greyscale and for the ~8% of men with a colour vision
 * deficiency.
 */

import * as React from 'react'
import { CircleCheck, CircleDot, Circle } from 'lucide-react'

export type RoadmapStatus = 'shipped' | 'in-progress' | 'planned'

export interface RoadmapItem {
  title: string
  description?: string
  status: RoadmapStatus
  /** e.g. "Q3 2026". Shown as a small tag. */
  eta?: string
}

export interface RoadmapColumnsProps {
  items?: RoadmapItem[]
  heading?: string
  subheading?: string
  className?: string
}

const COLUMNS: { status: RoadmapStatus; label: string; icon: React.ReactNode; accent: string }[] = [
  {
    status: 'shipped',
    label: 'Shipped',
    icon: <CircleCheck className="h-4 w-4" />,
    accent: 'text-emerald-500',
  },
  {
    status: 'in-progress',
    label: 'In progress',
    icon: <CircleDot className="h-4 w-4" />,
    accent: 'text-sky-500',
  },
  {
    status: 'planned',
    label: 'Planned',
    icon: <Circle className="h-4 w-4" />,
    accent: 'text-muted-foreground',
  },
]

const DEFAULT_ITEMS: RoadmapItem[] = [
  { title: 'Marketing sections', status: 'shipped', description: 'Heroes, pricing, FAQ, footers.' },
  { title: 'Dark mode tokens', status: 'shipped', description: 'Every block reads semantic colours.' },
  { title: 'Dashboard blocks', status: 'in-progress', description: 'Shells, tables, settings.', eta: 'Q3 2026' },
  { title: 'CLI install', status: 'in-progress', description: 'Add a block straight to your repo.', eta: 'Q3 2026' },
  { title: 'Full page layouts', status: 'planned', description: 'Composed screens, not sections.', eta: 'Q4 2026' },
  { title: 'Starter templates', status: 'planned', description: 'Multi-page projects, ready to deploy.', eta: 'Q4 2026' },
  { title: 'Vue and Svelte ports', status: 'planned', eta: '2027' },
]

export function RoadmapColumns({
  items = DEFAULT_ITEMS,
  heading = 'Roadmap',
  subheading = 'What is done, what is being built, and what is next.',
  className = '',
}: RoadmapColumnsProps) {
  return (
    <section className={`mx-auto w-full max-w-7xl px-4 py-16 sm:px-6 lg:px-8 ${className}`}>
      <div className="mx-auto mb-12 max-w-2xl text-center">
        <h2 className="text-balance text-3xl font-bold tracking-tight sm:text-4xl">
          {heading}
        </h2>
        {subheading ? <p className="mt-3 text-muted-foreground">{subheading}</p> : null}
      </div>

      <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
        {COLUMNS.map((col) => {
          const columnItems = items.filter((i) => i.status === col.status)
          return (
            <div key={col.status}>
              <div className="mb-4 flex items-center gap-2">
                <span className={col.accent}>{col.icon}</span>
                <h3 data-stress-ignore className="font-semibold tracking-tight">{col.label}</h3>
                <span className="ms-auto rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
                  {columnItems.length}
                </span>
              </div>

              <ul className="space-y-3">
                {columnItems.map((item) => (
                  <li
                    key={item.title}
                    className="rounded-xl border border-border/60 bg-card/80 p-4 backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-md"
                  >
                    <div className="flex items-start justify-between gap-2">
                      <span className="min-w-0 break-words font-medium leading-snug">{item.title}</span>
                      {item.eta ? (
                        <span className="max-w-[6rem] shrink-0 truncate rounded-md bg-muted px-1.5 py-0.5 text-[0.65rem] font-semibold text-muted-foreground">
                          {item.eta}
                        </span>
                      ) : null}
                    </div>
                    {item.description ? (
                      <p className="mt-1.5 text-sm text-muted-foreground">{item.description}</p>
                    ) : null}
                  </li>
                ))}

                {columnItems.length === 0 ? (
                  <li className="rounded-xl border border-dashed border-border/60 p-4 text-center text-sm text-muted-foreground">
                    Nothing here yet
                  </li>
                ) : null}
              </ul>
            </div>
          )
        })}
      </div>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/roadmap-columns.tsx and import it where you need the section:

import { RoadmapColumns } from '@/components/roadmap-columns'

Customize

2 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
itemsRoadmapItem[]DEFAULT_ITEMS
headingstring'Roadmap'
subheadingstring'What is done, what is being built, and what …
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

roadmap-columns.html
<!--
  Public Roadmap Columns — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block has no interactive behaviour, so nothing is missing.
-->
<section class="mx-auto w-full max-w-7xl px-4 py-16 sm:px-6 lg:px-8 ">
  <div class="mx-auto mb-12 max-w-2xl text-center">
    <h2 class="text-balance text-3xl font-bold tracking-tight sm:text-4xl">Roadmap</h2>
    <p class="mt-3 text-muted-foreground">What is done, what is being built, and what is next.</p>
  </div>
  <div class="grid grid-cols-1 gap-6 md:grid-cols-3">
    <div>
      <div class="mb-4 flex items-center gap-2">
        <span class="text-emerald-500">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check h-4 w-4" aria-hidden="true">
            <circle cx="12" cy="12" r="10"></circle>
            <path d="m9 12 2 2 4-4"></path>
          </svg>
        </span>
        <h3 data-stress-ignore="true" class="font-semibold tracking-tight">Shipped</h3>
        <span class="ms-auto rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">2</span>
      </div>
      <ul class="space-y-3">
        <li class="rounded-xl border border-border/60 bg-card/80 p-4 backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-md">
          <div class="flex items-start justify-between gap-2">
            <span class="min-w-0 break-words font-medium leading-snug">Marketing sections</span>
          </div>
          <p class="mt-1.5 text-sm text-muted-foreground">Heroes, pricing, FAQ, footers.</p>
        </li>
        <li class="rounded-xl border border-border/60 bg-card/80 p-4 backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-md">
          <div class="flex items-start justify-between gap-2">
            <span class="min-w-0 break-words font-medium leading-snug">Dark mode tokens</span>
          </div>
          <p class="mt-1.5 text-sm text-muted-foreground">Every block reads semantic colours.</p>
        </li>
      </ul>
    </div>
    <div>
      <div class="mb-4 flex items-center gap-2">
        <span class="text-sky-500">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-dot h-4 w-4" aria-hidden="true">
            <circle cx="12" cy="12" r="10"></circle>
            <circle cx="12" cy="12" r="1"></circle>
          </svg>
        </span>
        <h3 data-stress-ignore="true" class="font-semibold tracking-tight">In progress</h3>
        <span class="ms-auto rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">2</span>
      </div>
      <ul class="space-y-3">
        <li class="rounded-xl border border-border/60 bg-card/80 p-4 backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-md">
          <div class="flex items-start justify-between gap-2">
            <span class="min-w-0 break-words font-medium leading-snug">Dashboard blocks</span>
            <span class="max-w-[6rem] shrink-0 truncate rounded-md bg-muted px-1.5 py-0.5 text-[0.65rem] font-semibold text-muted-foreground">Q3 2026</span>
          </div>
          <p class="mt-1.5 text-sm text-muted-foreground">Shells, tables, settings.</p>
        </li>
        <li class="rounded-xl border border-border/60 bg-card/80 p-4 backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-md">
          <div class="flex items-start justify-between gap-2">
            <span class="min-w-0 break-words font-medium leading-snug">CLI install</span>
            <span class="max-w-[6rem] shrink-0 truncate rounded-md bg-muted px-1.5 py-0.5 text-[0.65rem] font-semibold text-muted-foreground">Q3 2026</span>
          </div>
          <p class="mt-1.5 text-sm text-muted-foreground">Add a block straight to your repo.</p>
        </li>
      </ul>
    </div>
    <div>
      <div class="mb-4 flex items-center gap-2">
        <span class="text-muted-foreground">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle h-4 w-4" aria-hidden="true">
            <circle cx="12" cy="12" r="10"></circle>
          </svg>
        </span>
        <h3 data-stress-ignore="true" class="font-semibold tracking-tight">Planned</h3>
        <span class="ms-auto rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">3</span>
      </div>
      <ul class="space-y-3">
        <li class="rounded-xl border border-border/60 bg-card/80 p-4 backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-md">
          <div class="flex items-start justify-between gap-2">
            <span class="min-w-0 break-words font-medium leading-snug">Full page layouts</span>
            <span class="max-w-[6rem] shrink-0 truncate rounded-md bg-muted px-1.5 py-0.5 text-[0.65rem] font-semibold text-muted-foreground">Q4 2026</span>
          </div>
          <p class="mt-1.5 text-sm text-muted-foreground">Composed screens, not sections.</p>
        </li>
        <li class="rounded-xl border border-border/60 bg-card/80 p-4 backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-md">
          <div class="flex items-start justify-between gap-2">
            <span class="min-w-0 break-words font-medium leading-snug">Starter templates</span>
            <span class="max-w-[6rem] shrink-0 truncate rounded-md bg-muted px-1.5 py-0.5 text-[0.65rem] font-semibold text-muted-foreground">Q4 2026</span>
          </div>
          <p class="mt-1.5 text-sm text-muted-foreground">Multi-page projects, ready to deploy.</p>
        </li>
        <li class="rounded-xl border border-border/60 bg-card/80 p-4 backdrop-blur transition-all hover:-translate-y-0.5 hover:shadow-md">
          <div class="flex items-start justify-between gap-2">
            <span class="min-w-0 break-words font-medium leading-snug">Vue and Svelte ports</span>
            <span class="max-w-[6rem] shrink-0 truncate rounded-md bg-muted px-1.5 py-0.5 text-[0.65rem] font-semibold text-muted-foreground">2027</span>
          </div>
        </li>
      </ul>
    </div>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

11 more blocks in Content & Blog

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Content & Blog

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Content & Blog blocks

View category
Open the full page for this block

Release Timeline

A dated changelog on a vertical rail with added/changed/fixed tags and machine-readable <time> values.

Content & Blog145 linesNo deps

Blog Index Grid

A featured post at full width above a card grid, with drawn covers built from tokens — a blog index with no image pipeline and no broken-thumbnail state.

Content & Blog188 lines1 dep

Case Study Card Grid

A grid of case-study cards that lead with the metric rather than the company name, each one linking out to the full write-up — the block a customer-stories section or a home page needed and only had a full case-study page for.

Content & Blog136 lines1 dep
Open the full page for this block

Article Header & Byline

Kicker, headline, standfirst, byline and share row, with the opening prose and a pull quote included — the type scale of a post, decided once instead of per-article.

Content & Blog171 lines1 dep
Open the full page for this block

Team Grid

An about-page team section on initials avatars rather than photos, so shipping it never waits on headshots and there is no broken-image state to design.

Content & Blog166 lines1 dep

Careers Openings Board

Openings grouped by department with location, type and salary on every row — candidates self-select by craft first, and a flat list makes them scan for it.

Content & Blog186 lines1 dep
Open the full page for this block

Three-Column Docs Frame

Sidebar, article and on-this-page rail — each column answers a different question, and they collapse on mobile in the order that keeps the article alive.

Content & Blog290 lines1 dep
Open the full page for this block

API Endpoint Reference

Method badge, path, parameter table and a request/response pair kept side by side — an example request whose response is a scroll away gets pasted blind.

Content & Blog237 lines1 dep
Open the full page for this block

Package Manager Code Tabs

One install command behind npm/pnpm/yarn/bun tabs with a working copy button — every reader uses exactly one package manager, so showing all four wastes three lines.

Content & Blog152 lines1 dep
Open the full page for this block

Résumé Document

A CV as a document rather than a timeline of cards — one column, dates on the end edge, and a print layer that puts it on paper without a separate PDF to keep in step.

Content & Blog347 lines1 dep
Open the full page for this block

Video Player Shell

The watch screen with no playback in it: a ratio-locked stage, a control bar drawn down to the buffered layer, and an up-next rail. Drop a video element in and wire your own player.

Content & Blog327 lines1 dep