Layout & Display

Grouping

Group rows by columns in NuGrid.

NuGrid supports grouping rows by one or more columns, creating expandable hierarchical views of your data.

ID
Name
Department
Role
Salary
Department: Engineering (3 items)
1
Alice
Engineering
Developer
$95,000
3
Carol
Engineering
Lead
$110,000
5
Emma
Engineering
Developer
$90,000
Department: Marketing (2 items)
2
Bob
Marketing
Manager
$85,000
6
Frank
Marketing
Designer
$75,000
Department: HR (2 items)
4
David
HR
Specialist
$60,000
7
Grace
HR
Manager
$80,000
<script setup lang="ts">
import type { NuGridColumn } from '#nu-grid/types'

interface Employee {
  id: number
  name: string
  department: string
  role: string
  salary: number
}

const data = ref<Employee[]>([
  { id: 1, name: 'Alice', department: 'Engineering', role: 'Developer', salary: 95000 },
  { id: 2, name: 'Bob', department: 'Marketing', role: 'Manager', salary: 85000 },
  { id: 3, name: 'Carol', department: 'Engineering', role: 'Lead', salary: 110000 },
  { id: 4, name: 'David', department: 'HR', role: 'Specialist', salary: 60000 },
  { id: 5, name: 'Emma', department: 'Engineering', role: 'Developer', salary: 90000 },
  { id: 6, name: 'Frank', department: 'Marketing', role: 'Designer', salary: 75000 },
  { id: 7, name: 'Grace', department: 'HR', role: 'Manager', salary: 80000 },
])

const columns: NuGridColumn<Employee>[] = [
  { accessorKey: 'id', header: 'ID', size: 60 },
  { accessorKey: 'name', header: 'Name', size: 120 },
  { accessorKey: 'department', header: 'Department', size: 120 },
  { accessorKey: 'role', header: 'Role', size: 120 },
  {
    accessorKey: 'salary',
    header: 'Salary',
    size: 100,
    cell: ({ row }) => `$${row.original.salary.toLocaleString()}`,
  },
]
</script>

<template>
  <div class="w-full">
    <NuGrid
      :data="data"
      :columns="columns"
      :grouping="['department']"
      :layout="{ mode: 'group' }"
      :ui="{
        base: 'w-full border-separate border-spacing-0',
        thead: '[&>tr]:bg-elevated/50',
        th: 'py-2 border-y border-default first:border-l last:border-r first:rounded-l-lg last:rounded-r-lg',
        td: 'border-b border-default',
      }"
    />
  </div>
</template>

Basic Grouping

Enable grouping with the grouping v-model:

<script setup lang="ts">
const grouping = ref(['category'])
</script>

<template>
  <NuGrid
    v-model:grouping="grouping"
    :data="data"
    :columns="columns"
  />
</template>

Groups render in the 'group' (or 'splitgroup') layout. A grid that starts with grouping set and no layout.mode uses 'group' automatically. If grouping can start empty and be switched on later, set :layout="{ mode: 'group' }" yourself: the layout is chosen when the grid is created, and a flat ('div') grid shows grouped rows ungrouped, with a warning in development. Until grouping is set, a 'group' or 'splitgroup' grid shows its rows flat, under one set of column headers.

Multi-Level Grouping

Group by multiple columns:

<script setup lang="ts">
const grouping = ref(['category', 'status'])
</script>

<template>
  <NuGrid
    v-model:grouping="grouping"
    :data="data"
    :columns="columns"
  />
</template>

Rows will be grouped first by category, then by status within each category.

Dynamic Grouping

Allow users to change grouping:

<script setup lang="ts">
const grouping = ref<string[]>([])
const availableGroups = ['category', 'status', 'assignee']
</script>

<template>
  <div class="mb-4 flex gap-2">
    <UButton
      v-for="col in availableGroups"
      :key="col"
      :color="grouping.includes(col) ? 'primary' : 'neutral'"
      :variant="grouping.includes(col) ? 'solid' : 'outline'"
      @click="
        grouping.includes(col)
          ? grouping = grouping.filter(g => g !== col)
          : grouping = [...grouping, col]
      "
    >
      {{ col }}
    </UButton>

    <UButton
      v-if="grouping.length"
      color="neutral"
      variant="ghost"
      @click="grouping = []"
    >
      Clear
    </UButton>
  </div>

  <NuGrid
    v-model:grouping="grouping"
    :data="data"
    :columns="columns"
  />
</template>

Expand/Collapse State

Track and control expanded groups:

<script setup lang="ts">
const expanded = ref<Record<string, boolean>>({})
</script>

<template>
  <NuGrid
    v-model:grouping="grouping"
    v-model:expanded="expanded"
    :data="data"
    :columns="columns"
  />

  <div class="mt-4">
    <UButton @click="expanded = {}">Collapse All</UButton>
  </div>
</template>

Programmatic Expand/Collapse

Use the grid ref to control expansion:

<script setup lang="ts">
const gridRef = useTemplateRef('grid')

function expandAll() {
  gridRef.value?.tableApi?.toggleAllRowsExpanded(true)
}

function collapseAll() {
  gridRef.value?.tableApi?.toggleAllRowsExpanded(false)
}

function toggleGroup(groupId: string) {
  gridRef.value?.tableApi?.getRow(groupId)?.toggleExpanded()
}
</script>

