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.
About this example
Replacing the built-in glyphs of the Svelte 5 data grid with your own icon set. Every icon the grid draws for its chrome has a name, and the icons prop is a map from name to snippet, so names you leave out keep their built-in glyph; a partial set is a complete answer. Toggle Ours, Mixed and Yours to see Mixed override four icons while the rest of the chrome carries on. Overrides can be SVG, a design-system icon component, an image or a character, sized by the grid and coloured through currentColor.
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)
Frequently asked questions
How do I replace one icon?
Pass icons={{ filter: myFunnel }} where myFunnel is a snippet. Only the filter glyph changes; sort, menu, pager and expander keep their defaults.
Do I have to size or colour the override?
No. The grid sizes the icon box so an override lands at the same size as the glyph it replaces, and the colour follows currentColor. The expander override also rotates when a group opens.
Which package is this in?
The icons prop is part of the MIT @svgrid/grid package, and the same map applies to the tool panel and pager icons when those are shown.
Related documentation
Related articles
- Status Badge Cells in SvGrid - Turn a plain text status column into colored, accessible pill badges without breaking sort, filter, or virtualization - using SvGrid's snippet cell renderer.
- 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 (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>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.