Export - Cell images
Product grid with thumbnail column. On xlsx export each thumbnail is embedded as a real picture cell. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Data Export & Import). See the SvGrid documentation for the full API.
About this example
Cell images in an Excel export from the Svelte 5 data grid. A products grid has a thumbnail column holding a data URL or http URL, rendered in the grid by a cell snippet; exportData({ imageFields: ['thumbnail'] }) tells the xlsx writer to treat that field as an embedded picture rather than text, so the exporter pulls each image into the workbook's media directory and anchors it in the cell.
A products grid with a thumbnail column. On export to xlsx the thumbnails are embedded as real image cells, not as URL strings.
Two pieces make this work:
1. The "thumbnail" data column holds a data-URL or http(s) URL pointing at an image. We render it in the grid via a custom cell snippet.
2. pro.exportData({ imageFields: ['thumbnail'] }) tells the xlsx writer to treat that field as an embedded image rather than text. The exporter pulls the image into the workbook's _media directory and references it from the cell.
Imports, features and API used
Imports: svelte, @svgrid/grid, @svgrid/enterprise
Table features registered: rowSortingFeature
Columns: thumbnail (Thumb), sku (SKU), name (Name), category (Category), price (Price), inStock (In stock), thumbnail (Thumb), sku (SKU), name (Name), category (Category), price (Price), inStock (In stock)
SvGridApi methods called: api.exportData()
Frequently asked questions
What can the image field contain?
A data URL or an http or https URL to a PNG or JPEG. Remote images are fetched at export time, so they must be reachable from the browser with CORS allowed.
How are the images placed in Excel?
Each picture is embedded in the workbook media and anchored to its cell, so the column reads as a thumbnail strip in the sheet rather than a list of URLs.
What happens in CSV export?
CSV has no images, so the field exports as its URL text. Only the xlsx writer honours imageFields.
Related articles
- What's New in SvGrid Enterprise - Export, Pivot, Import, Print, and AI - A practical look at @svgrid/enterprise - pivot tables, Excel and PDF export, data import, printing, and an AI assistant. Covers when each feature earns its place and what to watch out for in production.
- Sparkline Cells in a Svelte Data Grid - Show inline trend sparklines inside grid cells using SvGrid's built-in sparkline column property - no charting library needed, just a field that holds a number array.
- Progress and Percentage Bar Cells in SvGrid - Build in-cell progress bars in your Svelte 5 data grid - with color thresholds, accessible markup, and sorting that still works.
Source code (58-export-with-images.svelte)
<script lang="ts">
/**
* 58. Export - grid with images (Pro)
* ----------------------------------
* A products grid with a thumbnail column. On export to xlsx the
* thumbnails are embedded as real image cells, not as URL strings.
*
* Two pieces make this work:
*
* 1. The "thumbnail" data column holds a data-URL or http(s) URL
* pointing at an image. We render it in the grid via a custom
* cell snippet.
*
* 2. `pro.exportData({ imageFields: ['thumbnail'] })` tells the
* xlsx writer to treat that field as an embedded image rather
* than text. The exporter pulls the image into the workbook's
* `_media` directory and references it from the cell.
*/
import { onMount } from 'svelte'
import {
SvGrid,
tableFeatures,
rowSortingFeature,
renderSnippet,
type ColumnDef,
} from '@svgrid/grid'
import {
installEnterprise,
setLicenseKey,
type EnterpriseGridApi,
} from '@svgrid/enterprise'
setLicenseKey('SVENTERPRISE-DEV-DEMO')
type Product = {
id: string
sku: string
name: string
category: string
/** Data-URL or http(s) URL. */
thumbnail: string
price: number
inStock: number
}
// Small SVG-based thumbnails for on-screen display. Excel can't embed
// SVG reliably, so a PNG variant is built per row in `onMount` and
// swapped in for the export.
function tileSvg(initials: string, hue: number): string {
const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>
<rect width='32' height='32' rx='6' fill='hsl(${hue} 70% 55%)'/>
<text x='16' y='21' text-anchor='middle' fill='#fff'
font-family='Inter, sans-serif' font-size='13' font-weight='800'>${initials}</text>
</svg>`
return `data:image/svg+xml;base64,${btoa(svg)}`
}
/** Paint an SVG dataUrl to a 64×64 canvas and read back a PNG dataUrl. */
async function svgToPng(svgDataUrl: string): Promise<string> {
const img = new Image()
img.src = svgDataUrl
await new Promise<void>((resolve) => { img.onload = () => resolve() })
const cv = document.createElement('canvas')
cv.width = 64; cv.height = 64
cv.getContext('2d')!.drawImage(img, 0, 0, 64, 64)
return cv.toDataURL('image/png')
}
const SEED: Array<Omit<Product, 'thumbnail'> & { initials: string; hue: number }> = [
{ id: 'P-001', sku: 'HW-1100', name: 'Steel sheet 1/2"', category: 'Hardware', initials: 'SS', hue: 10, price: 180.0, inStock: 240 },
{ id: 'P-002', sku: 'HW-2240', name: 'Stainless rivets', category: 'Hardware', initials: 'SR', hue: 220, price: 24.5, inStock: 6800 },
{ id: 'P-003', sku: 'TL-3018', name: 'Impact driver', category: 'Tools', initials: 'ID', hue: 140, price: 289.0, inStock: 72 },
{ id: 'P-004', sku: 'CN-7711', name: 'Drum, 55 gal', category: 'Containers', initials: 'DR', hue: 280, price: 165.0, inStock: 410 },
{ id: 'P-005', sku: 'EL-9000', name: 'Industrial PLC', category: 'Electrical', initials: 'PLC', hue: 200, price: 1420.0, inStock: 18 },
{ id: 'P-006', sku: 'EL-9012', name: 'I/O expansion', category: 'Electrical', initials: 'IO', hue: 190, price: 215.0, inStock: 92 },
{ id: 'P-007', sku: 'WR-1208', name: 'Wire rope 1/4"', category: 'Rigging', initials: 'WR', hue: 40, price: 92.0, inStock: 1200 },
{ id: 'P-008', sku: 'TL-1001', name: 'Torque wrench', category: 'Tools', initials: 'TW', hue: 130, price: 245.0, inStock: 56 },
{ id: 'P-009', sku: 'CN-5530', name: 'Hardwood pallet', category: 'Containers', initials: 'HP', hue: 30, price: 32.0, inStock: 980 },
{ id: 'P-010', sku: 'AB-0455', name: 'Aluminum bar 6 ft', category: 'Hardware', initials: 'AB', hue: 170, price: 48.2, inStock: 760 },
]
// On-screen rows use SVG (smaller, crisper). The PNG variants are built
// once on mount and used only at export time.
const rows: Product[] = SEED.map((r) => ({
id: r.id, sku: r.sku, name: r.name, category: r.category, price: r.price, inStock: r.inStock,
thumbnail: tileSvg(r.initials, r.hue),
}))
let pngById = $state<Record<string, string>>({})
onMount(async () => {
const entries = await Promise.all(SEED.map(async (r) =>
[r.id, await svgToPng(tileSvg(r.initials, r.hue))] as const,
))
pngById = Object.fromEntries(entries)
})
const features = tableFeatures({ rowSortingFeature })
let api = $state<EnterpriseGridApi<typeof features, Product> | null>(null)
let busy = $state(false)
const pngReady = $derived(Object.keys(pngById).length === SEED.length)
async function doExport() {
if (!api) return
// If PNG generation is still running, kick it now synchronously so
// the click always produces a working file (instead of falling back
// to SVG which Excel may not display).
if (!pngReady) {
const entries = await Promise.all(SEED.map(async (r) =>
[r.id, await svgToPng(tileSvg(r.initials, r.hue))] as const,
))
pngById = Object.fromEntries(entries)
}
busy = true
try {
// Swap the SVG thumbnails for PNG copies just for the exported rows;
// Excel embeds PNG/JPEG reliably but not SVG.
const exportRows = rows.map((r) => ({ ...r, thumbnail: pngById[r.id] ?? r.thumbnail }))
// Pass `columns` explicitly so the wrapper exports the thumbnail
// column (which uses `field` so it's auto-detected as image-bearing).
await api.exportData({
format: 'xlsx',
filename: 'products-with-images',
rows: exportRows,
columns: [
{ field: 'thumbnail', header: 'Thumb' },
{ field: 'sku', header: 'SKU' },
{ field: 'name', header: 'Name' },
{ field: 'category', header: 'Category' },
{ field: 'price', header: 'Price' },
{ field: 'inStock', header: 'In stock' },
],
imageFields: ['thumbnail'],
imageSize: { width: 48, height: 48 },
})
} catch (err) {
console.error('[export]', err)
} finally {
busy = false
}
}
</script>
{#snippet ThumbCell(props: { row: Product })}
<img src={props.row.thumbnail} alt={props.row.name} width="28" height="28"
style="border-radius: 6px; display: block;" />
{/snippet}
<section class="flex flex-col flex-1 min-h-0 gap-3">
<div class="flex flex-wrap items-center gap-3 text-sm shrink-0">
<button
type="button"
onclick={doExport}
disabled={busy}
class="ex-btn rounded border px-3 py-1 disabled:opacity-50"
>
{busy ? 'Exporting…' : '⬇ Export XLSX with images'}
</button>
<span class="ex-note">
Open the file - each row's thumbnail will be embedded in the Thumb column.
</span>
</div>
<div class="flex-1 min-h-0">
<SvGrid responsive={true}
columnResize
data={rows}
columns={[
{
field: 'thumbnail', header: 'Thumb', width: 80,
cell: (ctx) => renderSnippet(ThumbCell, { row: ctx.row.original }),
},
{ field: 'sku', header: 'SKU', editorType: 'text', width: 110 },
{ field: 'name', header: 'Name', editorType: 'text', width: 200 },
{ field: 'category', header: 'Category', editorType: 'text', width: 140 },
{
field: 'price', header: 'Price', editorType: 'number', width: 120,
format: { type: 'currency', currency: 'USD' },
},
{ field: 'inStock', header: 'In stock', editorType: 'number', width: 110 },
] satisfies ColumnDef<typeof features, Product>[]}
features={features}
filterMode="none"
selectionMode="cell"
showRowNumbers={true}
enableInlineEditing={false}
enableCellSelection={true}
rowHeight={44}
containerHeight="100%"
fitColumns={true}
onApiReady={(next) => (api = installEnterprise(next))}
/>
</div>
</section>
<style>
.ex-btn {
border-color: var(--sg-border, #cbd5e1);
background: var(--sg-bg, #fff);
color: var(--sg-fg, #0f172a);
cursor: pointer;
}
.ex-btn:hover:not(:disabled) { background: var(--sg-row-hover-bg, rgba(148, 163, 184, 0.12)); }
.ex-note { color: var(--sg-muted, #64748b); }
</style>More Data Export & Import examples
- Excel / CSV import - File picker + column mapping + per-row validation preview before commit. Reads xlsx / csv / tsv / json with format auto-detect.
- Export + Print - Enterprise feature pack: download to Excel, PDF, CSV, TSV, HTML, or open a printable view in a new window.
- Export - Theme-matched - One xlsx, light or dark - styles read from the same --sg-* tokens the grid renders with.
- Export - Header + Footer + Logo - Branded xlsx: PNG logo + title + subtitle in the page header, generated date + page numbers in the footer.
- Export - Multiple sheets - One xlsx with 5 tabs - All orders + per-region splits - independent of the current grid filter.