Basic DataGrid
Learn basic column configuration, custom cell rendering with itemRender, frozen columns, alignment, and row click handling through a practical example.
import * as React from 'react';
import { BGrid, type BGridColumn, type BGridDataItem, type BGridSortParam } from 'beautiful-grid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';
type FulfillmentPriority = 'URGENT' | 'HIGH' | 'NORMAL';
type FulfillmentStatus = 'ON_HOLD' | 'PICKING' | 'PACKED' | 'SHIPPED';
interface FulfillmentOrder {
orderNo: string;
priority: FulfillmentPriority;
status: FulfillmentStatus;
customer: string;
product: string;
orderedQty: number;
availableQty: number;
warehouse: string;
promisedAt: string;
amount: number;
}
const fulfillmentOrders: BGridDataItem<FulfillmentOrder>[] = [
{
values: {
orderNo: 'SO-260822-1048',
priority: 'URGENT',
status: 'ON_HOLD',
customer: 'ACME 리테일',
product: '산업용 센서 A-100',
orderedQty: 18,
availableQty: 6,
warehouse: '이천 DC',
promisedAt: '2026-08-22 14:00',
amount: 12600000,
},
},
{
values: {
orderNo: 'SO-260822-1049',
priority: 'HIGH',
status: 'PICKING',
customer: '한빛 모빌리티',
product: '제어 모듈 CM-8',
orderedQty: 8,
availableQty: 8,
warehouse: '평택 DC',
promisedAt: '2026-08-22 15:30',
amount: 5840000,
},
},
{
values: {
orderNo: 'SO-260822-1050',
priority: 'NORMAL',
status: 'PACKED',
customer: '오로라 시스템즈',
product: '게이트웨이 GW-20',
orderedQty: 24,
availableQty: 31,
warehouse: '이천 DC',
promisedAt: '2026-08-22 17:00',
amount: 9120000,
},
},
{
values: {
orderNo: 'SO-260822-1051',
priority: 'URGENT',
status: 'ON_HOLD',
customer: '세림 테크',
product: '서보 드라이브 SD-4',
orderedQty: 12,
availableQty: 4,
warehouse: '부산 DC',
promisedAt: '2026-08-22 13:30',
amount: 10800000,
},
},
{
values: {
orderNo: 'SO-260822-1052',
priority: 'HIGH',
status: 'PICKING',
customer: '미래 자동화',
product: 'PLC 확장 모듈 X2',
orderedQty: 30,
availableQty: 30,
warehouse: '평택 DC',
promisedAt: '2026-08-22 18:00',
amount: 7650000,
},
},
{
values: {
orderNo: 'SO-260822-1053',
priority: 'NORMAL',
status: 'SHIPPED',
customer: '대성 로보틱스',
product: '엔코더 EC-12',
orderedQty: 15,
availableQty: 22,
warehouse: '이천 DC',
promisedAt: '2026-08-22 11:00',
amount: 4350000,
},
},
{
values: {
orderNo: 'SO-260822-1054',
priority: 'URGENT',
status: 'ON_HOLD',
customer: '뉴웨이브 에너지',
product: '인버터 IV-75',
orderedQty: 10,
availableQty: 0,
warehouse: '부산 DC',
promisedAt: '2026-08-22 16:00',
amount: 18900000,
},
},
{
values: {
orderNo: 'SO-260822-1055',
priority: 'HIGH',
status: 'PACKED',
customer: '정우 정밀',
product: '리니어 스케일 LS-9',
orderedQty: 6,
availableQty: 9,
warehouse: '평택 DC',
promisedAt: '2026-08-22 19:00',
amount: 3960000,
},
},
{
values: {
orderNo: 'SO-260822-1056',
priority: 'NORMAL',
status: 'PICKING',
customer: '에이스 팩토리',
product: '비전 카메라 VC-3',
orderedQty: 14,
availableQty: 14,
warehouse: '이천 DC',
promisedAt: '2026-08-23 09:00',
amount: 11200000,
},
},
{
values: {
orderNo: 'SO-260822-1057',
priority: 'HIGH',
status: 'ON_HOLD',
customer: '태성 이노텍',
product: '안전 라이트커튼 LC-5',
orderedQty: 20,
availableQty: 13,
warehouse: '부산 DC',
promisedAt: '2026-08-22 20:00',
amount: 6800000,
},
},
{
values: {
orderNo: 'SO-260822-1058',
priority: 'NORMAL',
status: 'PACKED',
customer: '비전 솔루션',
product: 'HMI 패널 H7',
orderedQty: 5,
availableQty: 11,
warehouse: '평택 DC',
promisedAt: '2026-08-23 10:30',
amount: 4750000,
},
},
{
values: {
orderNo: 'SO-260822-1059',
priority: 'HIGH',
status: 'PICKING',
customer: '글로벌 메카',
product: '토크 센서 TS-2',
orderedQty: 16,
availableQty: 16,
warehouse: '이천 DC',
promisedAt: '2026-08-23 12:00',
amount: 8320000,
},
},
];
const priorityView: Record<FulfillmentPriority, { label: string; className: string }> = {
URGENT: { label: '긴급', className: 'bg-rose-100 text-rose-700' },
HIGH: { label: '높음', className: 'bg-amber-100 text-amber-700' },
NORMAL: { label: '보통', className: 'bg-slate-100 text-slate-600' },
};
const statusView: Record<FulfillmentStatus, { label: string; className: string }> = {
ON_HOLD: { label: '출고 보류', className: 'bg-rose-100 text-rose-700' },
PICKING: { label: '피킹 중', className: 'bg-blue-100 text-blue-700' },
PACKED: { label: '포장 완료', className: 'bg-violet-100 text-violet-700' },
SHIPPED: { label: '출고 완료', className: 'bg-emerald-100 text-emerald-700' },
};
const initialColumns: BGridColumn<FulfillmentOrder>[] = [
{ key: 'orderNo', label: '주문번호', width: 145 },
{
key: 'priority',
label: '우선순위',
width: 90,
align: 'center',
itemRender: ({ value }) => {
const view = priorityView[value as FulfillmentPriority];
return (
<span className={`inline-flex rounded-full px-2 py-0.5 text-xs font-semibold ${view.className}`}>
{view.label}
</span>
);
},
},
{
key: 'status',
label: '처리상태',
width: 105,
align: 'center',
itemRender: ({ value }) => {
const view = statusView[value as FulfillmentStatus];
return (
<span className={`inline-flex rounded-full px-2 py-0.5 text-xs font-semibold ${view.className}`}>
{view.label}
</span>
);
},
},
{ key: 'customer', label: '고객사', width: 145 },
{ key: 'product', label: '상품', width: 185 },
{
key: 'orderedQty',
label: '주문수량',
width: 95,
align: 'right',
itemRender: ({ value }) => <>{Number(value).toLocaleString()}개</>,
},
{
key: 'availableQty',
label: '가용재고',
width: 95,
align: 'right',
itemRender: ({ value, values }) => (
<strong className={values.availableQty < values.orderedQty ? 'text-rose-600' : 'text-emerald-700'}>
{Number(value).toLocaleString()}개
</strong>
),
},
{ key: 'warehouse', label: '출고센터', width: 105, align: 'center' },
{ key: 'promisedAt', label: '출고 약속일', width: 155, align: 'center' },
{
key: 'amount',
label: '주문금액',
width: 135,
align: 'right',
itemRender: ({ value }) => <strong>{Number(value).toLocaleString()}원</strong>,
},
];
function BasicExample() {
const containerRef = React.useRef<HTMLDivElement>(null);
const { width: containerWidth, height: containerHeight } = useContainerSize(containerRef);
const [columns, setColumns] = React.useState(initialColumns);
const [sortParams, setSortParams] = React.useState<BGridSortParam[]>([]);
const [checkedRowKeys, setCheckedRowKeys] = React.useState<React.Key[]>([]);
const [focusedOrderNo, setFocusedOrderNo] = React.useState(fulfillmentOrders[0].values.orderNo);
const sortedOrders = React.useMemo(() => {
const [sort] = sortParams;
if (!sort?.key) return fulfillmentOrders;
const key = sort.key as keyof FulfillmentOrder;
return [...fulfillmentOrders].sort((a, b) => {
const left = a.values[key];
const right = b.values[key];
const result =
typeof left === 'number' && typeof right === 'number'
? left - right
: String(left).localeCompare(String(right), 'ko');
return sort.orderBy === 'asc' ? result : -result;
});
}, [sortParams]);
return (
<div className='flex min-h-0 flex-col gap-3'>
<div className='flex flex-wrap items-center justify-between gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600'>
<div>
<strong className='text-slate-900'>주문 출고 예외 관리</strong>
<span className='ml-2'>재고 부족과 마감 임박 주문을 한 화면에서 우선 처리합니다.</span>
</div>
<span aria-live='polite'>
검토 선택 {checkedRowKeys.length}건 · 현재 주문 {focusedOrderNo}
</span>
</div>
<DataGridContainer ref={containerRef} style={{ height: 400 }}>
<BGrid<FulfillmentOrder>
width={containerWidth}
height={containerHeight}
headerHeight={36}
itemHeight={18}
data={sortedOrders}
columns={columns}
rowKey='orderNo'
frozenColumnIndex={3}
showLineNumber
rowChecked={{
checkedRowKeys,
onChange: (_indexes, rowKeys) => setCheckedRowKeys(rowKeys),
}}
sort={{ sortParams, onChange: setSortParams }}
cellSelectionOptions={{ enabled: true }}
cellNavigationOptions={{ enabled: true, defaultActiveCell: { rowIndex: 0, columnIndex: 0 } }}
onChangeColumns={(_columnIndex, info) => setColumns(info.columns)}
onClick={({ item }) => setFocusedOrderNo(item.orderNo)}
/>
</DataGridContainer>
</div>
);
}
export default BasicExample;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,
};
}1. When should you use this pattern, and what will you learn?
A common business application needs to present order lists, account ledgers, or member directories with several data formats—currency, dates, status badges, and tags—while keeping key columns such as the order number or customer name visible during horizontal scrolling.
This guide covers four core techniques:
- Custom cell rendering (
itemRender): Turn raw values into badges, links, and formatted currency. - Frozen columns (
frozenColumnIndex): Keep the first 1–N columns visible while scrolling horizontally. - Column alignment (
align) and width: Apply consistent alignment rules for text, numbers, codes, and dates. - Row click handling (
onClick): Open a detail modal or popup when the user selects a row.
2. Complete example: order management
The following component models an e-commerce order-management screen:
import React, { useState } from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
interface OrderItem {
orderNo: string;
customerName: string;
productName: string;
orderDate: string;
amount: number;
status: 'PENDING' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED';
}
const statusBadgeStyles: Record<string, { bg: string; color: string; label: string }> = {
PENDING: { bg: '#fef3c7', color: '#92400e', label: 'Payment complete' },
SHIPPED: { bg: '#e0f2fe', color: '#075985', label: 'In transit' },
DELIVERED: { bg: '#dcfce7', color: '#166534', label: 'Delivered' },
CANCELLED: { bg: '#fee2e2', color: '#991b1b', label: 'Cancelled' },
};
export default function OrderListGrid() {
const [selectedOrder, setSelectedOrder] = useState<OrderItem | null>(null);
// 1. Configure the columns
const columns: BGridColumn<OrderItem>[] = [
{
key: 'orderNo',
label: 'Order number',
width: 130,
align: 'center',
itemRender: ({ values }) => (
<span style={{ fontWeight: 600, color: '#2563eb', cursor: 'pointer' }}>
{values.orderNo}
</span>
),
},
{
key: 'customerName',
label: 'Customer',
width: 120,
align: 'left',
},
{
key: 'productName',
label: 'Product',
width: 250,
align: 'left',
},
{
key: 'amount',
label: 'Amount',
width: 130,
align: 'right',
itemRender: ({ values }) => (
<span style={{ fontWeight: 600 }}>
KRW {values.amount.toLocaleString('en-US')}
</span>
),
},
{
key: 'status',
label: 'Status',
width: 110,
align: 'center',
itemRender: ({ values }) => {
const badge = statusBadgeStyles[values.status];
return (
<span style={{
padding: '2px 8px',
borderRadius: '12px',
fontSize: '13px',
fontWeight: 600,
backgroundColor: badge.bg,
color: badge.color,
}}>
{badge.label}
</span>
);
},
},
{
key: 'orderDate',
label: 'Ordered at',
width: 160,
align: 'center',
},
];
// 2. Provide the row data
const data: BGridDataItem<OrderItem>[] = [
{ values: { orderNo: 'ORD-2026-001', customerName: 'Alex Morgan', productName: 'Wireless mechanical keyboard', amount: 159000, status: 'DELIVERED', orderDate: '2026-08-15 14:22' } },
{ values: { orderNo: 'ORD-2026-002', customerName: 'Jamie Park', productName: '27-inch 4K monitor', amount: 489000, status: 'SHIPPED', orderDate: '2026-08-16 09:15' } },
{ values: { orderNo: 'ORD-2026-003', customerName: 'Taylor Kim', productName: 'Ergonomic vertical mouse', amount: 69000, status: 'PENDING', orderDate: '2026-08-17 11:40' } },
{ values: { orderNo: 'ORD-2026-004', customerName: 'Jordan Lee', productName: 'USB-C multiport hub', amount: 45000, status: 'CANCELLED', orderDate: '2026-08-17 13:02' } },
];
return (
<div>
<BGrid<OrderItem>
width={800}
height={320}
columns={columns}
data={data}
rowKey="orderNo"
frozenColumnIndex={2} // Freeze the order-number and customer columns on the left
headerHeight={36}
itemHeight={32}
onClick={({ item, index }) => {
setSelectedOrder(item);
console.log(`Selected row index: ${index}`, item);
}}
/>
{selectedOrder && (
<div style={{ marginTop: 12, padding: 12, backgroundColor: '#f1f5f9', borderRadius: 8, fontSize: 13 }}>
Selected order: <strong>{selectedOrder.orderNo}</strong> ({selectedOrder.customerName} / KRW {selectedOrder.amount.toLocaleString('en-US')})
</div>
)}
</div>
);
}
3. Key props
| Prop | Type | Default | Practical meaning |
|---|---|---|---|
columns |
BGridColumn<T>[] |
[] (required) |
The column definitions for header labels, widths, alignment, and custom renderers. |
data |
BGridDataItem<T>[] |
[] (required) |
The row data. Each item must be wrapped in { values: T }. |
frozenColumnIndex |
number |
0 |
Freezes every column whose index is lower than this value. For example, 2 freezes columns 0 and 1. |
headerHeight |
number |
30 |
The height of the column-header area in pixels. Adjust it for the font size or multi-row headers. |
itemHeight |
number |
15 |
The base height of the cell-content area in pixels. The grid uses it for virtual-scroll calculations. |
itemPadding |
number |
7 |
The vertical spacing added to each row. Confirm the resulting row height with the active theme. |
onClick |
(params) => void |
undefined |
Called when a cell is clicked. It receives { item, index, columnIndex, column }. |
4. Using the custom cell renderer (itemRender)
BGridColumn.itemRender lets you render a React node inside a cell instead of displaying only the raw value.
Callback parameters
itemRender?: (params: {
item: BGridDataItem<T>; // Full row wrapper ({ values, status, checked })
values: T; // Business data object for the current row
value: any; // Cell value resolved from the column key
column: BGridColumn<T>; // Current column definition
index: number; // Current displayed row index
columnIndex: number; // Column index
handleSave?: (value: any) => void; // Save trigger in edit mode
handleCancel?: () => void; // Cancel trigger in edit mode
}) => React.ReactNode;
Recommended patterns
- Currency and numbers: Format values with
values.amount.toLocaleString(). - Status badges: Render a tag based on
values.status. - Action buttons: Add per-row edit or delete controls. Call
event.stopPropagation()from the button handler to prevent the row-levelonClickevent from firing as well.
5. Practical tips and caveats
[!TIP] Check horizontal-scroll performance with frozen columns: BeautifulGrid renders the frozen and scrollable regions as separate components. If cell renderers are complex or the grid has many columns, verify scroll synchronization in the target browsers and with a realistic data volume.
[!WARNING] Control event propagation inside cells: If clicking a
<button>or<input>insideitemRendershould not also trigger the grid’s row-levelonClickevent, callevent.stopPropagation()in the control’s handler.