Gallery at scale (5,000 cards)
5,000 rows as cards, drawn in batches as the wall scrolls, so the first paint costs the same as a small table. Search narrows all of them; the cover is a computed column (fieldFn), so no image is stored in the rows. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Gallery). See the SvGrid documentation for the full API.
About this example
A Svelte 5 data grid gallery over 5,000 rows. The cards are drawn in batches of 120 as the wall scrolls, so the first paint costs the same as a small table; search narrows all 5,000 rows and starts the wall from the top. The cover is a computed column, drawn per card from the row.
5,000 rows as cards. The wall draws them in batches as it scrolls (120 at a time, the next batch a little before the end comes into view), so the first paint costs the same as a small table. Search narrows all 5,000 and starts the wall from the top again. The cover comes from a computed column (fieldFn), so no image is stored in the rows: each piece is drawn when its card is.
Imports, features and API used
Imports: @svgrid/enterprise, @svgrid/grid, ../shared/gallery-art
Columns: name (Name), sku (SKU), price (Price), stock (Stock), cover (Cover)
Frequently asked questions
Does the gallery mount every card?
No. It draws the first 120 cards and adds the next 120 a little before the end of the wall comes into view. A new search or filter starts again from the first batch.
Can the cover be computed instead of stored?
Yes. coverField can name a column with a fieldFn; the gallery reads its value per row, so the rows here hold a hue and the image is drawn from it when its card is drawn.
Related documentation
Source code (509-gallery-at-scale.svelte)
<script lang="ts">
// Gallery rendering is an Enterprise feature - register the renderer.
import { enableGalleryView } from '@svgrid/enterprise'
enableGalleryView()
/**
* 509. Gallery at scale
* ---------------------
* 5,000 rows as cards. The wall draws them in batches as it scrolls (120 at
* a time, the next batch a little before the end comes into view), so the
* first paint costs the same as a small table. Search narrows all 5,000 and
* starts the wall from the top again. The cover comes from a computed
* column (`fieldFn`), so no image is stored in the rows: each piece is drawn
* when its card is.
*/
import { SvGrid, type GridColumns } from '@svgrid/grid'
import { ceramic } from '../shared/gallery-art'
// `cover` is not stored: the Cover column computes it (fieldFn).
type Item = { id: number; sku: string; name: string; price: number; stock: number; cover?: string }
// Named as drawn: the glaze cycles every piece, the shape every ten.
const GLAZE = ['Sand', 'Celadon', 'Oat', 'Slate', 'Terracotta', 'Bone', 'Moss', 'Lilac', 'Ochre', 'Sky']
const SHAPE = ['vase', 'bowl', 'jar', 'bottle']
const rows: Item[] = Array.from({ length: 5000 }, (_, i) => ({
id: i + 1,
sku: `CER-${String(i + 1).padStart(5, '0')}`,
name: `${GLAZE[i % 10]} ${SHAPE[Math.floor(i / 10) % 4]} no. ${Math.floor(i / 40) + 1}`,
price: 18 + ((i * 37) % 160),
stock: (i * 13) % 60,
}))
// Each piece photographed per card from its index: no image is stored in the rows.
const shot = (r: Item) => ceramic(r.id - 1)
const columns: GridColumns<Item> = [
{ field: 'name', header: 'Name', width: 190 },
{ field: 'sku', header: 'SKU', width: 120 },
{ field: 'price', header: 'Price', width: 100, editorType: 'number', format: { type: 'currency', currency: 'USD' } },
{ field: 'stock', header: 'Stock', width: 90, editorType: 'number' },
// The cover: computed per row, not stored, and not shown as a table column.
{ id: 'cover', field: 'cover', header: 'Cover', fieldFn: shot, visible: false },
]
let view = $state<'gallery' | 'table'>('gallery')
</script>
<section class="flex flex-col gap-3 h-full min-h-0">
<div class="flex flex-wrap items-center gap-3 shrink-0">
<p class="sc-note text-sm m-0 flex-1 min-w-[240px]">5,000 cards, drawn in batches as you scroll. Search narrows all of them.</p>
<div class="sc-switch inline-flex rounded-md overflow-hidden text-sm" role="radiogroup" aria-label="View">
<button type="button" role="radio" aria-checked={view === 'gallery'} class="px-3 py-1 {view === 'gallery' ? 'sc-on' : ''}" onclick={() => (view = 'gallery')}>Gallery</button>
<button type="button" role="radio" aria-checked={view === 'table'} class="px-3 py-1 {view === 'table' ? 'sc-on' : ''}" onclick={() => (view = 'table')}>Table</button>
</div>
</div>
<div class="flex-1 min-h-0">
<SvGrid
data={rows}
{columns}
getRowId={(r) => String(r.id)}
sortable
containerHeight="100%"
gallery={view === 'gallery' ? { coverField: 'cover', titleField: 'name', cardFields: ['sku', 'price', 'stock'], cardSize: 'small' } : undefined}
/>
</div>
</section>
<style>
.sc-note { color: var(--sg-muted, #64748b); }
.sc-switch { border: 1px solid var(--sg-border, #cbd5e1); background: none; color: var(--sg-fg, #0f172a); }
.sc-on { background: var(--sg-header-bg, #f1f5f9); color: var(--sg-fg, #0f172a); font-weight: 600; }
</style>More Gallery examples
- Product catalog (gallery) - One <SvGrid> as a storefront: the `gallery` prop shows the rows as product cards drawn by a card snippet (photo, brand, rating, sale price, a badge), and Table shows the same rows. The category filter, search and sort are the grid, so both views agree; a click opens the product in a panel with Add to cart.
- Team directory (custom cards) - A gallery whose cards you draw yourself: `gallery.card` is a snippet that gets each row, so a person shows as a profile card with photo, role, team and a mail link. The search box and the Team filter buttons narrow the same rows the Table view shows.
- Real estate listings - Listings drawn by a card snippet: the photo with its status and a save button, the price, beds, baths and size, the address and the agent. The cards are the grid filtered and sorted rows: status buttons and a beds filter set column filters, the sort menu calls api.setSort, the search box is the quick filter.
- Cards from field types - With the fields module on, a gallery card draws each field as its cell does: select pills, stars, avatars, currency, with each field type icon by its name. The cover is the first image of an attachment field.