Inventory
An inventory, ready to use: Items and Suppliers as two linked tables. Items have photos, category, location, quantity, unit cost and a stock value formula; each supplier lists its items and units in stock. Views for stock by category, photos, items by location, a reorder list and an add-item form. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Base). See the SvGrid documentation for the full API.
About this example
An inventory built with SvGrid Base, a table database component for Svelte 5: an Items table and a Suppliers table as tabs, linked both ways. Items have photos, a category, a location, a supplier, quantity, unit cost and a stock value formula; each supplier lists its items and units in stock. Views for stock grouped by category, a photo gallery, items by location, a reorder list and an add-item form.
A studio's equipment and stock as a ready-to-use base with two tables. Each item has a photo, category, location, supplier, quantity, unit cost, the stock value (a formula: Qty times Unit cost), its condition, who has it checked out and when it was last counted. Suppliers list the items they supply, how many, and the units in stock, linked both ways.
The Items views: the stock list grouped by category with value totals, a photo gallery, items by location as a Kanban (drag one to move it), the "Reorder" view of what runs low, and the form for adding an item.
Imports, features and API used
Imports: @svgrid/enterprise/base, ../shared/gallery-art, ../shared/base-people
Columns: name (Supplier), email (Orders), leadDays (Lead time (days)), items (Items), itemCount (Item count), units (Units in stock), qty
Frequently asked questions
Is this a template or a component?
A component: <SvBase> with your rows and columns. Most of the demo is sample data; the views, the field editor, the record view and the form come with the component.
How is the stock value computed?
Stock value is a formula field, {Qty} * {Unit cost}, shown as currency. It is parsed once into functions, never evaluated as code, and its footer sums the column.
How does the Reorder view work?
A saved filter keeps items with three or fewer in stock, sorted by quantity, and condition colors mark them: red at zero, amber at three or fewer.
Which license does it need?
SvGrid Base is part of the Suite edition of @svgrid/enterprise. Without a key it runs with a watermark, so it can be evaluated as is.
Related documentation
Related articles
- Building an Inventory Management Grid in Svelte - Stock levels, low-stock highlighting, inline edits, bulk updates, and CSV import - a practical inventory grid built with SvGrid.
- A Svelte Data Grid with SvelteKit and Supabase - Wire SvGrid to a Supabase Postgres backend with server-side pagination, sorting, and filtering - keeping credentials on the server and queries fast with proper indexing.
- A Live Svelte Data Grid with Firebase Firestore - Wire Firestore onSnapshot to SvGrid in Svelte 5 - reactive listeners, query-driven sort and filter, cursor pagination, and the subtle cleanup detail most implementations get wrong.
Source code (515-base-inventory.svelte)
<script lang="ts">
/**
* 515. SvGrid Base: inventory
* ---------------------------
* A studio's equipment and stock as a ready-to-use base with two tables.
* Each item has a photo, category, location, supplier, quantity, unit cost,
* the stock value (a formula: Qty times Unit cost), its condition, who has
* it checked out and when it was last counted. Suppliers list the items
* they supply, how many, and the units in stock, linked both ways.
*
* The Items views: the stock list grouped by category with value totals, a
* photo gallery, items by location as a Kanban (drag one to move it), the
* "Reorder" view of what runs low, and the form for adding an item.
*/
import { SvBase, enableBase, type Attachment, type BaseTable } from '@svgrid/enterprise/base'
import { productShot, type ProductKind } from '../shared/gallery-art'
import { usePeople } from '../shared/base-people'
enableBase()
usePeople()
type Item = {
id: number
item: string
photo: Attachment[]
category: string
location: string
supplier: string[]
qty: number
unitCost: number
condition: number
checkedOut: string
audited: string
sku: string
}
const CATEGORIES = [
{ value: 'Furniture', color: '#c46f4e' },
{ value: 'Lighting', color: '#fcb400' },
{ value: 'Kitchen', color: '#20c933' },
{ value: 'Audio', color: '#8b46ff' },
{ value: 'Decor', color: '#2d7ff9' },
{ value: 'Plants', color: '#20d9d2' },
]
const LOCATIONS = [
{ value: 'Studio A', color: '#2d7ff9' },
{ value: 'Studio B', color: '#8b46ff' },
{ value: 'Warehouse', color: '#94a3b8' },
{ value: 'Showroom', color: '#ff6f2c' },
]
const ITEMS: Array<[string, ProductKind, string, string, number]> = [
['Lounge chair, sage', 'chair', '#8a9a7b', 'Furniture', 640],
['Dining chair, navy', 'chair', '#34495e', 'Furniture', 290],
['Dining chair, oat', 'chair', '#d8cbb3', 'Furniture', 290],
['Arc floor lamp', 'lamp', '#efe6d8', 'Lighting', 410],
['Table lamp, black', 'lamp', '#2f2f33', 'Lighting', 160],
['Table lamp, sand', 'lamp', '#d9b38c', 'Lighting', 160],
['Stoneware mug, slate', 'mug', '#6d8fa3', 'Kitchen', 18],
['Stoneware mug, rust', 'mug', '#9e5a3c', 'Kitchen', 18],
['Espresso cup, forest', 'mug', '#2e3d34', 'Kitchen', 12],
['Bookshelf speaker', 'speaker', '#3a3d42', 'Audio', 380],
['Portable speaker, olive', 'speaker', '#6b7f6e', 'Audio', 140],
['Wall clock, brass', 'clock', '#b08d57', 'Decor', 95],
['Desk clock, celadon', 'clock', '#9fb4a8', 'Decor', 70],
['Fiddle leaf fig', 'plant', '#c67b5c', 'Plants', 85],
['Snake plant', 'plant', '#e9e4dc', 'Plants', 45],
]
// Who supplies each category.
const SUPPLIERS = [
['Nordic Home Supply', '[email protected]', 14, ['Furniture', 'Decor']],
['Lumen & Co', '[email protected]', 10, ['Lighting']],
['Kiln Ceramics', '[email protected]', 21, ['Kitchen']],
['Signal Audio', '[email protected]', 7, ['Audio']],
['Greenhouse Co', '[email protected]', 5, ['Plants']],
] as const
const supplierOf = (category: string) => `s${SUPPLIERS.findIndex((s) => (s[3] as readonly string[]).includes(category)) + 1}`
const day = (n: number) => new Date(Date.UTC(2026, 8, 1 + n)).toISOString().slice(0, 10)
let nextId = ITEMS.length + 1
const items: Item[] = ITEMS.map(([item, kind, color, category, unitCost], i) => ({
id: i + 1,
item,
photo: [{ id: `ph${i}`, name: `${item.toLowerCase().replace(/[^a-z]+/g, '-')}.svg`, type: 'image/svg+xml', url: productShot(kind, color) }],
category,
location: LOCATIONS[(i * 3) % LOCATIONS.length]!.value,
supplier: [supplierOf(category)],
qty: [12, 2, 30, 4, 1, 9, 48, 6, 3, 2, 7, 1, 5, 3, 0][i]!,
unitCost,
condition: 2 + ((i * 3) % 4),
checkedOut: i % 4 === 1 ? ['u2', 'u3', 'u4'][i % 3]! : '',
audited: day((i * 4) % 40),
sku: `INV-${String(1000 + i * 7)}`,
}))
const suppliers = SUPPLIERS.map(([name, email, leadDays], i) => ({
id: `s${i + 1}`,
name,
email,
leadDays,
items: items.filter((it) => it.supplier[0] === `s${i + 1}`).map((it) => String(it.id)),
}))
const itemColumns: any[] = [
{ field: 'item', header: 'Item', width: 230, fieldType: 'text' },
{ field: 'photo', header: 'Photo', width: 104, fieldType: 'attachment', fieldProps: { accept: 'image/*', maxFiles: 3 } },
{ field: 'category', header: 'Category', width: 150, fieldType: 'singleSelect', fieldProps: { options: CATEGORIES } },
{ field: 'location', header: 'Location', width: 130, fieldType: 'singleSelect', fieldProps: { options: LOCATIONS } },
{ field: 'supplier', header: 'Supplier', width: 180, fieldType: 'link', fieldProps: { table: 'inventory-suppliers', multiple: false, inverse: 'items' } },
{ field: 'qty', header: 'Qty', width: 90, fieldType: 'number', fieldProps: { format: 'integer', summary: 'sum' } },
{ field: 'unitCost', header: 'Unit cost', width: 132, fieldType: 'number', fieldProps: { format: 'currency', symbol: '$', precision: 2 } },
{ field: 'value', header: 'Stock value', width: 150, fieldType: 'formula', fieldProps: { formula: '{Qty} * {Unit cost}', result: 'currency', precision: 2, summary: 'sum' } },
{ field: 'condition', header: 'Condition', width: 130, fieldType: 'rating', fieldProps: { max: 5, color: '#20c933' } },
{ field: 'checkedOut', header: 'Checked out to', width: 170, fieldType: 'user' },
{ field: 'audited', header: 'Last counted', width: 150, fieldType: 'date', fieldProps: { dateFormat: 'friendly' } },
{ field: 'sku', header: 'SKU', width: 120, fieldType: 'barcode' },
]
let tables = $state.raw<BaseTable[]>([
{
id: 'inventory-items',
name: 'Items',
primaryField: 'item',
data: items,
columns: itemColumns,
views: [
{ id: 'stock', name: 'Stock by category', type: 'grid', state: { grouping: ['category'], sorting: [{ id: 'category', desc: false }, { id: 'item', desc: false }] } },
{ id: 'photos', name: 'Photos', type: 'gallery', coverField: 'photo' },
{ id: 'where', name: 'By location', type: 'kanban', stackBy: 'location' },
{ id: 'units', name: 'Units by location', type: 'chart', chart: { kind: 'bar', x: 'location', y: 'qty' } },
{
id: 'reorder',
name: 'Reorder',
type: 'grid',
color: { mode: 'conditions', rules: [{ color: '#f82b60', condition: { column: 'qty', kind: 'number', op: 'eq', value: 0 } }, { color: '#fcb400', condition: { column: 'qty', kind: 'number', op: 'lte', value: 3 } }] },
state: {
sorting: [{ id: 'qty', desc: false }],
advancedFilter: { kind: 'fieldFilter', conjunction: 'and', conditions: [{ column: 'qty', kind: 'number', op: 'lte', value: 3 }] },
},
},
{ id: 'add', name: 'Add an item', type: 'form', formFields: ['item', 'photo', 'category', 'location', 'supplier', 'qty', 'unitCost'], formDescription: 'New stock or equipment. It shows in the list as soon as you submit.' },
],
props: { rowAdder: { label: 'Add item', defaults: () => ({ id: nextId++, item: '', photo: [], qty: 0, condition: 5 }) } },
},
{
id: 'inventory-suppliers',
name: 'Suppliers',
primaryField: 'name',
data: suppliers,
columns: [
{ field: 'name', header: 'Supplier', width: 200, fieldType: 'text' },
{ field: 'email', header: 'Orders', width: 260, fieldType: 'email' },
{ field: 'leadDays', header: 'Lead time (days)', width: 150, fieldType: 'number', fieldProps: { format: 'integer' } },
{ field: 'items', header: 'Items', width: 320, fieldType: 'link', fieldProps: { table: 'inventory-items', multiple: true, inverse: 'supplier' } },
{ field: 'itemCount', header: 'Item count', width: 120, fieldType: 'count', fieldProps: { linkField: 'items' } },
{ field: 'units', header: 'Units in stock', width: 140, fieldType: 'rollup', fieldProps: { linkField: 'items', field: 'qty', aggregation: 'SUM' } },
],
views: [{ id: 'suppliers', name: 'All suppliers', type: 'grid' }],
props: { rowAdder: { label: 'Add supplier' } },
},
])
</script>
<section class="base-demo">
<SvBase
bind:tables
editable
sortable
showRowNumbers
expandRows
pinSplitter
summary
rowNumberWidth={64}
showRowSelection={false}
rowHeight={44}
containerHeight="100%"
selectionBar={['selectAll', 'editFields', 'deleteRows']}
columnAdder
fieldMenu
/>
</section>
<style>
.base-demo { display: flex; flex-direction: column; flex: 1; min-height: 480px; height: 100%; }
</style>More Base examples
- Project tracker - A project tracker, ready to use: Tasks and Milestones as two linked tables. Tasks have status, tags, assignee, dates, estimate, progress, budget and files; each milestone lists its tasks with their progress and budget. Views for all tasks, the board by status, due dates on a calendar, a gallery and a request form. Automations close finished tasks, start work from a button and flag overdue ones.
- Content calendar - An editorial content calendar, ready to use: Posts and Campaigns as two linked tables. Posts have stage, channels, author, publish date, cover and brief. Views for the plan, the pipeline as a Kanban, the publishing calendar, a wall of covers and a pitch form.
- Sales CRM - A sales CRM, ready to use: Deals, Accounts and Contacts as three linked tables. Link a deal to an account and the account lists it. Views for all deals, the pipeline as a Kanban, the closing calendar, open deals by owner and a new-lead form.
- Product roadmap - A product roadmap, ready to use: Features and customer Feedback as two linked tables. Features have quarter, status, teams, lead, impact, effort and votes. Views for the roadmap grouped by quarter, a status board, a timeline, the work in progress now and a request form.
- Hiring tracker - A hiring tracker, ready to use: Candidates and Roles as two linked tables. Candidates have photo, role, stage, recruiter, rating, interview date and resume; each role lists its candidates. Views for the pipeline as a Kanban, all candidates, a gallery of faces, interviews and an application form.