Basic DataGrid

Learn basic column configuration, custom cell rendering with itemRender, frozen columns, alignment, and row click handling through a practical example.

#columns#itemRender#frozen-columns#onClick#align
Last reviewed: 2026-08-17
GitHub
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;

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:

  1. Custom cell rendering (itemRender): Turn raw values into badges, links, and formatted currency.
  2. Frozen columns (frozenColumnIndex): Keep the first 1–N columns visible while scrolling horizontally.
  3. Column alignment (align) and width: Apply consistent alignment rules for text, numbers, codes, and dates.
  4. 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;
  • 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-level onClick event 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> inside itemRender should not also trigger the grid’s row-level onClick event, call event.stopPropagation() in the control’s handler.