Google Sheets to webpage: show the results without sharing the sheet

Most people who open your numbers only want to read them. A web page shows the results, and the sheet with its formulas and extra tabs stays yours.

To turn a Google Sheet into a webpage, copy the finished results, ask an AI chat to build one HTML page from them, and send that page as a link. Viewers open the link in a browser. They do not need access to your sheet.

That is the point. Sharing the sheet means managing who may open the file, and every tab comes with it. A page holds only the table you pasted into it, laid out so it reads well on a phone.

Here is a results page of that kind, for a made-up company. Tap a region, tap a column heading to sort, and shrink the window to see it turn into cards.

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>Q3 sales results</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; font-family: system-ui, sans-serif; background: #f4f6fa; color: #1d2330; line-height: 1.45; }
  header { background: #1b3a6b; color: #fff; padding: 20px 16px 16px; }
  .in { max-width: 760px; margin: 0 auto; }
  header h1 { margin: 0 0 4px; font-size: 22px; }
  header p { margin: 0; font-size: 14px; opacity: .92; }
  main { padding: 14px 16px 24px; }
  .tiles { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 14px; }
  .tile { background: #fff; border-radius: 12px; padding: 12px 14px; box-shadow: 0 1px 5px rgba(0,0,0,.07); }
  .tile span { display: block; font-size: 12px; font-weight: 700; color: #5b6472; text-transform: uppercase; letter-spacing: .03em; }
  .tile b { display: block; font-size: 22px; margin-top: 2px; }
  .chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
  .chips button { border: 1px solid #c9d1de; background: #fff; color: #1b3a6b; border-radius: 999px; padding: 8px 14px; font-size: 14px; font-weight: 600; cursor: pointer; }
  .chips button[aria-pressed="true"] { background: #1b3a6b; color: #fff; border-color: #1b3a6b; }
  .card { background: #fff; border-radius: 12px; padding: 6px 12px 10px; box-shadow: 0 1px 5px rgba(0,0,0,.07); }
  table { width: 100%; border-collapse: collapse; font-size: 14px; }
  th, td { text-align: left; padding: 10px 6px; border-bottom: 1px solid #e6e9f0; vertical-align: middle; }
  th { font-size: 12px; color: #5b6472; text-transform: uppercase; letter-spacing: .03em; }
  th button { all: unset; cursor: pointer; font: inherit; color: inherit; text-transform: inherit; letter-spacing: inherit; font-weight: 700; padding: 4px 0; }
  th button:focus-visible { outline: 2px solid #1b3a6b; outline-offset: 2px; }
  td.num, th.num { text-align: right; }
  .bar { height: 8px; background: #e6e9f0; border-radius: 4px; overflow: hidden; min-width: 70px; }
  .bar i { display: block; height: 100%; background: #2f6fd0; }
  .up { color: #0f6b3a; font-weight: 700; }
  .down { color: #b4470c; font-weight: 700; }
  footer { text-align: center; font-size: 13px; color: #5b6472; padding: 0 16px 20px; }
  @media (max-width: 560px) {
    .tiles { grid-template-columns: 1fr 1fr; }
    .tile:last-child { grid-column: span 2; }
    thead { display: none; }
    tr { display: block; border-bottom: 1px solid #e6e9f0; padding: 8px 0; }
    td { display: flex; justify-content: space-between; gap: 12px; border: 0; padding: 4px 4px; text-align: right; }
    td::before { content: attr(data-label); font-size: 12px; font-weight: 700; color: #5b6472; text-transform: uppercase; text-align: left; }
    td:first-child { font-size: 16px; font-weight: 700; justify-content: flex-start; }
    td:first-child::before { display: none; }
    td.bars { display: block; }
    td.bars::before { display: none; }
  }
</style>
</head>
<body>
<header><div class="in">
  <h1>Q3 sales by product line</h1>
  <p>Northline Supplies (fictional) &middot; Figures as of 30 September 2026</p>
</div></header>
<main><div class="in">
  <div class="tiles" id="tiles"></div>
  <div class="chips" id="chips" role="group" aria-label="Filter by region"></div>
  <div class="card">
    <table>
      <thead><tr>
        <th><button data-key="line">Product line</button></th>
        <th><button data-key="region">Region</button></th>
        <th class="num"><button data-key="sales">Sales</button></th>
        <th class="num"><button data-key="change">vs Q2</button></th>
        <th>Share of top line</th>
      </tr></thead>
      <tbody id="rows"></tbody>
    </table>
  </div>
</div></main>
<footer>This page is a snapshot of the sheet. The date above says when it was made.</footer>

<script>
  // Paste your sheet rows here, one line per row. Everything else builds itself.
  const DATA = [
    { line: 'Notebooks',     region: 'North', sales: 48200, change: 6.4 },
    { line: 'Desk lamps',    region: 'North', sales: 31750, change: -2.1 },
    { line: 'Pens',          region: 'South', sales: 22980, change: 11.8 },
    { line: 'Chairs',        region: 'South', sales: 64300, change: 3.2 },
    { line: 'Whiteboards',   region: 'East',  sales: 18400, change: -7.5 },
    { line: 'Filing boxes',  region: 'East',  sales: 27150, change: 1.9 },
    { line: 'Staplers',      region: 'West',  sales: 9600,  change: 0.4 },
    { line: 'Standing desks',region: 'West',  sales: 71900, change: 14.2 }
  ];

  const money = (n) => '$' + n.toLocaleString('en-US');
  const state = { region: 'All', key: 'sales', dir: -1 };
  const tiles = document.getElementById('tiles');
  const chips = document.getElementById('chips');
  const rows = document.getElementById('rows');

  function addCell(tr, text, cls, label) {
    const td = document.createElement('td');
    td.textContent = text;
    if (cls) td.className = cls;
    td.dataset.label = label;
    tr.appendChild(td);
    return td;
  }

  function render() {
    const shown = DATA.filter((d) => state.region === 'All' || d.region === state.region);
    shown.sort((a, b) => {
      const x = a[state.key], y = b[state.key];
      return (x > y ? 1 : x < y ? -1 : 0) * state.dir;
    });
    const total = shown.reduce((s, d) => s + d.sales, 0);
    const top = Math.max(...shown.map((d) => d.sales));

    tiles.innerHTML = '';
    [['Total sales', money(total)], ['Product lines', String(shown.length)], ['Best line', shown.length ? shown.slice().sort((a, b) => b.sales - a.sales)[0].line : '-']]
      .forEach(([label, value]) => {
        const t = document.createElement('div');
        t.className = 'tile';
        const s = document.createElement('span'); s.textContent = label;
        const b = document.createElement('b'); b.textContent = value;
        t.append(s, b);
        tiles.appendChild(t);
      });

    rows.innerHTML = '';
    shown.forEach((d) => {
      const tr = document.createElement('tr');
      addCell(tr, d.line, '', 'Product line');
      addCell(tr, d.region, '', 'Region');
      addCell(tr, money(d.sales), 'num', 'Sales');
      addCell(tr, (d.change > 0 ? '+' : '') + d.change.toFixed(1) + '%', 'num ' + (d.change >= 0 ? 'up' : 'down'), 'vs Q2');
      const bar = addCell(tr, '', 'bars', '');
      bar.innerHTML = '<div class="bar"><i></i></div>';
      bar.querySelector('i').style.width = Math.round((d.sales / top) * 100) + '%';
      rows.appendChild(tr);
    });
  }

  ['All', ...new Set(DATA.map((d) => d.region))].forEach((r) => {
    const b = document.createElement('button');
    b.textContent = r;
    b.setAttribute('aria-pressed', r === 'All' ? 'true' : 'false');
    b.addEventListener('click', () => {
      state.region = r;
      chips.querySelectorAll('button').forEach((x) => x.setAttribute('aria-pressed', String(x === b)));
      render();
    });
    chips.appendChild(b);
  });

  document.querySelectorAll('th button').forEach((btn) => {
    btn.addEventListener('click', () => {
      const k = btn.dataset.key;
      state.dir = state.key === k ? -state.dir : (k === 'line' || k === 'region' ? 1 : -1);
      state.key = k;
      render();
    });
  });

  render();
</script>
</body>
</html>
One page built from eight sheet rows. The rows are one list near the bottom of the code.

The rows live in one list in the code, so replacing the numbers means replacing those lines. The totals, bars and filters rebuild themselves.

Make it with AI

You do not have to write the page. Copy the results from your sheet and paste them into ChatGPT, Claude or Gemini with this prompt.

Make one HTML page from the table below. Show a title and an
"As of" date at the top, three summary tiles (total, row count,
largest row), filter buttons for the Region column, and a table
whose columns sort when I click the heading. On a phone, show each
row as a small card. Put the rows in one JavaScript list near the
bottom, one line per row. Large text. No outside libraries, images
or fonts. Do not save anything.

[paste your cells here]

Then check the page before you send it:

  • The totals. Compare the total and two or three rows with the sheet. An AI can mis-add or drop a row without saying so.
  • The formatting. Percentages, currencies and dates can change shape when they are pasted. Look at each column.
  • What came along. Make sure no column you meant to keep private is on the page.
  • Phone width. Make the browser window narrow and look at the cards.
  1. Copy all of the code the AI returned.
  2. Paste it into a NOS document. The page renders and its scripts run, so test the filters yourself.
  3. Choose Create share link and copy the address. HTML to link walks through it.
  4. Send that address by email, chat or a QR code.
From your sheet to one link, in four steps.
From your sheet to one link, in four steps.

Anyone with the link can open the page in a browser, on a phone or a laptop, without an account. They cannot edit it.

If someone sees code instead of a page, they were probably sent the file. See opening an HTML file on a phone.

For a printed report, put the link in a QR code. QR code link explains why a printed code keeps working when the page behind it changes.

A link to the sheet compared with a link to a page.
A link to the sheet compared with a link to a page.

Ways to show a sheet to other people

Google Sheets has its own options, and each fits a different job. This is how they compare. As of October 2026, check Google's help pages for the current menus.

Share the sheet Publish to web Download a copy Page made with AI
What people get Access to the file A read-only view A file A designed page
Updates Live Automatic by default A new download You paste new numbers
Layout Sheet grid Sheet grid Sheet grid Whatever you ask for
Good for Working together A quick public view Archives, Excel users A report people read

Google's help page for Publish to web says people with view permission can see charts, cell formatting and cell values but cannot view or edit formulas.

It also warns that publishing a chart lets people see the data behind it, and to be careful with private information.

The same page says you can publish the whole spreadsheet or individual sheets. Whether you can publish to everyone may depend on your account settings.

Publish to web is the quicker choice when the sheet grid is fine and you want it to follow your edits. A page made with AI is better when you want summary tiles, a layout for phones, or only some of the cells shown.

When the numbers change

A page made this way is a snapshot. Your NOS page cannot reach out to the sheet, because requests from a page to other sites are blocked. It shows what you pasted.

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>Budget check</title>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; font-family: system-ui, sans-serif; background: #f6f5f2; color: #20242c; line-height: 1.45; }
  .wrap { max-width: 640px; margin: 0 auto; padding: 18px 16px 24px; }
  h1 { margin: 0 0 2px; font-size: 22px; }
  .sub { margin: 0 0 14px; font-size: 14px; color: #5b6472; }
  .sum { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; }
  .sum div { flex: 1 1 140px; background: #fff; border-radius: 12px; padding: 10px 14px; box-shadow: 0 1px 5px rgba(0,0,0,.07); }
  .sum span { display: block; font-size: 12px; font-weight: 700; color: #5b6472; text-transform: uppercase; letter-spacing: .03em; }
  .sum b { font-size: 20px; }
  label.sw { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; font-size: 15px; min-height: 44px; cursor: pointer; }
  label.sw input { width: 20px; height: 20px; }
  .item { background: #fff; border-radius: 12px; padding: 12px 14px; margin-bottom: 10px; box-shadow: 0 1px 5px rgba(0,0,0,.07); }
  .top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; flex-wrap: wrap; }
  .top b { font-size: 16px; }
  .top span { font-size: 14px; color: #5b6472; }
  .track { height: 12px; background: #e8e6e0; border-radius: 6px; overflow: hidden; margin: 8px 0 6px; }
  .track i { display: block; height: 100%; background: #2f8f5b; }
  .item.over .track i { background: #d1621b; }
  .flag { font-size: 14px; font-weight: 700; }
  .ok .flag { color: #0f6b3a; }
  .over .flag { color: #b4470c; }
</style>
</head>
<body>
<div class="wrap">
  <h1>Team budget: spent so far</h1>
  <p class="sub">Operations (fictional) &middot; Copied from the sheet on 30 September 2026</p>
  <div class="sum" id="sum"></div>
  <label class="sw"><input type="checkbox" id="onlyOver"> Show only lines over budget</label>
  <div id="list"></div>
</div>

<script>
  // One line per row of the sheet: name, budget, spent.
  const LINES = [
    { name: 'Software licences', budget: 12000, spent: 11400 },
    { name: 'Travel',            budget: 8000,  spent: 9350 },
    { name: 'Training',          budget: 6500,  spent: 3100 },
    { name: 'Office supplies',   budget: 2400,  spent: 2710 },
    { name: 'Events',            budget: 9000,  spent: 8200 }
  ];

  const money = (n) => '$' + n.toLocaleString('en-US');
  const list = document.getElementById('list');
  const sum = document.getElementById('sum');
  const onlyOver = document.getElementById('onlyOver');

  function box(label, value) {
    const d = document.createElement('div');
    const s = document.createElement('span'); s.textContent = label;
    const b = document.createElement('b'); b.textContent = value;
    d.append(s, b);
    return d;
  }

  const budget = LINES.reduce((s, l) => s + l.budget, 0);
  const spent = LINES.reduce((s, l) => s + l.spent, 0);
  sum.append(box('Budget', money(budget)), box('Spent', money(spent)), box('Lines over', String(LINES.filter((l) => l.spent > l.budget).length)));

  function render() {
    list.innerHTML = '';
    LINES.filter((l) => !onlyOver.checked || l.spent > l.budget).forEach((l) => {
      const over = l.spent > l.budget;
      const pct = Math.round((l.spent / l.budget) * 100);
      const item = document.createElement('div');
      item.className = 'item ' + (over ? 'over' : 'ok');
      item.innerHTML = '<div class="top"><b></b><span></span></div><div class="track"><i></i></div><div class="flag"></div>';
      item.querySelector('b').textContent = l.name;
      item.querySelector('span').textContent = money(l.spent) + ' of ' + money(l.budget);
      item.querySelector('i').style.width = Math.min(pct, 100) + '%';
      item.querySelector('.flag').textContent = over
        ? 'Over by ' + money(l.spent - l.budget) + ' (' + pct + '%)'
        : money(l.budget - l.spent) + ' left (' + pct + '% used)';
      list.appendChild(item);
    });
  }

  onlyOver.addEventListener('change', render);
  render();
</script>
</body>
</html>
A second page: budget against spent, with a switch for lines over budget.

To refresh it, copy the new cells, paste them into the chat, and ask: "Replace the rows in this page with these, and change the As of date."

Paste the new code into the same NOS document. The link you already sent shows the new page.

Show the date at the top, as both examples do. A reader who finds the link in an old email can then see how old the numbers are.

If you only need a plain table from exported data, HTML table from CSV covers that. For why a page beats a workbook, see share spreadsheet data without the file.

What a viewer sees when the link is opened on a phone.
What a viewer sees when the link is opened on a phone.

When it does not work

Symptom Cause Fix
Viewers see code, not a page The file was sent as an attachment and opened as text Send the link, not the file
The total is wrong The AI mis-added or dropped a row Compare with the sheet and ask it to fix the list
A date or percentage looks odd The format changed when the cells were pasted Fix that line in the code, or ask the AI to format the column
The page is out of date It holds a copy, not a live connection Paste the new numbers and keep the same link
Charts or logos are missing They come from another website, which the page blocks Ask for a version with no outside images
You want people to enter data The page saves nothing from visitors Collect it in your sheet or another tool and show the results here

For a wider look at the same idea, share a spreadsheet as a link covers numbers that readers can sort, and share AI-generated HTML explains what to check before sending any page an AI wrote.

Questions people ask

Will the page update when I change the sheet?

No. The page holds a copy of the numbers you pasted into it, so it shows the sheet as it was that day. To refresh it, copy the new cells, have the AI replace the rows, and paste the new code into the same NOS document. The link stays the same. Put the date on the page so readers know how fresh it is.

Is this different from Publish to web in Google Sheets?

Yes. Publish to web is Google's own feature: it gives you an address for a read-only view of the sheet and, by default, republishes when the sheet changes. A page made with AI is a separate document with the layout you chose, and it only changes when you paste new code. Both are described in the table above.

Do my viewers need a Google account?

Not for the page. It opens in a browser like any web page, with no sign-in. For the sheet itself it depends on its sharing setting: Google says that with Anyone with the link, people can use the file without signing in, while Restricted limits it to people with access.

Can viewers sort or filter the numbers?

Yes, if you ask for it. A page can have filter buttons and sortable columns, as in the first example. What they do on the page is not saved, and it does not change your sheet.

What should I leave out of the page?

Anything the readers should not see: private columns, staff names, costs and notes. Copy only the cells you mean to show, and read the finished page once before you send it. Whatever is on the page is visible to everyone who has the link.

Keep reading