Summary Row
Use the BGridProps summary configuration to display custom summary cells above or below the data rows.
LIVE DEMOSummary Row interactive example
Loading the interactive demo…
LIVE DEMOSummary Row interactive example
Loading the interactive demo…
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;import * as React from 'react';
import './DataGridContainer.css';
interface DataGridContainerProps extends React.HTMLAttributes<HTMLDivElement> {
children?: React.ReactNode;
}
/**
* Keeps a DataGrid in a measured, fixed layout box.
*
* BGrid's rendered root is absolutely positioned within this relative
* container. This makes a ResizeObserver measurement authoritative when a
* surrounding flex or grid layout shrinks as well as when it expands.
*/
const DataGridContainer = React.forwardRef<HTMLDivElement, DataGridContainerProps>(
({ className, ...rest }, ref) => (
<div ref={ref} className={`data-grid-container ${className ?? ''}`.trim()} {...rest} />
),
);
DataGridContainer.displayName = 'DataGridContainer';
export default DataGridContainer;.data-grid-container {
position: relative;
width: 100%;
height: 400px;
overflow: hidden;
font-size: 13px;
}
.data-grid-container > .bgrid-root {
position: absolute;
inset: 0;
}import * as React from 'react';
export function useContainerSize(ref: React.MutableRefObject<HTMLElement | null>, additionalDeps: unknown[] = []) {
const [width, setWidth] = React.useState(0);
const [height, setHeight] = React.useState(0);
const resizeObserver = React.useRef(
new ResizeObserver(entries => {
if (entries.length !== 1) {
throw new Error('Invalid Container length');
}
const [entry] = entries;
const { width, height } = entry.contentRect;
setWidth(width);
setHeight(height);
}),
);
React.useEffect(() => {
if (!ref.current) return;
const observer = resizeObserver.current;
const element = ref.current;
setWidth(element.clientWidth);
setHeight(element.clientHeight);
observer.observe(element);
return () => {
observer.unobserve(element);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...additionalDeps, ref]);
return {
width,
height,
};
}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
- Accessing data (
data): BecausedatainitemRenderis typed asBGridDataItem<T>[], read row values safely viaitem.values. - Backward compatibility: The legacy
summary={{ position: 'bottom', columns: [...] }}syntax remains 100% compatible. For new code,rows: [...]is recommended for future extensibility. - 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.