Grouping + aggregation

Group by department, sum salaries, average performance, expand/collapse keys.

A live, editable Svelte 5 data grid example from the SvGrid gallery (Sorting & Grouping). See the SvGrid documentation for the full API.

About this example

Row grouping in the Svelte 5 data grid over 500 people. columnGroupingFeature buckets rows by department, country or both and renders a group row in their place, rowExpandingFeature expands and collapses each group, and showGroupingControls exposes Group by this column in the header menu; buttons above the grid switch the grouping through api.setGroupBy. enableRowSummaries adds a sticky footer that sums the salary column and counts the others across every filtered row.

The grid's built-in grouping pipeline buckets rows by one or more columns and renders a group row in their place. Aggregation here is computed in the demo (the engine resolves shared values per group; this component layers sum/avg on top for the visible "Salary" and "Performance" columns via the row-summary footer).

Imports, features and API used

Imports: @svgrid/grid, ../shared/seed

Table features registered: rowSortingFeature, columnGroupingFeature, rowExpandingFeature

Columns: department (Department), country (Country), firstName (First name), lastName (Last name), age (Age), salary (Salary), performance (Performance)

SvGridApi methods called: api.setGroupBy()

Frequently asked questions

How do I group rows by a column?

Register columnGroupingFeature and rowExpandingFeature, then call api.setGroupBy(['department']) or let users pick Group by this column from the header menu with showGroupingControls on. Pass several ids for multi-level groups.

How do I show totals?

enableRowSummaries appends a sticky footer row that sums numeric columns and shows a count for the rest across every filtered row. For a rolled-up value inside each group header, set aggregate on the column, as the group aggregators demo shows.

How do I expand or collapse every group?

Click a group row to toggle it, or call api.expandAllGroups() and api.collapseAllGroups() from a toolbar. Passing an empty array to api.setGroupBy removes the grouping altogether.

Related documentation

Related articles

Source code (07-grouping-aggregation.svelte)

<script lang="ts">
  /**
   * 07. Grouping + aggregation
   * --------------------------
   * The grid's built-in grouping pipeline buckets rows by one or more
   * columns and renders a group row in their place. Aggregation here is
   * computed in the demo (the engine resolves shared values per group; this
   * component layers sum/avg on top for the visible "Salary" and
   * "Performance" columns via the row-summary footer).
   */
  import {
    SvGrid,
    tableFeatures,
    rowSortingFeature,
    columnGroupingFeature,
    rowExpandingFeature,
    type GridColumns,
    type SvGridApi,
  } from '@svgrid/grid'
  import { makePeople, type Person } from '../shared/seed'

  const features = tableFeatures({
    rowSortingFeature,
    columnGroupingFeature,
    rowExpandingFeature,
  })

  const rows = makePeople(500)

  const columns: GridColumns<Person> = [
    { field: 'department', header: 'Department', editorType: 'text' },
    { field: 'country',    header: 'Country',    editorType: 'text' },
    { field: 'firstName',  header: 'First name', editorType: 'text' },
    { field: 'lastName',   header: 'Last name',  editorType: 'text' },
    { field: 'age',        header: 'Age',        editorType: 'number' },
    {
      field: 'salary',
      header: 'Salary',
      editorType: 'number',
      format: { type: 'currency', currency: 'USD', options: { maximumFractionDigits: 0 } },
    },
    { field: 'performance', header: 'Performance', editorType: 'number' },
  ]

  let api = $state<SvGridApi<typeof features, Person> | null>(null)
  let groupBy = $state<string[]>(['department'])

  function applyGroup(by: string[]) {
    groupBy = by
    api?.setGroupBy(by)
  }
</script>

<section class="flex flex-col flex-1 min-h-0 gap-3">
  <div class="flex flex-wrap items-center gap-2 text-sm shrink-0">
    <span class="font-medium">Group by:</span>
    <button
      onclick={() => applyGroup([])}
      class="gb-btn rounded border px-3 py-1 {groupBy.length === 0 ? 'is-on' : ''}"
    >None</button>
    <button
      onclick={() => applyGroup(['department'])}
      class="gb-btn rounded border px-3 py-1 {groupBy.join() === 'department' ? 'is-on' : ''}"
    >Department</button>
    <button
      onclick={() => applyGroup(['country'])}
      class="gb-btn rounded border px-3 py-1 {groupBy.join() === 'country' ? 'is-on' : ''}"
    >Country</button>
    <button
      onclick={() => applyGroup(['department', 'country'])}
      class="gb-btn rounded border px-3 py-1 {groupBy.join() === 'department,country' ? 'is-on' : ''}"
    >Department → Country</button>
    <span class="gb-hint ml-3">
      Click a group row to expand. The row-summaries footer aggregates totals.
    </span>
  </div>

  <div class="flex-1 min-h-0">
    <SvGrid responsive={true}
      columnResize
      data={rows}
      columns={columns}
      features={features}
      filterMode="menu"
      selectionMode="cell"
      showGroupingControls={true}
      enableInlineEditing={false}
      enableCellSelection={true}
      enableRowSummaries={true}
      rowHeight={36}
      containerHeight="100%"
      fitColumns={true}
      onApiReady={(next) => {
        api = next
        // Apply the initial group-by once the imperative API is available,
        // then auto-expand the first group so the data isn't hidden behind a
        // single closed row on first paint.
        next.setGroupBy(groupBy)
        queueMicrotask(() => next.setRowExpanded(`department:Engineering`, true))
      }}
    />
  </div>
</section>

<style>
  .gb-btn {
    border-color: var(--sg-border, #cbd5e1);
    background: var(--sg-bg, #fff);
    color: var(--sg-fg, #0f172a);
  }
  .gb-btn:hover { background: var(--sg-row-hover-bg, rgba(148, 163, 184, 0.12)); }
  .gb-btn.is-on {
    background: var(--sg-bg-subtle, var(--sg-header-bg, #e2e8f0));
    border-color: var(--sg-border, #cbd5e1);
  }
  .gb-hint { color: var(--sg-muted, #64748b); }
</style>

View this example on GitHub

More Sorting & Grouping examples

  • Group aggregators - Declarative per-column rollups for group rows via the aggregate column option: sum, avg, min, max, count, countDistinct, extent, first, or a custom (values, rows) reducer. Each rollup is formatted with the column format and shown in the group header.
  • Group panel (drag & drop) - A Group Panel: drag chips into the panel to group, drag inside to reorder grouping levels, × to ungroup. Drives api.setGroupBy() under the hood.
  • Group display modes + footers - Switch between groupRows banners, a single combined Group column, and one column per grouped field. groupFooters closes each group with a subtotal row under the real columns. Paging counts DATA rows, so pageSize means what it says: a page reprints the banners its rows sit under, and footers never eat the budget.
  • Tree data (hierarchy) - treeData nests rows by parent id into an expandable hierarchy. Tree rows stay real data rows - own cells, formatting, editing - and just gain an expander plus indent. Takes flat parent-id data directly, or nested children arrays via flattenTreeData. Full treegrid a11y with arrow-key expand.
  • Reporting workspace - Pivot-lite: group-by chips, per-column aggregator picker, saved views with localStorage persistence, live KPI strip + summary cards.