Bulk-edit selected rows

Goal: the user selects N rows, opens a toolbar, picks a field + value, and every selected row updates in one click. Standard back-office workflow for status changes, reassignments, tag application.

Open the live example: Bulk actions toolbar (Selection & Clipboard)

Implementation

The recipe is three pieces:

  1. selectionMode="row" + showRowSelection={true} gives you the checkbox column and the multi-select keyboard model.
  2. onRowSelectionChange keeps a $state array of selected rows.
  3. A toolbar above the grid commits the bulk change to internalData via the api's setCellValue (one call per selected row).

The examples on this page run against these rows:

<script lang="ts">
  import { SvGrid, renderSnippet, type GridColumns, type SvGridApi } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    age: number
    salary: number
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   age: 36, salary: 142000 },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', age: 45, salary: 168000 },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', age: 54, salary: 155000 },
    { id: 4, name: 'Radia Perlman',  department: 'Networking',  city: 'Seattle',  age: 49, salary: 161000 },
    { id: 5, name: 'Barbara Liskov', department: 'Platform',    city: 'Boston',   age: 52, salary: 172000 },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 190 },
    { field: 'department', header: 'Department', width: 150 },
    { field: 'city',       header: 'City',       width: 130 },
    { field: 'age',        header: 'Age',        width: 80 },
    { field: 'salary',     header: 'Salary',     width: 130, format: { type: 'currency', currency: 'USD' } },
  ]
</script>
<script lang="ts">
  import {
    SvGrid, tableFeatures, rowSortingFeature, columnFilteringFeature,
    rowSelectionFeature, type ColumnDef, type SvGridApi,
  } from '@svgrid/grid'

  const features = tableFeatures({
    rowSortingFeature,
    columnFilteringFeature,
    rowSelectionFeature,
  })

  type Order = {
    id: string
    customer: string
    status: 'New' | 'In review' | 'Approved' | 'Shipped' | 'Cancelled'
    priority: 'Low' | 'Normal' | 'High'
    assignee: string
  }

  let rows = $state<Order[]>([/* ... */])
  let api  = $state<SvGridApi<typeof features, Order> | null>(null)
  let selected = $state<Order[]>([])

  // Per-action UI state.
  let action = $state<'status' | 'priority' | 'assignee'>('status')
  let value  = $state<string>('Approved')

  const STATUS_OPTIONS    = ['New', 'In review', 'Approved', 'Shipped', 'Cancelled'] as const
  const PRIORITY_OPTIONS  = ['Low', 'Normal', 'High'] as const
  const ASSIGNEE_OPTIONS  = ['Ada', 'Linus', 'Grace', 'Kathleen', '(unassigned)']

  function applyBulk() {
    if (!api || selected.length === 0) return
    // Resolve each selected row's index in the live data array and
    // write through setCellValue. `setCellValue` triggers a single
    // re-render after the batch via the wrapper's state-version bump.
    for (const row of selected) {
      const idx = rows.indexOf(row)
      if (idx < 0) continue
      api.setCellValue(idx, action, value)
    }
    // Refresh `rows` reference so $derived bindings outside the grid
    // (footer counts, etc.) update. The grid itself is already in sync.
    rows = [...rows]
  }
</script>

<header class="flex items-end gap-2 mb-3">
  <strong>{selected.length} selected</strong>
  <label>
    Field
    <select bind:value={action} onchange={() => {
      value = action === 'status'   ? 'Approved'
            : action === 'priority' ? 'Normal'
            :                         'Ada'
    }}>
      <option value="status">Status</option>
      <option value="priority">Priority</option>
      <option value="assignee">Assignee</option>
    </select>
  </label>
  <label>
    Value
    <select bind:value={value}>
      {#each (action === 'status'   ? STATUS_OPTIONS
            : action === 'priority' ? PRIORITY_OPTIONS
            :                         ASSIGNEE_OPTIONS) as opt}
        <option value={opt}>{opt}</option>
      {/each}
    </select>
  </label>
  <button onclick={applyBulk} disabled={selected.length === 0}>
    Apply to {selected.length} rows
  </button>
</header>

