Summary Row

Use the BGridProps summary configuration to display custom summary cells above or below the data rows.

#summary#BGridSummaryColumn#itemRender#colSpan
Last reviewed: 2026-09-16
LIVE DEMOSummary Row interactive example
Loading the interactive demo…
GitHub
import { t } from './i18n';
import { Radio } from 'antd';
import * as React from 'react';
import { BGrid, BGridColumn, BGridDataItem, BGridProps } from 'beautiful-grid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';

interface SalesOrder {
  channel: string;
  orderNo: string;
  customer: string;
  quantity: number;
  supplyAmount: number;
  taxAmount: number;
  totalAmount: number;
}

const customers = [t('에이원 리테일', 'A1 Retail'), t('한빛상사', 'Hanbit Sangsa'), t('모노마켓', 'Mono Market'), t('오로라스토어', 'Aurora Store')];
const channels = [t('자사몰', 'Own Mall'), t('스마트스토어', 'Smart Store'), 'B2B'];
const data: BGridDataItem<SalesOrder>[] = Array.from({ length: 30 }, (_, index) => {
  const quantity = (index % 8) + 1;
  const supplyAmount = quantity * (48_000 + (index % 5) * 12_000);
  const taxAmount = Math.round(supplyAmount * 0.1);
  return {
    values: {
      channel: channels[index % channels.length],
      orderNo: `ORD-2026-${String(index + 1).padStart(5, '0')}`,
      customer: customers[index % customers.length],
      quantity,
      supplyAmount,
      taxAmount,
      totalAmount: supplyAmount + taxAmount,
    },
  };
});

const formatWon = (value: number) => `${value.toLocaleString()}${t('원', 'KRW')}`;
const sumBy = (rows: BGridDataItem<SalesOrder>[], key: 'quantity' | 'supplyAmount' | 'taxAmount' | 'totalAmount') =>
  rows.reduce((total, item) => total + item.values[key], 0);

const createSummary = (position: 'top' | 'bottom'): BGridProps<SalesOrder>['summary'] => ({
  position,
  rows: [
    {
      columns: [
        {
          columnIndex: 0,
          colSpan: 3,
          align: 'center',
          itemRender: ({ data }) => <strong>{position === 'top' ? t('상단', 'Top') : t('하단', 'Bottom')} {t('매출 합계 ·', 'Total Sales ·')} {data.length}{t('건', 'cases')}</strong>,
        },
        { columnIndex: 3, align: 'right', itemRender: ({ data }) => <strong>{sumBy(data, 'quantity')}{t('개', 'ea')}</strong> },
        { columnIndex: 4, align: 'right', itemRender: ({ data }) => <strong>{formatWon(sumBy(data, 'supplyAmount'))}</strong> },
        { columnIndex: 5, align: 'right', itemRender: ({ data }) => <strong>{formatWon(sumBy(data, 'taxAmount'))}</strong> },
        { columnIndex: 6, align: 'right', itemRender: ({ data }) => <strong>{formatWon(sumBy(data, 'totalAmount'))}</strong> },
      ],
    },
  ],
});

const createMultiSummary = (position: 'top' | 'bottom'): BGridProps<SalesOrder>['summary'] => ({
  position,
  rows: [
    {
      style: { backgroundColor: '#fffbeb', fontWeight: 600 },
      columns: [
        {
          columnIndex: 0,
          colSpan: 3,
          align: 'center',
          itemRender: ({ data }) => (
            <span>
              {position === 'top' ? t('상단', 'Top') : t('하단', 'Bottom')} {t('평균', 'Average')} ({data.length}{t('건', 'cases')})
            </span>
          ),
        },
        {
          columnIndex: 3,
          align: 'right',
          itemRender: ({ data }) => <span>{(sumBy(data, 'quantity') / data.length).toFixed(1)}{t('개', 'ea')}</span>,
        },
        {
          columnIndex: 4,
          align: 'right',
          itemRender: ({ data }) => <span>{formatWon(Math.round(sumBy(data, 'supplyAmount') / data.length))}</span>,
        },
        {
          columnIndex: 5,
          align: 'right',
          itemRender: ({ data }) => <span>{formatWon(Math.round(sumBy(data, 'taxAmount') / data.length))}</span>,
        },
        {
          columnIndex: 6,
          align: 'right',
          itemRender: ({ data }) => <span>{formatWon(Math.round(sumBy(data, 'totalAmount') / data.length))}</span>,
        },
      ],
    },
    {
      style: { backgroundColor: '#f1f5f9', fontWeight: 700 },
      columns: [
        {
          columnIndex: 0,
          colSpan: 3,
          align: 'center',
          itemRender: ({ data }) => (
            <strong>
              {position === 'top' ? t('상단', 'Top') : t('하단', 'Bottom')} {t('합계', 'Total')} ({data.length}{t('건', 'cases')})
            </strong>
          ),
        },
        { columnIndex: 3, align: 'right', itemRender: ({ data }) => <strong>{sumBy(data, 'quantity')}{t('개', 'ea')}</strong> },
        { columnIndex: 4, align: 'right', itemRender: ({ data }) => <strong>{formatWon(sumBy(data, 'supplyAmount'))}</strong> },
        { columnIndex: 5, align: 'right', itemRender: ({ data }) => <strong>{formatWon(sumBy(data, 'taxAmount'))}</strong> },
        { columnIndex: 6, align: 'right', itemRender: ({ data }) => <strong>{formatWon(sumBy(data, 'totalAmount'))}</strong> },
      ],
    },
  ],
});

