Custom icon set
Replace the built-in glyphs with your own icon set. `icons` is a map keyed by icon name, so names you leave out keep their built-in glyph - a partial set is a complete answer. Toggle Ours / Mixed / Yours: Mixed overrides four icons and the rest of the chrome carries on unchanged.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Themes & Styling). See the SvGrid documentation for the full API.
What this example shows
Every glyph the grid draws for its own chrome has a name, and `icons` replaces any of them with a snippet of your markup.
icons={{ filter: myFunnel }} one icon icons={{ ...wholeSet }} as many as you like
It is a MAP, not one catch-all snippet, and that is the point: names you leave out keep their built-in glyph. So a partial set is a complete answer, and you can never blank an icon by forgetting it exists. Toggle "Ours" / "Mixed" below to see that - Mixed overrides four names and the rest of the chrome carries on unchanged.
The overrides here are hand-drawn SVG, but a snippet is a snippet: an <Icon> from your design system, an <img>, or a plain character all work. Colour follows `currentColor` and the grid sizes the box for you, so an override lands the same size as the glyph it replaced and rotates with the expander when a group opens.
Free, in @svgrid/grid.
Imports, features and API used
Imports: @svgrid/grid
Table features registered: rowSortingFeature, columnFilteringFeature, rowPaginationFeature
Columns: service (Service), version (Version), env (Env), owner (Owner), status (Status), errorRate (Error %), shippedAt (Shipped)
Source code (431-custom-icons.svelte)
<script lang="ts">
/**
* 431. Your icon set instead of ours
* ----------------------------------
* Every glyph the grid draws for its own chrome has a name, and `icons`
* replaces any of them with a snippet of your markup.
*
* icons={{ filter: myFunnel }} one icon
* icons={{ ...wholeSet }} as many as you like
*
* It is a MAP, not one catch-all snippet, and that is the point: names you
* leave out keep their built-in glyph. So a partial set is a complete answer,
* and you can never blank an icon by forgetting it exists. Toggle "Ours" /
* "Mixed" below to see that - Mixed overrides four names and the rest of the
* chrome carries on unchanged.
*
* The overrides here are hand-drawn SVG, but a snippet is a snippet: an
* <Icon> from your design system, an <img>, or a plain character all work.
* Colour follows `currentColor` and the grid sizes the box for you, so an
* override lands the same size as the glyph it replaced and rotates with the
* expander when a group opens.
*
* Free, in @svgrid/grid.
*/
import {
SvGrid,
tableFeatures,
rowSortingFeature,
columnFilteringFeature,
rowPaginationFeature,
type GridColumns,
} from '@svgrid/grid'
type Release = {
id: number
service: string
version: string
env: 'prod' | 'staging' | 'canary'
owner: string
status: 'shipped' | 'rolling' | 'held'
errorRate: number
shippedAt: string
}
const SERVICES = ['checkout', 'billing', 'search', 'identity', 'notifications', 'ledger']
const OWNERS = ['A. Osei', 'R. Vance', 'M. Iqbal', 'J. Lindqvist', 'D. Okonkwo']
const ENVS: Release['env'][] = ['prod', 'staging', 'canary']
const STATUSES: Release['status'][] = ['shipped', 'rolling', 'held']
const iso = (days: number) => {
const d = new Date(2026, 8, 1)
d.setDate(d.getDate() + days)
return d.toISOString().slice(0, 10)
}
const rows: Release[] = Array.from({ length: 40 }, (_, i) => ({
id: i + 1,
service: SERVICES[i % SERVICES.length],
version: `2.${(i % 7) + 1}.${i % 4}`,
env: ENVS[i % ENVS.length],
owner: OWNERS[i % OWNERS.length],
status: STATUSES[(i * 2) % STATUSES.length],
// Deterministic, so the demo reads the same on every load.
errorRate: Number((((i * 37) % 90) / 100).toFixed(2)),
shippedAt: iso(i % 28),
}))
const features = tableFeatures({
rowSortingFeature,
columnFilteringFeature,
rowPaginationFeature,
})
const columns: GridColumns<Release> = [
{ field: 'service', header: 'Service', width: 140 },
{ field: 'version', header: 'Version', width: 100 },
{ field: 'env', header: 'Env', width: 110 },
{ field: 'owner', header: 'Owner', width: 150 },
{ field: 'status', header: 'Status', width: 120 },
{ field: 'errorRate', header: 'Error %', width: 110, align: 'right' },
{ field: 'shippedAt', header: 'Shipped', width: 130 },
]
let set = $state<'builtin' | 'mixed' | 'full'>('mixed')
// The map itself is assembled in the markup, not here: snippets are a
// template construct, so `<script>` cannot see them.
</script>
<!-- The override snippets. Each is plain markup; the grid wraps them in its own
icon box, so they inherit the size and colour of the glyph they replace. -->
{#snippet arrowUp()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 19V5" /><path d="M6 11l6-6 6 6" />
</svg>
{/snippet}
{#snippet arrowDown()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5v14" /><path d="M6 13l6 6 6-6" />
</svg>
{/snippet}
{#snippet funnel()}
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none">
<path d="M4 4h16a1 1 0 0 1 .8 1.6L15 13v6.2a1 1 0 0 1-1.4.9l-3-1.4a1 1 0 0 1-.6-.9V13L3.2 5.6A1 1 0 0 1 4 4z" />
</svg>
{/snippet}
{#snippet dots()}
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none">
<circle cx="12" cy="5" r="1.9" /><circle cx="12" cy="12" r="1.9" /><circle cx="12" cy="19" r="1.9" />
</svg>
{/snippet}
{#snippet lens()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round">
<circle cx="10.5" cy="10.5" r="6.5" /><path d="M20 20l-4.6-4.6" />
</svg>
{/snippet}
{#snippet caret()}
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M9 5l8 7-8 7z" /></svg>
{/snippet}
{#snippet caretDown()}
<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M5 9l7 8 7-8z" /></svg>
{/snippet}
{#snippet pageFirst()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6l-6 6 6 6" /><path d="M11 6l-6 6 6 6" />
</svg>
{/snippet}
{#snippet pagePrev()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 6l-6 6 6 6" />
</svg>
{/snippet}
{#snippet pageNext()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 6l6 6-6 6" />
</svg>
{/snippet}
{#snippet pageLast()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M6 6l6 6-6 6" /><path d="M13 6l6 6-6 6" />
</svg>
{/snippet}
{#snippet cross()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round">
<path d="M6 6l12 12" /><path d="M18 6L6 18" />
</svg>
{/snippet}
{#snippet panel()}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round">
<rect x="3" y="4" width="18" height="16" rx="2" /><path d="M9 4v16" />
</svg>
{/snippet}
<section class="wrap">
<header class="chrome">
<p class="hint">
Sort a column, open a header menu, page through the rows. <strong>Mixed</strong> replaces four
icons and leaves everything else ours - that is the fallback doing its job.
</p>
<div class="seg" role="group" aria-label="Icon set">
<button class:on={set === 'builtin'} onclick={() => (set = 'builtin')}>Ours</button>
<button class:on={set === 'mixed'} onclick={() => (set = 'mixed')}>Mixed</button>
<button class:on={set === 'full'} onclick={() => (set = 'full')}>Yours</button>
</div>
</header>
<div class="grid-host">
<!-- A snippet per name. Nothing special about them: ordinary markup, and
`currentColor` is what lets them pick up the hover / muted / accent
state of whatever chrome they land in. `builtin` passes an empty map,
which is the same as passing nothing. -->
<SvGrid
responsive={true}
data={rows}
{columns}
{features}
icons={set === 'builtin'
? {}
: set === 'mixed'
? { 'sort-asc': arrowUp, 'sort-desc': arrowDown, filter: funnel, menu: dots }
: {
'sort-asc': arrowUp,
'sort-desc': arrowDown,
filter: funnel,
menu: dots,
search: lens,
'chevron-right': caret,
'chevron-down': caretDown,
'page-first': pageFirst,
'page-prev': pagePrev,
'page-next': pageNext,
'page-last': pageLast,
close: cross,
clear: cross,
'tool-panel': panel,
}}
getRowId={(r) => String(r.id)}
showPagination
pageSize={12}
filterMode="row"
toolPanel
rowHeight={36}
containerHeight="100%"
fitColumns
/>
</div>
</section>
<style>
.wrap {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
gap: 12px;
}
.chrome {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
flex: none;
}
.hint {
margin: 0;
font-size: 13px;
color: var(--sg-muted, #64748b);
max-width: 62ch;
}
.seg {
display: inline-flex;
border: 1px solid var(--sg-border, #e2e8f0);
border-radius: 8px;
overflow: hidden;
}
.seg button {
font: inherit;
font-size: 13px;
padding: 5px 12px;
border: 0;
background: var(--sg-bg, #fff);
color: var(--sg-fg, #0f172a);
cursor: pointer;
}
.seg button + button {
border-left: 1px solid var(--sg-border, #e2e8f0);
}
.seg button.on {
background: var(--sg-accent, #2563eb);
color: var(--sg-on-accent, #fff);
}
.grid-host {
flex: 1;
min-height: 0;
}
</style>Related documentation
Related articles
- 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.
- A Custom Column Header Menu in SvGrid - Build a per-column header menu for sort, hide, pin, and custom actions using header snippets and your own dropdown component.
- Conditional Row Styling in SvGrid - Drive row-level background tints, classes, and styles from your data - overdue invoices, failed jobs, VIP records - without touching the DOM directly.
More Themes & Styling examples
- Theming studio - Live token playground: brand color, density, radius, font, dark/light, zebra. Copy-ready CSS snippet, persists across reloads.
- Theme integrations - Five design-system presets (Ant, MUI, Fluent, Base Web, shadcn) toggled by mapping --sg-* tokens. Side panel shows the exact CSS to paste into your app.
- High-contrast theme - WCAG 2.2 AAA-grade preset for accessibility procurement. Token block opts a subtree into the high-contrast skin while the rest of the page stays standard. Light + dark.