Paste a block of cells from Excel into a web app's text field and something interesting happens: the clipboard carries a tab-separated values (TSV) string in text/plain, a full HTML table in text/html, and a proprietary Excel format in a custom application/* MIME type. Which one your app reads determines whether "paste spreadsheet data here" actually works.
This article walks through building a reliable spreadsheet paste handler: what is on the clipboard for each source, how to parse TSV defensively (including the gotchas that break naive splits), and when the HTML representation is worth the trouble.
What the clipboard actually contains
When you copy cells from Excel:
text/plainis tab-separated rows, with rows joined by\r\n. A cell containing a newline is wrapped in double quotes and the internal newline is preserved as\n.text/htmlis a full<table>with cell styles, merged-cell attributes, and Excel-specific metadata.application/x-officedrawand several otherapplication/*keys carry the native Excel data — usually ignored by web apps.
Google Sheets is very similar but with Google-specific custom MIME types instead of Microsoft ones. Apple Numbers produces TSV and HTML but no useful custom types.
Start with text/plain for simple imports
For a "paste values into this grid" feature, text/plain is the right source of truth. The parse is a split on rows and columns:
const tsv = event.clipboardData.getData('text/plain');
const rows = tsv.split(/\r?\n/).map(line => line.split('\t'));
This handles the happy path. It breaks the moment a cell contains a newline or a tab — both of which happen in real data.
Handling quoted cells
Excel quotes any cell that contains a tab, a newline, or a double-quote character. Inside a quoted cell, double-quotes are escaped by doubling: a "b" c becomes "a ""b"" c". A correct TSV parser walks character by character, tracking whether it is inside a quoted cell:
function parseTsv(tsv) {
const rows = [[]];
let cell = '';
let inQuotes = false;
for (let i = 0; i < tsv.length; i++) {
const c = tsv[i];
if (inQuotes) {
if (c === '"' && tsv[i + 1] === '"') { cell += '"'; i++; }
else if (c === '"') inQuotes = false;
else cell += c;
} else if (c === '"') inQuotes = true;
else if (c === '\t') { rows.at(-1).push(cell); cell = ''; }
else if (c === '\n') { rows.at(-1).push(cell); rows.push([]); cell = ''; }
else if (c !== '\r') cell += c;
}
rows.at(-1).push(cell);
return rows.filter(r => r.length > 1 || r[0] !== '');
}
This 15-line parser handles the common Excel edge cases. Battle-tested alternatives include papaparse (configured with delimiter: '\t'), which also handles variations in line-ending conventions and malformed quoting.
Number parsing and localisation
A cell that Excel displays as 1,234.56 comes through as the string 1,234.56 — the display format is not carried over. Worse, in locales that use comma as the decimal separator, the same value is 1.234,56. If your app expects numbers, parse them explicitly:
- Detect the locale by inspecting the characters: a comma-only string is almost certainly a decimal comma; a period-and-comma mix follows one convention or the other based on which comes later.
- Expose a "parse as" dropdown (US, European, plain) if your users span multiple locales.
- Reject cells that do not parse to a number, rather than silently inserting
NaN.
Dates are worse
Excel displays dates as localised strings, and that is exactly what the clipboard carries. The ISO date 2026-04-18 never appears unless the spreadsheet author formatted the cell that way. Depending on the user's Excel locale, you will see 4/18/2026, 18/04/2026, 2026/04/18, or 18.04.26. Do not assume — either ask the user to confirm the format or read the text/html representation, which sometimes embeds an ISO-formatted data-excel-date attribute.
When to fall back to text/html
The HTML representation is useful for two things the TSV cannot express:
- Merged cells. Excel's HTML uses
rowspanandcolspanattributes. The TSV collapses merged cells into the first row, leaving blank cells in the other rows. - Cell formatting. If you need to preserve bold, colour, or fonts, only the HTML carries them.
Parsing HTML is heavier. Use the browser's DOMParser rather than regex:
const doc = new DOMParser().parseFromString(html, 'text/html');
const table = doc.querySelector('table');
for (const tr of table.querySelectorAll('tr')) { /* ... */ }
Strip Excel-specific styles if you are importing into your own table schema. Office's HTML contains hundreds of mso-* CSS properties that serve no purpose outside Excel and make the document enormous.
Google Sheets specifics
Sheets produces cleaner HTML than Excel but adds Google-specific attributes like data-sheets-value on each cell, which carries a JSON representation of the typed value. For apps that need rich imports, parsing this attribute gives you the exact typed value Sheets had, without re-parsing the display string.
Apple Numbers and the one-cell problem
Numbers on macOS has a specific quirk: a single-cell copy produces no table-shaped clipboard at all, just a text/plain string with the cell's value. Your parser should treat a no-tab, no-newline string as a single 1x1 cell, not as an error.
Rendering a preview before committing
Pasting into a grid is a destructive-feeling action. Before overwriting the user's current data, render a preview of what the parsed values will produce and give them a confirm button. This also lets them see header-row ambiguity — Excel does not mark header rows, so your app will not know whether the first row is headers or data.
A consolidated flow
Pulling the pieces together:
- On paste, read both
text/plainandtext/html. - If the HTML contains a
<table>, prefer it if the user's app needs formatting or merged cells; otherwise prefer the TSV. - Parse defensively with a quote-aware TSV parser or a DOM-based HTML parser.
- Apply number and date normalisation based on the detected or user-selected locale.
- Render a preview and confirm before committing.
Inspecting what Excel really sent
When a paste does not produce what you expected, open Clipboard Inspector and paste the same cells. Every MIME type is shown with its raw content — you can see the exact TSV string, the full HTML with Excel's mso-* noise, and the custom Office types. This is the fastest way to diagnose whether the problem is in the clipboard data or in your parser. For a broader field guide, see clipboard MIME types explained.