Cell Merge

Learn how to visually merge identical values in adjacent rows to create readable, grouped report tables.

#cellMergeOptions#mergeBy#rowspan#reporting#grouping
Last reviewed: 2026-08-23
GitHub
import * as React from 'react';
import { BGrid, BGridColumn, BGridDataItem } from 'beautiful-grid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';

interface InventoryItem {
  mainCategory: string;
  subCategory: string;
  itemCode: string;
  itemName: string;
  unitPrice: number;
  stockQty: number;
  warehouse: string;
}

const inventoryRows: InventoryItem[] = [
  { mainCategory: '가전/디지털', subCategory: '컴퓨터 주변기기', itemCode: 'IT-001', itemName: '프리미엄 무선 키보드', unitPrice: 129000, stockQty: 84, warehouse: 'A-01' },
  { mainCategory: '가전/디지털', subCategory: '컴퓨터 주변기기', itemCode: 'IT-002', itemName: '인체공학 마우스', unitPrice: 69000, stockQty: 46, warehouse: 'A-01' },
  { mainCategory: '가전/디지털', subCategory: '컴퓨터 주변기기', itemCode: 'IT-003', itemName: 'USB-C 멀티 허브', unitPrice: 89000, stockQty: 31, warehouse: 'A-02' },
  { mainCategory: '가전/디지털', subCategory: '모니터/디스플레이', itemCode: 'IT-004', itemName: '27인치 QHD 모니터', unitPrice: 389000, stockQty: 18, warehouse: 'B-01' },
  { mainCategory: '가전/디지털', subCategory: '모니터/디스플레이', itemCode: 'IT-005', itemName: '32인치 4K 모니터', unitPrice: 629000, stockQty: 9, warehouse: 'B-01' },
  { mainCategory: '가구/인테리어', subCategory: '사무용 가구', itemCode: 'FN-001', itemName: '모션 데스크 1400', unitPrice: 459000, stockQty: 22, warehouse: 'C-01' },
  { mainCategory: '가구/인테리어', subCategory: '사무용 가구', itemCode: 'FN-002', itemName: '인체공학 메시 의자', unitPrice: 329000, stockQty: 37, warehouse: 'C-01' },
  { mainCategory: '가구/인테리어', subCategory: '수납 가구', itemCode: 'FN-003', itemName: '이동식 서랍장', unitPrice: 119000, stockQty: 41, warehouse: 'C-02' },
  { mainCategory: '가구/인테리어', subCategory: '수납 가구', itemCode: 'FN-004', itemName: '5단 철제 선반', unitPrice: 149000, stockQty: 26, warehouse: 'C-02' },
  { mainCategory: '생활/주방', subCategory: '홈카페', itemCode: 'KT-001', itemName: '전자동 커피머신', unitPrice: 749000, stockQty: 12, warehouse: 'D-01' },
  { mainCategory: '생활/주방', subCategory: '홈카페', itemCode: 'KT-002', itemName: '온도조절 전기포트', unitPrice: 99000, stockQty: 53, warehouse: 'D-01' },
  { mainCategory: '생활/주방', subCategory: '조리도구', itemCode: 'KT-003', itemName: '스테인리스 팬 세트', unitPrice: 189000, stockQty: 29, warehouse: 'D-02' },
];

const data: BGridDataItem<InventoryItem>[] = inventoryRows.map(values => ({ values }));

function CellMergeExample() {
  const [columns, setColumns] = React.useState<BGridColumn<InventoryItem>[]>([
    { key: 'mainCategory', label: '대분류', width: 130, align: 'center' },
    { key: 'subCategory', label: '중분류', width: 150, align: 'center' },
    { key: 'itemCode', label: '품목코드', width: 100, align: 'center' },
    { key: 'itemName', label: '품목명', width: 220 },
    {
      key: 'unitPrice',
      label: '단가',
      width: 120,
      align: 'right',
      itemRender: ({ values }) => <>{values.unitPrice.toLocaleString()}원</>,
    },
    { key: 'stockQty', label: '재고', width: 80, align: 'right', itemRender: ({ values }) => <>{values.stockQty}개</> },
    { key: 'warehouse', label: '창고', width: 90, align: 'center' },
  ]);
  const containerRef = React.useRef<HTMLDivElement>(null);
  const { width, height } = useContainerSize(containerRef);

  return (
    <DataGridContainer ref={containerRef}>
      <BGrid<InventoryItem>
        showLineNumber
        frozenColumnIndex={2}
        width={width}
        height={height}
        data={data}
        columns={columns}
        rowKey='itemCode'
        onChangeColumns={(_columnIndex, { columns }) => setColumns(columns)}
        cellMergeOptions={{
          columnsMap: {
            0: { mergeBy: 'mainCategory' },
            1: { mergeBy: 'subCategory' },
          },
        }}
        variant='vertical-bordered'
      />
    </DataGridContainer>
  );
}

