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

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>

View this example on GitHub

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.