Line Numbers
Display sequential row numbers in the frozen area on the left and keep them aligned with virtual scrolling.
import * as React from 'react';
import { BGrid, BGridColumn, BGridDataItem } from 'beautiful-grid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';
interface FulfillmentOrder {
orderNo: string;
customerName: string;
channel: string;
productName: string;
quantity: number;
amount: number;
status: string;
manager: string;
orderedAt: string;
promisedAt: string;
}
export const LINE_NUMBER_RECORD_COUNT = 2_500;
const customers = ['에이원 리테일', '한빛상사', '모노마켓', '오로라스토어', '동해유통', '새봄리빙'];
const channels = ['자사몰', '스마트스토어', '쿠팡', 'B2B'];
const products = ['프리미엄 무선 키보드', '27인치 QHD 모니터', 'USB-C 멀티 허브', '인체공학 마우스', '노트북 거치대'];
const statuses = ['출고 준비', '피킹 완료', '배송 중', '출고 보류'];
const managers = ['김서준', '이하린', '박도윤', '최지우', '정유진'];
const amountFormatter = new Intl.NumberFormat('ko-KR');
const addDays = (base: Date, days: number) => {
const date = new Date(base);
date.setUTCDate(date.getUTCDate() + days);
return date.toISOString().slice(0, 10);
};
const list: BGridDataItem<FulfillmentOrder>[] = Array.from({ length: LINE_NUMBER_RECORD_COUNT }, (_, index) => {
const quantity = (index % 12) + 1;
const orderedDate = new Date(Date.UTC(2026, 3, 1 + (index % 120)));
return {
values: {
orderNo: `ORD-2026-${String(index + 1).padStart(6, '0')}`,
customerName: customers[index % customers.length],
channel: channels[index % channels.length],
productName: products[index % products.length],
quantity,
amount: quantity * (39_800 + (index % 7) * 7_500),
status: statuses[index % statuses.length],
manager: managers[index % managers.length],
orderedAt: addDays(orderedDate, 0),
promisedAt: addDays(orderedDate, 2 + (index % 4)),
},
};
});
function LineNumberExample() {
const [columns, setColumns] = React.useState<BGridColumn<FulfillmentOrder>[]>([
{ key: 'orderNo', label: '주문번호', width: 140 },
{ key: 'customerName', label: '고객사', width: 140 },
{ key: 'channel', label: '주문채널', width: 100, align: 'center' },
{ key: 'productName', label: '상품명', width: 210 },
{ key: 'quantity', label: '수량', width: 70, align: 'right' },
{
key: 'amount',
label: '주문금액',
width: 120,
align: 'right',
itemRender: ({ values }) => <>{amountFormatter.format(values.amount)}원</>,
},
{ key: 'status', label: '출고상태', width: 100, align: 'center' },
{ key: 'manager', label: '담당자', width: 90, align: 'center' },
{ key: 'orderedAt', label: '주문일', width: 110, align: 'center' },
{ key: 'promisedAt', label: '출고예정일', width: 110, align: 'center' },
]);
const containerRef = React.useRef<HTMLDivElement>(null);
const { width: containerWidth, height: containerHeight } = useContainerSize(containerRef);
return (
<DataGridContainer ref={containerRef}>
<BGrid<FulfillmentOrder>
width={containerWidth}
height={containerHeight}
data={list}
columns={columns}
rowKey='orderNo'
onChangeColumns={(_columnIndex, { columns }) => setColumns(columns)}
onClick={item => console.log(item)}
cellSelectionOptions={{ enabled: true }}
showLineNumber
/>
</DataGridContainer>
);
}
export default LineNumberExample;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 and why is it needed?
When reviewing hundreds or thousands of records, a row-number column makes it easy to see where you are in the dataset. This is a familiar spreadsheet convention.
Set showLineNumber={true} to add a row-number column automatically on the left side of the DataGrid. During virtual scrolling, the Grid efficiently calculates the correct number from 1 through N for the current position.
The live demo above uses 2,500 order and fulfillment records. Scroll down to see the number column automatically reserve enough width for 3- and 4-digit row numbers. Click or drag row numbers to select entire rows. Click or drag a non-sortable column header to select the entire column. Use Shift for contiguous ranges and Ctrl/Cmd for multiple ranges.
2. Practical example: row numbers for a large order dataset
import React, { useState } from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';
interface Order {
orderNo: string;
customerName: string;
status: string;
}
export default function LineNumberGrid() {
const [data] = useState<BGridDataItem<Order>[]>(
Array.from({ length: 2500 }).map((_, i) => ({
values: {
orderNo: `ORD-2026-${String(i + 1).padStart(6, '0')}`,
customerName: ['A-One Retail', 'Hanbit Trading', 'Mono Market'][i % 3],
status: ['Preparing shipment', 'Picking complete', 'In transit'][i % 3],
},
}))
);
const columns: BGridColumn<Order>[] = [
{ key: 'orderNo', label: 'Order No.', width: 140 },
{ key: 'customerName', label: 'Customer', width: 160 },
{ key: 'status', label: 'Fulfillment Status', width: 120, align: 'center' },
];
return (
<div>
<BGrid<Order>
width={650}
height={300}
columns={columns}
data={data}
rowKey="orderNo"
showLineNumber={true} // Show row numbers
/>
</div>
);
}