<template>
  <div class="mb-4 flex gap-2">
    <UButton @click="expandAll">Expand All</UButton>
    <UButton @click="collapseAll">Collapse All</UButton>
  </div>

  <NuGrid
    ref="grid"
    v-model:grouping="grouping"
    :data="data"
    :columns="columns"
  />
</template>

Summaries & Aggregates

NuGrid provides a built-in summary system for calculating aggregates across groups and displaying grand totals.

Enabling Summaries

Use the summaries prop to enable summary features:

<template>
  <NuGrid
    v-model:grouping="grouping"
    :data="data"
    :columns="columns"
    :summaries="{
      grandTotals: { label: 'Grand Total' },
      groupSummaries: true,
    }"
  />
</template>

Summaries Prop Options

OptionTypeDefaultDescription
grandTotalsboolean | { enabled?, label?, position? }falseShow grand totals footer row
groupSummariesbooleantrueShow summary values in collapsed group headers

Column Summary Configuration

Define aggregates at the column level using the summary property:

const columns: NuGridColumn<Product>[] = [
  {
    accessorKey: 'price',
    header: 'Price',
    summary: {
      aggregate: 'sum',
      format: (v) => `$${v.toFixed(2)}`,
    },
  },
  {
    accessorKey: 'quantity',
    header: 'Quantity',
    summary: { aggregate: 'sum' },
  },
  {
    accessorKey: 'rating',
    header: 'Rating',
    summary: {
      aggregate: 'avg',
      format: (v) => `${v.toFixed(1)} ★`,
    },
  },
]

Built-in Aggregates

AggregateDescription
sumSum of all values
avgAverage of all values
countCount of rows
minMinimum value
maxMaximum value

Custom Aggregate Function

{
  accessorKey: 'status',
  header: 'Status',
  summary: {
    aggregate: (rows) => {
      const active = rows.filter(r => r.status === 'active').length
      return `${active}/${rows.length} active`
    },
  },
}

Split Group Mode Behavior

In splitgroup layout mode:

  • Collapsed groups show summary values in a compact header row below the group header
  • When group summaries are disabled, the column headers are hidden for collapsed groups, showing only the group header
<template>
  <NuGrid
    v-model:grouping="grouping"
    :data="data"
    :columns="columns"
    :layout="{ mode: 'splitgroup' }"
    :summaries="{ grandTotals: true, groupSummaries: true }"
  />
</template>

Grand Totals

The grand totals row appears at the bottom of the grid and shows aggregate values across all data:

<template>
  <NuGrid
    :data="data"
    :columns="columns"
    :summaries="{
      grandTotals: {
        enabled: true,
        label: 'Total',
        position: 'bottom',  // 'top' or 'bottom'
      },
    }"
  />
</template>

Group Row Rendering

Customize group row appearance:

const columns: NuGridColumn<Product>[] = [
  {
    accessorKey: 'category',
    header: 'Category',
    cell: ({ row, getValue }) => {
      if (row.getIsGrouped()) {
        return h('div', { class: 'flex items-center gap-2' }, [
          h(UIcon, {
            name: row.getIsExpanded()
              ? 'i-lucide-chevron-down'
              : 'i-lucide-chevron-right',
          }),
          h('span', { class: 'font-semibold' }, getValue()),
          h('span', { class: 'text-muted' }, `(${row.subRows.length})`),
        ])
      }
      return getValue()
    },
  },
]

Grouping with Selection

Row selection works with grouping:

<template>
  <NuGrid
    v-model:grouping="grouping"
    v-model:selected-rows="selectedRows"
    :data="data"
    :columns="columns"
    row-selection="multi"
  />
</template>

Selecting a group row selects all rows within that group.

Grouping with Pagination

Pagination and grouping can be combined:

<template>
  <NuGrid
    v-model:grouping="grouping"
    :data="data"
    :columns="columns"
    :paging="{ pageSize: 20 }"
  />
</template>

Groups that span multiple pages will show partial content on each page.

Nested Groups Example

<script setup lang="ts">
interface Task {
  id: number
  name: string
  project: string
  status: string
  priority: string
  assignee: string
}

const data = ref<Task[]>([
  { id: 1, name: 'Task 1', project: 'Alpha', status: 'active', priority: 'high', assignee: 'Alice' },
  { id: 2, name: 'Task 2', project: 'Alpha', status: 'active', priority: 'low', assignee: 'Bob' },
  { id: 3, name: 'Task 3', project: 'Beta', status: 'pending', priority: 'high', assignee: 'Alice' },
  // ...
])

const grouping = ref(['project', 'status'])

const columns: NuGridColumn<Task>[] = [
  { accessorKey: 'project', header: 'Project' },
  { accessorKey: 'status', header: 'Status' },
  { accessorKey: 'name', header: 'Task' },
  { accessorKey: 'priority', header: 'Priority' },
  { accessorKey: 'assignee', header: 'Assignee' },
]
</script>

<template>
  <NuGrid
    v-model:grouping="grouping"
    :data="data"
    :columns="columns"
  />
</template>

Styling Groups

Group rows receive special attributes for styling:

<template>
  <NuGrid
    v-model:grouping="grouping"
    :data="data"
    :columns="columns"
    :ui="{
      tbody: '[&>tr[data-grouped=true]]:bg-elevated/30 [&>tr[data-grouped=true]]:font-semibold',
    }"
  />
</template>

Next Steps

Sticky Headers

Keep headers visible while scrolling.

Virtualization

Handle large datasets efficiently.