export default CellMergeExample;

1. When and why should you use cell merging?

Management dashboards, settlement reports, and inventory summaries often repeat the same values—such as category, subcategory, or owner—across several rows. That repetition can make a table harder to scan.

BeautifulGrid’s cell merge feature lets you:

  • Detect consecutive adjacent rows with the same value and display their cells as a visual rowspan.
  • Apply the same columnsMap merge rules to regular and frozen columns.
  • Configure each merge rule with mergeBy so that it follows the sort order of the data.

2. Complete example: sales items grouped by category

The following example merges cells in the Category and Subcategory columns:

import React, { useState } from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';

interface CategoryItem {
  mainCategory: string;
  subCategory: string;
  itemCode: string;
  itemName: string;
  unitPrice: number;
  stockQty: number;
}

export default function CategoryMergeGrid() {
  const [data] = useState<BGridDataItem<CategoryItem>[]>([
    { values: { mainCategory: 'Electronics', subCategory: 'Computer accessories', itemCode: 'IT-01', itemName: 'Wireless keyboard', unitPrice: 45000, stockQty: 120 } },
    { values: { mainCategory: 'Electronics', subCategory: 'Computer accessories', itemCode: 'IT-02', itemName: 'Gaming mouse', unitPrice: 38000, stockQty: 85 } },
    { values: { mainCategory: 'Electronics', subCategory: 'Monitors and displays', itemCode: 'IT-03', itemName: '27-inch 4K monitor', unitPrice: 420000, stockQty: 30 } },
    { values: { mainCategory: 'Electronics', subCategory: 'Monitors and displays', itemCode: 'IT-04', itemName: '32-inch curved monitor', unitPrice: 580000, stockQty: 15 } },
    { values: { mainCategory: 'Furniture and interiors', subCategory: 'Office furniture', itemCode: 'FN-01', itemName: '1400 mm sit-stand desk', unitPrice: 350000, stockQty: 25 } },
    { values: { mainCategory: 'Furniture and interiors', subCategory: 'Office furniture', itemCode: 'FN-02', itemName: 'Ergonomic mesh chair', unitPrice: 280000, stockQty: 40 } },
  ]);

  const columns: BGridColumn<CategoryItem>[] = [
    { key: 'mainCategory', label: 'Category', width: 140, align: 'center' },
    { key: 'subCategory', label: 'Subcategory', width: 160, align: 'center' },
    { key: 'itemCode', label: 'Item code', width: 100, align: 'center' },
    { key: 'itemName', label: 'Item name', width: 200 },
    {
      key: 'unitPrice',
      label: 'Unit price',
      width: 120,
      align: 'right',
      itemRender: ({ values }) => `KRW ${values.unitPrice.toLocaleString('en-US')}`,
    },
    {
      key: 'stockQty',
      label: 'In stock',
      width: 90,
      align: 'right',
      itemRender: ({ values }) => `${values.stockQty} units`,
    },
  ];

  return (
    <div>
      <BGrid<CategoryItem>
        width={810}
        height={320}
        columns={columns}
        data={data}
        rowKey="itemCode"
        frozenColumnIndex={2} // Freeze Category and Subcategory on the left
        // Configure cell merging
        cellMergeOptions={{
          columnsMap: {
            0: { mergeBy: 'mainCategory' }, // Merge Category cells with the same mainCategory value
            1: { mergeBy: 'subCategory' },  // Merge Subcategory cells with the same subCategory value
          },
        }}
        headerHeight={34}
        itemHeight={30}
      />
    </div>
  );
}

3. cellMergeOptions specification

type CellMergeOptions = {
  columnsMap: {
    [columnIndex: number]: BGridCellMergeColumn;
  };
};

interface BGridCellMergeColumn {
  wordWrap?: boolean;
  mergeBy: string | string[]; // Data key used to determine equality
}
  • columnIndex: The zero-based index of the column to merge.
  • mergeBy: The data field whose values are compared between adjacent rows.

4. Practical tips and caveats

[!IMPORTANT] Sort the data before merging: Cells merge only when consecutive adjacent rows have the same value. If a row with mainCategory: 'Furniture' appears between rows whose mainCategory is 'Electronics', the merge is interrupted. Sort the data by the merge keys before passing it to the grid so that each group remains contiguous.