A roadmap template you can fill with AI and share as a link

A roadmap is a short list of what is happening now, what is next and what is later. Shared as a page, it opens in any browser, so stakeholders need no tool login and you skip the PDF export.

A roadmap template is a fixed layout you refill as plans move. Write it as one HTML page and share it as a link, and stakeholders open it in a browser with no tool login. You also skip the export step.

That step is a pain for some people. One person, writing about exporting from Claude, put it this way: "save as pdf, layout shifts. always end up just screenshotting."

A page avoids it. The browser lays the page out for whichever screen opens it. Here is a finished product roadmap with three lanes. Tap a team to filter it.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tallybird roadmap</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 18px 14px 28px; font: 16px/1.5 system-ui, sans-serif; color: #1d2330; background: #f4f5f8; }
  .wrap { max-width: 920px; margin: 0 auto; }
  h1 { font-size: 22px; margin: 0 0 2px; }
  .meta { color: #5b6473; font-size: 14px; margin: 0 0 14px; }
  .changes { background: #fff; border: 1px solid #e1e4ea; border-left: 4px solid #2563eb; border-radius: 10px; padding: 10px 14px; margin-bottom: 14px; }
  .changes h2 { font-size: 13px; text-transform: uppercase; letter-spacing: .5px; color: #1d4ed8; margin: 0 0 4px; }
  .changes ul { margin: 0; padding-left: 18px; font-size: 15px; }
  .filters { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
  .filters button { font: inherit; font-size: 14px; padding: 7px 14px; border-radius: 99px; border: 1px solid #c9cfd9; background: #fff; color: #1d2330; cursor: pointer; }
  .filters button[aria-pressed="true"] { background: #1d2330; color: #fff; border-color: #1d2330; }
  .lanes { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; align-items: start; }
  .lane { background: #e9ecf2; border-radius: 12px; padding: 12px; }
  .lane h2 { font-size: 16px; margin: 0 0 2px; }
  .lane .sub { font-size: 13px; color: #5b6473; margin: 0 0 10px; }
  .card { background: #fff; border-radius: 9px; padding: 11px 12px; margin-bottom: 9px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
  .card h3 { font-size: 15px; margin: 0 0 3px; }
  .card p { font-size: 14px; margin: 0 0 8px; color: #3b4352; }
  .tags { display: flex; flex-wrap: wrap; gap: 6px; }
  .tag { font-size: 12px; font-weight: 600; padding: 3px 8px; border-radius: 99px; background: #eef1f5; color: #3b4352; }
  .st-track { background: #dcfce7; color: #0f5132; }
  .st-risk { background: #ffedd5; color: #9a3412; }
  .st-plan { background: #dbeafe; color: #1e3a8a; }
  .st-idea { background: #f1f5f9; color: #475569; }
  .empty { font-size: 14px; color: #5b6473; padding: 4px 2px; }
  .note { font-size: 13px; color: #5b6473; margin-top: 14px; }
  @media (max-width: 700px) { .lanes { grid-template-columns: 1fr; } }
  @media print {
    body { background: #fff; padding: 0; }
    .filters { display: none; }
    .card, .lane { break-inside: avoid; }
    .tag, .lane { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
  }
</style>
</head>
<body>
<div class="wrap">
  <h1 id="title"></h1>
  <p class="meta" id="meta"></p>

  <div class="changes">
    <h2>Changed since the last update</h2>
    <ul id="changes"></ul>
  </div>

  <div class="filters" id="filters" role="group" aria-label="Filter by team"></div>
  <div class="lanes" id="lanes"></div>
  <p class="note">Dates are not promises. Items in Next and Later can move.</p>
</div>

<script>
  // ---- Change the content here each time. Everything below renders from it. ----
  const PRODUCT = 'Tallybird roadmap';
  const UPDATED = '1 Oct 2026';
  const OWNER = 'Product team';
  const CHANGES = [
    'Moved: Bank sync, from Next to Later (waiting on a partner).',
    'Added: Reminder emails, in Next.',
    'Done: Invoice PDF export.'
  ];
  // lane: now | next | later    status: track | risk | plan | idea
  const LANES = [
    { id: 'now',   name: 'Now',   sub: 'In progress this quarter' },
    { id: 'next',  name: 'Next',  sub: 'Planned, not started' },
    { id: 'later', name: 'Later', sub: 'Ideas we are weighing' }
  ];
  const STATUS = { track: 'On track', risk: 'At risk', plan: 'Planned', idea: 'Idea' };
  const ITEMS = [
    { lane: 'now',   team: 'Design',      status: 'track', title: 'New invoice screen',  note: 'Fewer fields, one tap to send.' },
    { lane: 'now',   team: 'Engineering', status: 'risk',  title: 'Faster client search', note: 'Waiting on a database change.' },
    { lane: 'now',   team: 'Support',     status: 'track', title: 'Help center rewrite',  note: 'Top 20 questions first.' },
    { lane: 'next',  team: 'Engineering', status: 'plan',  title: 'Reminder emails',      note: 'Nudge clients on unpaid invoices.' },
    { lane: 'next',  team: 'Design',      status: 'plan',  title: 'Mobile layout pass',   note: 'Every screen at phone width.' },
    { lane: 'later', team: 'Engineering', status: 'idea',  title: 'Bank sync',            note: 'Match payments automatically.' },
    { lane: 'later', team: 'Support',     status: 'idea',  title: 'In-app chat',          note: 'Only if tickets keep growing.' }
  ];
  // ------------------------------------------------------------------------------

  const teams = ['All'].concat([...new Set(ITEMS.map(i => i.team))]);
  let team = 'All';

  document.getElementById('title').textContent = PRODUCT;
  document.getElementById('meta').textContent = 'Updated ' + UPDATED + ' · ' + OWNER;
  document.title = PRODUCT;

  const changes = document.getElementById('changes');
  CHANGES.forEach(text => {
    const li = document.createElement('li');
    li.textContent = text;
    changes.appendChild(li);
  });

  function el(tag, cls, text) {
    const e = document.createElement(tag);
    if (cls) e.className = cls;
    if (text) e.textContent = text;
    return e;
  }

  function drawFilters() {
    const box = document.getElementById('filters');
    box.textContent = '';
    teams.forEach(t => {
      const b = el('button', '', t);
      b.type = 'button';
      b.setAttribute('aria-pressed', String(t === team));
      b.addEventListener('click', () => { team = t; drawFilters(); drawLanes(); });
      box.appendChild(b);
    });
  }

  function drawLanes() {
    const box = document.getElementById('lanes');
    box.textContent = '';
    LANES.forEach(lane => {
      const col = el('section', 'lane');
      const shown = ITEMS.filter(i => i.lane === lane.id && (team === 'All' || i.team === team));
      col.appendChild(el('h2', '', lane.name + ' (' + shown.length + ')'));
      col.appendChild(el('p', 'sub', lane.sub));
      if (!shown.length) col.appendChild(el('p', 'empty', 'Nothing here for this team.'));
      shown.forEach(i => {
        const c = el('article', 'card');
        c.appendChild(el('h3', '', i.title));
        c.appendChild(el('p', '', i.note));
        const tags = el('div', 'tags');
        tags.appendChild(el('span', 'tag st-' + i.status, STATUS[i.status]));
        tags.appendChild(el('span', 'tag', i.team));
        c.appendChild(tags);
        col.appendChild(c);
      });
      box.appendChild(col);
    });
  }

  drawFilters();
  drawLanes();
</script>
</body>
</html>
A roadmap for a fictional invoicing app. The items, dates and changes are one block at the top of the script. Edit them and it reruns.

What goes on a roadmap page

The example follows one layout. It is an editorial choice, not a standard, and it is meant to be read in under a minute.

  1. Title and update date – so a reader knows how old it is.
  2. Changed since the last update – two or three lines, at the top.
  3. Lanes – Now (in progress), Next (planned), Later (ideas).
  4. Items – a name, one line of why, a status and a team.
  5. A note that dates are not promises, if that is true for your team.

Stakeholders mostly want two answers: what changed, and whether their item moved. Put those first. If you track work by week instead of by lane, an HTML Gantt chart is the other common shape, and the second example below uses it.

Make it with AI

Paste this prompt into ChatGPT, Claude or Gemini. Put your own facts at the bottom.

Write ONE self-contained HTML file: a product roadmap page, to be read on a phone.
Rules:
- Plain HTML, CSS and JavaScript in one file. No images, no external files, no form that sends data.
- One column on a phone, three lanes side by side on a wide screen. Add a viewport meta tag. Text at least 14px.
- At the top: the title, the date last updated, and a short list called "Changed since the last update".
- Three lanes: Now, Next, Later. Each item shows a name, one line of why, a status chip and a team.
- Put every item, date and change in one block at the top of the script, with the comment "Change the content here".
- Add a row of buttons that filters the items by team.
- Add a print style: no grey page background, cards do not split across pages.
- If I did not give you a fact, write TODO. Do not invent items, dates, owners or statuses.
My facts:
Product or team: ...
Last updated: ...
What changed since last time: ...
Items, with lane, team, status and one line of why: ...

The block-at-the-top line matters. It turns each update into one edit instead of a hunt through the page.

What to check in the result

An AI is good at layout and loose with facts. Read the page the way a stakeholder will.

  • Every date and status. Compare them with your own plan.
  • Every owner. A wrong name on a roadmap causes real confusion.
  • Every TODO. Replace each one, or delete the line.
  • Invented items. Anything you did not give it.
  • The phone. Open it on your own phone before you send it.

If the page looks wrong, fixing AI-generated HTML covers the usual causes.

Paste the HTML into a NOS document and choose Create share link. HTML to link walks through it.

From AI chat to a link, in four steps.
From AI chat to a link, in four steps.

The result is a page anyone with the link can open in a browser, on a phone too, with no account. They can read it but not edit it. The filter buttons still work for them, because the script runs.

When a date moves, open the page in NOS, change the block at the top of the script and save. The same link shows the new version. Add a line to the changed-since-last-update list, so readers can see what moved without comparing.

Send the address in the body of your email or message, with one line saying what changed. A link is plain text, so it goes anywhere you can paste text. For the chat tools, see sending an HTML file in Slack and sharing in Teams.

A team timeline for the quarter

Lanes show order. A timeline shows when. The second example puts five items on a 13-week quarter. Tap a bar for the owner and the note.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Platform team Q4 timeline</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; padding: 18px 14px 26px; font: 16px/1.5 system-ui, sans-serif; color: #1d2330; background: #f4f5f8; }
  .wrap { max-width: 920px; margin: 0 auto; }
  h1 { font-size: 22px; margin: 0 0 2px; }
  .meta { color: #5b6473; font-size: 14px; margin: 0 0 14px; }
  .board { background: #fff; border: 1px solid #e1e4ea; border-radius: 12px; padding: 12px 14px 8px; }
  .months, .row { display: grid; grid-template-columns: 190px 1fr; gap: 12px; align-items: center; }
  .months { margin-bottom: 6px; }
  .mtrack { display: grid; grid-template-columns: 4fr 5fr 4fr; }
  .mtrack span { font-size: 13px; font-weight: 700; color: #3b4352; border-left: 1px solid #d5d9e0; padding-left: 6px; }
  .row { padding: 7px 0; border-top: 1px solid #eef0f4; }
  .label { font-size: 15px; font-weight: 600; }
  .label small { display: block; font-weight: 400; font-size: 13px; color: #5b6473; }
  .track { position: relative; height: 34px; background: repeating-linear-gradient(to right, #f1f3f7 0, #f1f3f7 calc(100% / 13 - 1px), #fff calc(100% / 13 - 1px), #fff calc(100% / 13)); border-radius: 6px; }
  .bar { position: absolute; top: 3px; height: 28px; border: 0; border-radius: 6px; font: inherit; font-size: 13px; font-weight: 600; text-align: left; padding: 0 8px; cursor: pointer; color: #fff; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
  .bar:focus-visible { outline: 3px solid #1d2330; outline-offset: 1px; }
  .bar[aria-pressed="true"] { box-shadow: 0 0 0 3px #1d2330; }
  .s-done { background: #15803d; }
  .s-go { background: #1d4ed8; }
  .s-plan { background: #475569; }
  .s-risk { background: #b45309; }
  .detail { margin-top: 12px; background: #fff; border: 1px solid #e1e4ea; border-radius: 12px; padding: 12px 14px; min-height: 84px; }
  .detail h2 { font-size: 16px; margin: 0 0 2px; }
  .detail p { margin: 0; font-size: 15px; color: #3b4352; }
  .legend { display: flex; flex-wrap: wrap; gap: 6px 14px; font-size: 13px; color: #3b4352; margin-top: 10px; }
  .legend i { display: inline-block; width: 11px; height: 11px; border-radius: 3px; margin-right: 5px; vertical-align: -1px; }
  @media (max-width: 640px) {
    .months, .row { grid-template-columns: 1fr; gap: 4px; }
    .months .spacer { display: none; }
    .row { padding: 9px 0; }
  }
  @media print { body { background: #fff; } .bar { print-color-adjust: exact; -webkit-print-color-adjust: exact; } .row { break-inside: avoid; } }
</style>
</head>
<body>
<div class="wrap">
  <h1 id="title"></h1>
  <p class="meta" id="meta"></p>
  <div class="board">
    <div class="months"><div class="spacer"></div><div class="mtrack"><span>Oct</span><span>Nov</span><span>Dec</span></div></div>
    <div id="rows"></div>
  </div>
  <div class="legend">
    <span><i class="s-done"></i>Done</span><span><i class="s-go"></i>In progress</span><span><i class="s-plan"></i>Planned</span><span><i class="s-risk"></i>At risk</span>
  </div>
  <div class="detail" id="detail" aria-live="polite"><p>Tap a bar to see what it is and who owns it.</p></div>
</div>

<script>
  // ---- Change the content here each time. Week 1 is the week of 5 Oct. ----
  const TITLE = 'Platform team, Q4 2026';
  const UPDATED = '1 Oct 2026';
  const WEEKS = 13;
  // status: done | go | plan | risk   (start and end are week numbers, 1 to 13)
  const ITEMS = [
    { name: 'Move logins to the new service', owner: 'Priya N.',  start: 1, end: 5,  status: 'go',   note: 'Old logins keep working until week 6.' },
    { name: 'Retire the old report server',   owner: 'Tomas W.',  start: 3, end: 8,  status: 'risk', note: 'Two teams still read from it. Waiting on their dates.' },
    { name: 'Backup restore test',            owner: 'Lena K.',   start: 2, end: 3,  status: 'done', note: 'Restored a full copy in a test space. It worked.' },
    { name: 'Faster build pipeline',          owner: 'Sam O.',    start: 6, end: 10, status: 'plan', note: 'Goal: builds that wait less for each other.' },
    { name: 'Holiday on-call plan',           owner: 'Priya N.',  start: 10, end: 13, status: 'plan', note: 'Cover for the last two weeks of December.' }
  ];
  // ------------------------------------------------------------------------

  const NAMES = { done: 'Done', go: 'In progress', plan: 'Planned', risk: 'At risk' };
  document.getElementById('title').textContent = TITLE;
  document.getElementById('meta').textContent = 'Updated ' + UPDATED + ' · Week 1 = week of 5 Oct';
  document.title = TITLE;

  const rows = document.getElementById('rows');
  const detail = document.getElementById('detail');
  const buttons = [];

  ITEMS.forEach((it) => {
    const row = document.createElement('div');
    row.className = 'row';

    const label = document.createElement('div');
    label.className = 'label';
    label.textContent = it.name;
    const small = document.createElement('small');
    small.textContent = it.owner;
    label.appendChild(small);

    const track = document.createElement('div');
    track.className = 'track';
    const bar = document.createElement('button');
    bar.type = 'button';
    bar.className = 'bar s-' + it.status;
    bar.style.left = ((it.start - 1) / WEEKS * 100) + '%';
    bar.style.width = ((it.end - it.start + 1) / WEEKS * 100) + '%';
    bar.textContent = 'W' + it.start + (it.end > it.start ? '–' + it.end : '');
    bar.setAttribute('aria-pressed', 'false');
    bar.setAttribute('aria-label', it.name + ', ' + NAMES[it.status] + ', weeks ' + it.start + ' to ' + it.end);
    bar.addEventListener('click', () => {
      buttons.forEach(b => b.setAttribute('aria-pressed', String(b === bar)));
      detail.textContent = '';
      const h = document.createElement('h2');
      h.textContent = it.name + ' – ' + NAMES[it.status];
      const p = document.createElement('p');
      p.textContent = 'Weeks ' + it.start + ' to ' + it.end + ' · Owner: ' + it.owner + ' · ' + it.note;
      detail.appendChild(h);
      detail.appendChild(p);
    });
    buttons.push(bar);
    track.appendChild(bar);

    row.appendChild(label);
    row.appendChild(track);
    rows.appendChild(row);
  });
</script>
</body>
</html>
A Q4 timeline for a fictional platform team. Add an item to the list at the top of the script and a new bar appears.

On a phone, each label moves above its bar, so nothing is squeezed. If you want a longer view with milestones, see a timeline as a link.

File, screenshot or page: how they compare

A file or screenshot sent each time, against one page behind one link.
A file or screenshot sent each time, against one page behind one link.
PDF attached Screenshot Page as a link
Layout on a phone Fixed page size Fixed picture Fits the screen
A date moves New export, new send New picture, new send Edit the page, same link
Needs a tool login to read No No No
Old versions Pile up in inboxes Pile up in chats One address, one current version

For personal Gmail accounts, Google's help page says the attachment limit is 25 MB, and Gmail adds a larger file as a Drive link instead (as of October 2026).

A one-page roadmap is far below that, so size is rarely the issue. The issue is what happens when a date moves after you sent it. See email vs link for documents.

What a stakeholder sees on a phone: lanes stacked in one column, not a wide page shrunk to fit.
What a stakeholder sees on a phone: lanes stacked in one column, not a wide page shrunk to fit.

The phone layout needs a viewport meta tag. MDN says some mobile devices render pages in a virtual window wider than the screen and shrink the result, and that width=device-width avoids that.

The prompt above already asks for it.

When it does not work

What you see Cause Fix
The chat shows raw code, not a page You copied the chat text, not the code block Copy only the code, from the doctype line to the last html tag
A logo or screenshot is missing Images from other sites are blocked Ask for no images, or a logo drawn in SVG
A date or owner is wrong The AI guessed or filled a gap Compare with your plan and fix it in the script block
A feedback form does nothing Forms cannot send to a server from the link Write your email address as plain text
A stakeholder quotes an old date They opened an old file or an old link Send only the link, never a file copy
The text is tiny on a phone No viewport meta tag Ask the AI to add one, and a 14px base size
Three lanes are squeezed on a phone The layout does not stack Ask for one column below 700px

Related reading. For a weekly rhythm, HTML for weekly updates shows a page you refill each Friday, and sharing a dashboard with your team covers the live-numbers case.

Questions people ask

What should a roadmap template include?

A title with the date it was last updated, the lanes (Now, Next, Later in the first example), and for each item a name, an owner or team, a status and one line of why. Add a short list of what changed since the last update. This layout is an editorial choice, not a standard.

Can an AI write my roadmap?

It can write the layout and tidy your wording. Give it your own items, dates and owners, and tell it to write TODO where it has no fact. Then check every date and owner yourself, because an AI can invent a date that reads as real.

Why not send a PDF or a screenshot?

You can. A PDF or a screenshot is a fixed file, so a date that moves means a new export and a new send. One person, writing about exporting from Claude, said "save as pdf, layout shifts" and that they always end up just screenshotting. A page is laid out by the browser for each reader's screen.

How do I update the roadmap?

Open the page in NOS, change the items in the block at the top of the script, and save. The share link stays the same and shows the new version. Add a line to the changed-since-last-update list so readers see what moved.

Can stakeholders comment or edit?

No. Anyone with the link can open and read the page, not edit it. Ask them to reply in your usual channel, such as email or chat, and make the change yourself.

Keep reading