Paste from Excel: formats and formulas survive
Excel puts an HTML document on the clipboard beside the tab-separated text: formats in a style block keyed by class, formulas in x:fmla, raw numbers in x:num. The sheet reads it, so a pasted block arrives bold, filled, with its number formats, and with its formulas moved to where they landed; Google Sheets' data-sheets-formula flavour reads the same. Two buttons put exactly what Excel and Sheets put on the clipboard; click a cell and Ctrl+V. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Spreadsheet). See the SvGrid documentation for the full API.
About this example
Paste from Excel into the Svelte 5 spreadsheet shell and keep the formatting: Excel puts an HTML document on the clipboard beside the tab-separated text, with the formats in a style block keyed by class, the formulas in x:fmla and the raw numbers in x:num, and the sheet reads it, so a pasted block arrives bold, filled, with its currency and percent formats and with its formulas moved to the cells they landed on; Google Sheets' inline styles and data-sheets-formula (R1C1) read the same way. Two buttons put on the clipboard exactly what Excel and Sheets put there, for a browser with no Excel in it; a copy out of the sheet writes both flavours back. Enterprise, in @svgrid/enterprise.
Excel puts two things on the clipboard when you copy a block: the tab separated text every grid takes, and an HTML document with the formats in a style block keyed by class, the formulas in x:fmla and the raw numbers in x:num. The sheet reads the second one, so a paste from Excel arrives with its bold, its fills, its number formats and its formulas, moved to where they landed; Google Sheets' flavour (data-sheets-formula) reads the same way. Copying OUT writes both flavours, so a block pasted into Excel keeps its formats too.
There is no Excel in the browser, so the buttons here put exactly what Excel puts on the clipboard: the same markup, byte for byte the parts that matter. Click one, click a cell, press Ctrl+V. Then open Excel or Sheets, copy a few cells of your own and paste them: the result is the same.
Try: "Copy an Excel block", click B3, Ctrl+V. The header is bold on a yellow fill, the prices read $1,234.50, the Total column is a formula (click one and read the bar) that has been moved to the rows it landed on. Paste Special > Values drops the formulas; Formats drops the text.
Imports, features and API used
Imports: @svgrid/enterprise
Frequently asked questions
How do the formulas end up in the right cells?
Excel's HTML does not say where a block was copied from, so the sheet checks whether the formulas are self-contained: when every reference falls inside the block read as if copied from A1 (a totals column, a running sum), they are kept and moved with the paste; one that reaches outside the block is pasted as its value instead. Sheets writes R1C1, which is resolved against the cell it lands in.
Why does 1,234.50 paste as a number?
Excel writes the raw number in x:num beside the display text, and the sheet takes the number and the mso-number-format, so the cell holds 1234.5 and shows $1,234.50, the way it did in Excel.
Does copying out of the sheet work in Excel?
Yes. A copy writes text/plain and text/html together, the HTML with inline mso-number-format, bold, colours and the formula in x:fmla, so a paste into Excel keeps the look and the numbers.
Related documentation
Related articles
- Paste from Excel into a Svelte Data Grid - How to wire up clipboard paste so users can drop a copied Excel or Google Sheets block directly into SvGrid - TSV parsing, type coercion, validation, and row growth all covered.
- How We Built Excel-Style Filters - How SvGrid implements per-column filter menus, type-aware operators, and a shared filter model that works identically for in-memory and server-side data.
- A Fill Handle (Drag to Fill) in SvGrid - Build a working spreadsheet-style fill handle on top of SvGrid's cell selection and editing - pointer tracking, range highlighting, series fill, and undo/redo integration all covered.
Source code (466-paste-from-excel.svelte)
<script lang="ts">
/**
* 466. Paste from Excel: formats and formulas survive
* ---------------------------------------------------
* Excel puts two things on the clipboard when you copy a block: the tab
* separated text every grid takes, and an HTML document with the formats
* in a style block keyed by class, the formulas in x:fmla and the raw
* numbers in x:num. The sheet reads the second one, so a paste from Excel
* arrives with its bold, its fills, its number formats and its formulas,
* moved to where they landed; Google Sheets' flavour (data-sheets-formula)
* reads the same way. Copying OUT writes both flavours, so a block pasted
* into Excel keeps its formats too.
*
* There is no Excel in the browser, so the buttons here put exactly what
* Excel puts on the clipboard: the same markup, byte for byte the parts
* that matter. Click one, click a cell, press Ctrl+V. Then open Excel or
* Sheets, copy a few cells of your own and paste them: the result is the
* same.
*
* Try: "Copy an Excel block", click B3, Ctrl+V. The header is bold on a
* yellow fill, the prices read $1,234.50, the Total column is a formula
* (click one and read the bar) that has been moved to the rows it landed
* on. Paste Special > Values drops the formulas; Formats drops the text.
*/
import { SvSheet, createWorkbook, type CellFormatEntry } from '@svgrid/enterprise'
const wb = createWorkbook([{
name: 'Sheet1',
cells: [
['A paste target. Click a cell, then Ctrl+V.'],
],
}])
const formats: Record<string, CellFormatEntry> = { A1: { italic: true, color: '#64748b' } }
// ---- what Excel for Windows puts on the clipboard -------------------------
// Trimmed to the parts that matter: the style block, the classes on the
// cells, x:num for the raw number, x:fmla for the formula, the DISPLAY text
// as the cell's content. Excel's own dump is longer, not different. The
// style tags are spelled in two halves: a literal one inside a script is
// taken for the component's own style block by the Svelte preprocessor.
const STYLE = ['<', 'style>'].join('')
const END_STYLE = ['</', 'style>'].join('')
const EXCEL_HTML = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">
<head>${STYLE}
<!--table {mso-displayed-decimal-separator:"\\.";}
.xl65 {font-weight:700;background:#FFEB9C;color:#7F6000;text-align:center;border:.5pt solid #BFBFBF;}
.xl66 {mso-number-format:"\\$\\#\\,\\#\\#0\\.00";text-align:right;}
.xl67 {mso-number-format:"0";text-align:right;}
.xl68 {mso-number-format:"\\$\\#\\,\\#\\#0\\.00";font-weight:700;color:#006100;background:#C6EFCE;text-align:right;}
.xl69 {font-style:italic;color:#595959;}
-->
${END_STYLE}</head><body>
<table border=0 cellpadding=0 cellspacing=0>
<tr><td class=xl65>Item</td><td class=xl65>Unit price</td><td class=xl65>Qty</td><td class=xl65>Total</td></tr>
<tr><td>Desk lamp</td><td class=xl66 x:num="1234.5">$1,234.50</td><td class=xl67 x:num="3">3</td><td class=xl68 x:num="3703.5" x:fmla="=B2*C2">$3,703.50</td></tr>
<tr><td>Monitor arm</td><td class=xl66 x:num="89.99">$89.99</td><td class=xl67 x:num="12">12</td><td class=xl68 x:num="1079.88" x:fmla="=B3*C3">$1,079.88</td></tr>
<tr><td>Cable tray</td><td class=xl66 x:num="17.25">$17.25</td><td class=xl67 x:num="40">40</td><td class=xl68 x:num="690" x:fmla="=B4*C4">$690.00</td></tr>
<tr><td class=xl69>Prices exclude tax</td><td></td><td></td><td class=xl68 x:num="5473.38" x:fmla="=SUM(D2:D4)">$5,473.38</td></tr>
</table></body></html>`
const EXCEL_TEXT = [
'Item\tUnit price\tQty\tTotal',
'Desk lamp\t$1,234.50\t3\t$3,703.50',
'Monitor arm\t$89.99\t12\t$1,079.88',
'Cable tray\t$17.25\t40\t$690.00',
'Prices exclude tax\t\t\t$5,473.38',
].join('\n')
// ---- and Google Sheets' -----------------------------------------------------
// Sheets writes inline styles and carries the formula in data-sheets-formula
// and the typed value in data-sheets-value, a JSON object keyed by type.
const SHEETS_HTML = `<meta charset="utf-8"><google-sheets-html-origin><table><tbody>
<tr><td style="font-weight:bold;background-color:#d9ead3;">Region</td><td style="font-weight:bold;background-color:#d9ead3;">Target</td><td style="font-weight:bold;background-color:#d9ead3;">Actual</td><td style="font-weight:bold;background-color:#d9ead3;">Attainment</td></tr>
<tr><td>North</td><td data-sheets-value="{"1":3,"3":150000}" style="text-align:right;">150,000</td><td data-sheets-value="{"1":3,"3":155700}" style="text-align:right;">155,700</td><td data-sheets-value="{"1":3,"3":1.038}" data-sheets-formula="=R[0]C[-1]/R[0]C[-2]" data-sheets-numberformat="{"1":2,"2":"0.0%"}" style="text-align:right;color:#38761d;">103.8%</td></tr>
<tr><td>South</td><td data-sheets-value="{"1":3,"3":120000}" style="text-align:right;">120,000</td><td data-sheets-value="{"1":3,"3":104700}" style="text-align:right;">104,700</td><td data-sheets-value="{"1":3,"3":0.8725}" data-sheets-formula="=R[0]C[-1]/R[0]C[-2]" data-sheets-numberformat="{"1":2,"2":"0.0%"}" style="text-align:right;color:#cc0000;">87.3%</td></tr>
</tbody></table></google-sheets-html-origin>`
const SHEETS_TEXT = ['Region\tTarget\tActual\tAttainment', 'North\t150,000\t155,700\t103.8%', 'South\t120,000\t104,700\t87.3%'].join('\n')
let status = $state('')
async function copy(html: string, text: string, what: string) {
try {
// Both flavours at once, the way Excel does it: a plain text for the
// grids that read only that, the HTML for the ones that read more.
await navigator.clipboard.write([
new ClipboardItem({ 'text/html': new Blob([html], { type: 'text/html' }), 'text/plain': new Blob([text], { type: 'text/plain' }) }),
])
status = `${what} is on the clipboard. Click a cell and press Ctrl+V.`
} catch {
status = 'The browser refused the clipboard (it needs a secure context and a click). Copy from Excel itself instead.'
}
}
</script>
<section class="wrap flex flex-col flex-1 min-h-0">
<div class="bar">
<button type="button" class="btn" onclick={() => copy(EXCEL_HTML, EXCEL_TEXT, 'An Excel block')}>Copy an Excel block</button>
<button type="button" class="btn" onclick={() => copy(SHEETS_HTML, SHEETS_TEXT, 'A Google Sheets block')}>Copy a Google Sheets block</button>
<span class="status">{status}</span>
</div>
<SvSheet workbook={wb} height="100%" rows={20} columns={9} columnWidths={{ A: 170 }} {formats} />
<p class="note shrink-0">
The buttons put on the clipboard what Excel and Google Sheets put there:
the formats in a <code><style></code> block or inline, the formulas
in <code>x:fmla</code> or <code>data-sheets-formula</code>, the raw
numbers beside the display text. Paste into the sheet and the block
arrives formatted, its formulas moved to the cells they landed on. A
copy from the sheet writes the same two flavours back.
</p>
</section>
<style>
.wrap { gap: 8px; }
.bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
padding: 6px 10px;
font-size: 12px;
color: var(--sg-fg, #0f172a);
background: var(--sg-header-bg, #f1f5f9);
border: 1px solid var(--sg-border, #e2e8f0);
border-radius: 6px;
}
.btn {
padding: 4px 10px;
font: inherit;
font-size: 12px;
color: var(--sg-fg, #0f172a);
background: var(--sg-bg, #fff);
border: 1px solid var(--sg-border, #cbd5e1);
border-radius: 4px;
cursor: pointer;
}
.btn:hover { background: var(--sg-row-hover-bg, #f8fafc); }
.status { color: var(--sg-muted, #64748b); }
.note { margin: 0; font-size: 13px; line-height: 1.6; color: var(--sg-muted, #64748b); }
.note code {
font-family: ui-monospace, Menlo, monospace;
font-size: 12px;
padding: 1px 4px;
border-radius: 4px;
background: var(--sg-header-bg, #f1f5f9);
color: var(--sg-fg, #0f172a);
}
</style>More Spreadsheet examples
- Spreadsheet + Ribbon bar - The whole Excel surface as one component, <SvSheet workbook={wb} />: a six-tab ribbon (Home, Insert, Formulas, Data, Review, View), the Name Box and fx bar, sheet tabs and the Sum / Average / Count status bar. Format cells, merge them, comment on them, validate what goes in, colour them by rule, filter the region, protect the sheet; every button is the same call as its shortcut, so Ctrl+B and the Bold button cannot drift. A two-sheet P&L with the formats travelling in the document.
- Spreadsheet + formulas - Real formula engine inside the grid: cell refs (A1), ranges (A1:A10), SUM / AVG / IF / COUNTIF / ROUND, arithmetic, string concat, cycle detection.
- Per-cell custom borders (KPI) - Editable KPI scorecard. spreadsheetLayout paints spreadsheet-style per-edge custom borders via an absolute-positioned overlay (no border-collapse conflicts). Edit any quarter or target - the borders re-derive: green double = beat target, blue solid = hit, amber dotted = near miss, red dashed = bad miss; row champion gets a colored full frame.
- Cell merging (spreadsheet shell) - A real invoice rendered on an Excel-style shell: A / B / C / D / E column letters across the top, row numbers down the left. Brand band, bill-from / bill-to address blocks, meta block, line items, totals, notes, signatures - all assembled from MergeSpec + CellBorderSpec. Editable Qty / Rate / addresses / notes; totals recompute live.
- HyperFormula integration - Full HyperFormula engine wired into the grid as a peer-optional dep. Editable spreadsheet with A1-style cell refs, dozens of formulas across math (SUM / SUMIF), lookup (VLOOKUP / INDEX-MATCH), text (CONCAT / UPPER), date (TODAY / DATEDIF), logical (IF nests), financial (PMT / IRR / NPV), statistical (AVERAGE / MAX / RANK).