function SummaryExample() {
  const [position, setPosition] = React.useState<'top' | 'bottom'>('top');
  const [summaryMode, setSummaryMode] = React.useState<'single' | 'multi'>('single');
  const [columns, setColumns] = React.useState<BGridColumn<SalesOrder>[]>([
    { key: 'channel', label: t('주문채널', 'Order Channel'), width: 100, align: 'center' },
    { key: 'orderNo', label: t('주문번호', 'Order Number'), width: 140 },
    { key: 'customer', label: t('고객사', 'Client'), width: 150 },
    { key: 'quantity', label: t('수량', 'Quantity'), width: 80, align: 'right' },
    { key: 'supplyAmount', label: t('공급가액', 'Supply Amount'), width: 130, align: 'right', itemRender: ({ values }) => <>{formatWon(values.supplyAmount)}</> },
    { key: 'taxAmount', label: t('부가세', 'VAT'), width: 110, align: 'right', itemRender: ({ values }) => <>{formatWon(values.taxAmount)}</> },
    { key: 'totalAmount', label: t('결제금액', 'Payment Amount'), width: 140, align: 'right', itemRender: ({ values }) => <strong>{formatWon(values.totalAmount)}</strong> },
  ]);
  const containerRef = React.useRef<HTMLDivElement>(null);
  const { width, height } = useContainerSize(containerRef);

  return (
    <>
      <div style={{ display: 'flex', gap: 16, marginBottom: 10, flexWrap: 'wrap' }}>
        <Radio.Group
          options={[{ label: t('다중 행 요약 (평균+합계)', 'Multi-row (Avg+Sum)'), value: 'multi' }, { label: t('단일 행 요약', 'Single Row'), value: 'single' }]}
          value={summaryMode}
          onChange={event => setSummaryMode(event.target.value)}
        />
        <Radio.Group
          options={[{ label: t('상단 요약', 'Top Summary'), value: 'top' }, { label: t('하단 요약', 'Bottom Summary'), value: 'bottom' }]}
          value={position}
          onChange={event => setPosition(event.target.value)}
        />
      </div>
      <DataGridContainer ref={containerRef}>
        <BGrid<SalesOrder>
          showLineNumber
          frozenColumnIndex={1}
          width={width}
          height={height}
          data={data}
          columns={columns}
          rowKey='orderNo'
          onChangeColumns={(_columnIndex, { columns }) => setColumns(columns)}
          variant='vertical-bordered'
          summary={summaryMode === 'multi' ? createMultiSummary(position) : createSummary(position)}
        />
      </DataGridContainer>
    </>
  );
}

export default SummaryExample;

Summary setup

summary is not a render prop that returns a raw <table> or <tr>. Instead, pass the summary position (position) and an array of summary row definitions (rows). You can also easily configure Multi-row Summary such as average and total rows.

const summary: BGridProps<Row>['summary'] = {
  position: 'bottom', // 'top' | 'bottom'
  rows: [
    // Row 1: Average
    {
      className: 'summary-avg-row',
      columns: [
        { columnIndex: 0, colSpan: 2, align: 'center', itemRender: () => <>Average</> },
        {
          columnIndex: 2,
          align: 'right',
          itemRender: ({ data }) => (
            <>{Math.round(data.reduce((sum, item) => sum + item.values.amount, 0) / (data.length || 1)).toLocaleString()}</>
          ),
        },
      ],
    },
    // Row 2: Total
    {
      className: 'summary-total-row',
      style: { fontWeight: 'bold' },
      columns: [
        { columnIndex: 0, colSpan: 2, align: 'center', itemRender: () => <>Total</> },
        {
          columnIndex: 2,
          align: 'right',
          itemRender: ({ data }) => (
            <>{data.reduce((sum, item) => sum + item.values.amount, 0).toLocaleString()}</>
          ),
        },
      ],
    },
  ],
};

<BGrid
  summary={summary}
  summaryRowHeight={32} // or (rowIndex) => (rowIndex === 1 ? 36 : 28)
  {...props}
/>

Options and property reference

Top-level Summary options (summary)

Property Type Description
position 'top' | 'bottom' Places the summary rows above (top) or below (bottom) data rows.
rows BGridSummaryRow<T>[] Array of summary row definitions. Use this to configure one or more summary rows. (Recommended)
columns BGridSummaryColumn<T>[] Legacy shorthand for a single summary row.

Summary row options (BGridSummaryRow<T>)

Property Type Description
columns BGridSummaryColumn<T>[] Array of summary column cell definitions for this row.
className string Custom CSS class name to apply to the summary <tr> element.
style CSSProperties Inline styles to apply to the summary <tr> element.

Summary cell options (BGridSummaryColumn<T>)

Property Type Description
columnIndex number The zero-based index of the column where the summary cell starts.
colSpan number The number of columns occupied horizontally by the summary cell.
align 'left' | 'center' | 'right' Text alignment inside the summary cell.
className string Custom CSS class name to apply to the summary <td> element.
style CSSProperties Inline styles to apply to the summary <td> element.
itemRender (props) => ReactNode Function that renders the summary cell content. Receives { column, columnIndex, rowIndex, data }.

Summary height configuration

Top-level Prop Type Description
summaryRowHeight number | ((rowIndex: number) => number) Configures the height of each summary row. When a number is passed, it is applied uniformly. When a function is passed, row-specific heights can be set by rowIndex. (Default: 30px)
summaryHeight number Fixes the total height of the summary area. For multi-row summaries, it is divided equally across rows.

Tips & Best Practices

  1. Accessing data (data): Because data in itemRender is typed as BGridDataItem<T>[], read row values safely via item.values.
  2. Backward compatibility: The legacy summary={{ position: 'bottom', columns: [...] }} syntax remains 100% compatible. For new code, rows: [...] is recommended for future extensibility.
  3. Empty trailing cells: Columns not covered by summary cells are filled with empty cells automatically and their right borders are removed for a clean table appearance.