<SvGrid
  data={rows}
  columns={[
    { field: 'id',       header: 'Order',    width: 110 },
    { field: 'customer', header: 'Customer', width: 200 },
    { field: 'status',   header: 'Status',   width: 130 },
    { field: 'priority', header: 'Priority', width: 110 },
    { field: 'assignee', header: 'Assignee', width: 140 },
  ] satisfies ColumnDef<typeof features, Order>[]}
  features={features}
  selectionMode="row"
  showRowSelection={true}
  onApiReady={(next) => (api = next)}
  onRowSelectionChange={(_, selectedRows) => (selected = selectedRows)}
/>

Variations

Cycle through values per row

If the bulk action is "increment priority by one level", you read each row's current value first:

const next = (cur: 'Low' | 'Normal' | 'High') =>
  cur === 'Low' ? 'Normal' : cur === 'Normal' ? 'High' : 'High'

for (const row of selected) {
  const idx = rows.indexOf(row)
  if (idx < 0) continue
  api.setCellValue(idx, 'priority', next(row.priority))
}

Confirm before applying

For destructive bulk actions (cancel orders, delete rows), pop a confirm dialog before the loop. Don't rely on the browser's native confirm() - it's blocked by some embedded contexts. A Svelte <dialog> element is the lowest-friction path.

Server-side persistence

Pair the bulk write with a single POST /api/orders/bulk-update request rather than N writes. The grid update is purely client-side; the server roundtrip is independent. If the server fails, roll the grid back by re-applying the previous values from a before snapshot.

async function applyBulkAndPersist() {
  const before = selected.map((r) => ({ ...r }))
  applyBulk() // optimistic local write
  try {
    await fetch('/api/orders/bulk-update', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ ids: selected.map((r) => r.id), [action]: value }),
    })
  } catch (err) {
    // Roll back
    for (const old of before) {
      const idx = rows.indexOf(rows.find((r) => r.id === old.id)!)
      if (idx >= 0) api?.setCellValue(idx, action, (old as any)[action])
    }
    throw err
  }
}

Notes

Try it

Tick some rows, then apply. onRowSelectionChange hands you the selection map and the rows themselves - the rows are the second argument, and they are what a bulk action actually wants.

<script lang="ts">
  import { SvGrid, renderSnippet, type GridColumns } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    department: string
    city: string
    age: number
    salary: number
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   department: 'Engineering', city: 'London',   age: 36, salary: 142000 },
    { id: 2, name: 'Grace Hopper',   department: 'Engineering', city: 'New York', age: 45, salary: 168000 },
    { id: 3, name: 'Linus Torvalds', department: 'Platform',    city: 'Portland', age: 54, salary: 155000 },
    { id: 4, name: 'Radia Perlman',  department: 'Networking',  city: 'Seattle',  age: 49, salary: 161000 },
    { id: 5, name: 'Barbara Liskov', department: 'Platform',    city: 'Boston',   age: 52, salary: 172000 },
  ]

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 190 },
    { field: 'department', header: 'Department', width: 150 },
    { field: 'salary',     header: 'Salary',     width: 130, format: { type: 'currency', currency: 'USD' } },
  ]

  let rows = $state<Person[]>(people.map((p) => ({ ...p })))
  let picked = $state<Person[]>([])

  function raise() {
    const ids = new Set(picked.map((p) => p.id))
    // New objects rather than in-place edits, so the grid sees the change.
    rows = rows.map((r) => (ids.has(r.id) ? { ...r, salary: Math.round(r.salary * 1.1) } : r))
  }
</script>

<button type="button" disabled={picked.length === 0} onclick={raise}>
  Raise {picked.length} selected by 10%
</button>

<SvGrid
  data={rows}
  {columns}
  selectable
  onRowSelectionChange={(_selection, selected) => (picked = selected)}
/>

See also

Related articles

  • Bulk Operations on Selected Rows in SvGrid - Wire row selection into a contextual action toolbar - bulk edit, delete, and export selected rows with the SvGrid selection API.
  • Paste from Excel into a Svelte Data Grid - How to wire up clipboard paste so users can drop a copied Excel or Google Sheets block directly into SvGrid - TSV parsing, type coercion, validation, and row growth all covered.
  • A Fill Handle (Drag to Fill) in SvGrid - Build a working spreadsheet-style fill handle on top of SvGrid's cell selection and editing - pointer tracking, range highlighting, series fill, and undo/redo integration all covered.