Copy a range in Excel, Google Sheets or Numbers,
paste it here, and get a real <table>. Take it
away as clean HTML for a page, as Markdown for a README, or as a
bordered table that keeps its layout when pasted into an email. You
skip the file export and the Office CSS that Excel's Save as Web
Page adds to every cell.
Region→Q1→Q2→Owner→Notes
North→12,400→13,150→Priya→On track
South→9,800→8,900→Marco→"Missed Q2:
supplier delay" <table>
<thead>
<tr>
<th>Region</th>
<th>Q1</th>
<th>Q2</th>
<th>Owner</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>North</td>
<td>12,400</td>
<td>13,150</td>
<td>Priya</td>
<td>On track</td>
</tr>
...
</tbody>
</table> The Marco row has a line break inside one cell (Alt+Enter in Excel). The spreadsheet wraps that cell in quotes on the clipboard, and it comes out as one cell, not two rows.
Spreadsheets put the formatted value on the clipboard, so 12,400, 15% and 03/04/2026 arrive exactly as you saw them. Formulas are not copied, only their results.
Colors, bold, number alignment and column widths are dropped. The HTML is plain markup that picks up your own CSS, which suits most websites.
A merged range is copied as its value followed by empty cells. The output has no colspan or rowspan, so add those by hand if you need them.
Whether hidden or filtered-out rows end up on the clipboard depends on the app and on how they were hidden. Be sure to check the row count before you publish the table.
Copy HTML gives indented
<table>, <thead> and
<tbody> markup with escaped cell text and no
classes. Copy Markdown gives a GitHub pipe
table. Copy for Email puts a rich-text table
with inline borders and a shaded header row on the clipboard,
for Gmail, Outlook or Google Docs. That pastes more cleanly
than the spreadsheet selection itself, which can bring the
sheet's fonts and column widths along.
Untick First row is header when the selection
has no heading row: the HTML then has no
<thead>, and the Markdown copy invents
Column 1, Column 2 headers because Markdown tables require one.
// Read what the user pasted, e.g. from a paste event or navigator.clipboard.readText()
document.querySelector("#paste").addEventListener("paste", (e) => {
const text = e.clipboardData.getData("text/plain");
const rows = parseSheetClipboard(text);
e.preventDefault();
document.querySelector("#out").innerHTML = toHtmlTable(rows, true);
});
// Tabs between cells, newlines between rows. Cells with a line break are "quoted", "" is a quote.
function parseSheetClipboard(text) {
const rows = [];
let row = [], field = "", q = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (q) {
if (c === '"' && text[i + 1] === '"') { field += '"'; i++; }
else if (c === '"') q = false;
else field += c;
} else if (c === '"' && field === "") q = true;
else if (c === "\t") { row.push(field); field = ""; }
else if (c === "\n") { row.push(field); rows.push(row); row = []; field = ""; }
else if (c !== "\r") field += c;
}
if (field !== "" || row.length) { row.push(field); rows.push(row); }
return rows;
}
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
function toHtmlTable(rows, header) {
const [first, ...rest] = rows;
const head = header ? `<thead><tr>${first.map((c) => `<th>${esc(c)}</th>`).join("")}</tr></thead>` : "";
const body = (header ? rest : rows)
.map((r) => `<tr>${r.map((c) => `<td>${esc(c)}</td>`).join("")}</tr>`).join("");
return `<table>${head}<tbody>${body}</tbody></table>`;
} A quote only opens a quoted cell at the start of a field, so a value like 5" screen in the middle of a row is kept literally.
import csv
import html
import io
# Text copied from Excel or Google Sheets, e.g. via pyperclip.paste()
pasted = 'Region\tQ1\tNotes\r\nNorth\t12,400\tOn track\r\nSouth\t9,800\t"Missed:\nsupplier"\r\n'
# The clipboard format is tab-separated with CSV-style quoting, so the csv module reads it
rows = list(csv.reader(io.StringIO(pasted, newline=""), delimiter="\t"))
head, body = rows[0], rows[1:]
out = ["<table>", " <thead><tr>" + "".join(f"<th>{html.escape(c)}</th>" for c in head) + "</tr></thead>", " <tbody>"]
for r in body:
out.append(" <tr>" + "".join(f"<td>{html.escape(c)}</td>" for c in r) + "</tr>")
out += [" </tbody>", "</table>"]
print("\n".join(out)) html.escape also escapes quotes by default, which keeps the output safe inside attributes as well.
This tool follows the specifications below. Read them when you need the exact normative wording, for example to settle an edge case between two implementations.
Select the range in Excel, press Ctrl+C (Cmd+C on a Mac), click into the Pasted Cells pane and paste. Click Copy HTML to get the <table> markup.
Yes. Excel, Google Sheets and Numbers all put tab-separated text on the clipboard, which is what the tool reads. LibreOffice Calc works the same way.
No. The clipboard text holds the displayed values only, so you get the numbers as formatted in the sheet but not colors, bold, borders or formulas. The HTML is plain markup that uses your own CSS.
Excel wraps a cell containing a line break in double quotes when it copies, and the tool reads it back as a single cell with the break intact.
Click Copy for Email and paste into the message body in Gmail or Outlook. It copies rich text with inline borders and a shaded header row.
Copy HTML when you will put the markup into a page, a CMS code view or an email template that has its own CSS: it is clean <table> code with no styles. Copy for Email when you will paste into a visual editor such as Gmail, Outlook, Google Docs, Word or Notion. That copy is a rendered table with the borders and header shading inlined, so it looks right straight away.
Yes. Paste the range and click Copy Markdown to get a GitHub-flavored pipe table, ready for a README, a pull request description, a wiki page or Notion. Cells with line breaks become <br> so the row stays intact, and if First row is header is unticked the tool invents Column 1, Column 2 headers, because Markdown tables cannot exist without a header row.
If you have the file instead of a selection, export it as CSV and use CSV to Table, or turn the workbook into data with Excel to JSON. For a Markdown table with alignment options, use CSV to Markdown Table (pick Tab as the delimiter for pasted cells).