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
LIVE DEMOBasic DataGrid interactive example
Loading the interactive demo…
GitHub
import { t } from './i18n';
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: t('ACME 리테일', 'ACME Retail'),
      product: t('산업용 센서 A-100', 'Industrial Sensor A-100'),
      orderedQty: 18,
      availableQty: 6,
      warehouse: t('이천 DC', 'Icheon DC'),
      promisedAt: '2026-08-22 14:00',
      amount: 12600000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1049',
      priority: 'HIGH',
      status: 'PICKING',
      customer: t('한빛 모빌리티', 'Hanbit Mobility'),
      product: t('제어 모듈 CM-8', 'Control Module CM-8'),
      orderedQty: 8,
      availableQty: 8,
      warehouse: t('평택 DC', 'Pyeongtaek DC'),
      promisedAt: '2026-08-22 15:30',
      amount: 5840000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1050',
      priority: 'NORMAL',
      status: 'PACKED',
      customer: t('오로라 시스템즈', 'Aurora Systems'),
      product: t('게이트웨이 GW-20', 'Gateway GW-20'),
      orderedQty: 24,
      availableQty: 31,
      warehouse: t('이천 DC', 'Icheon DC'),
      promisedAt: '2026-08-22 17:00',
      amount: 9120000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1051',
      priority: 'URGENT',
      status: 'ON_HOLD',
      customer: t('세림 테크', 'Serim Tech'),
      product: t('서보 드라이브 SD-4', 'Servo Drive SD-4'),
      orderedQty: 12,
      availableQty: 4,
      warehouse: t('부산 DC', 'Busan DC'),
      promisedAt: '2026-08-22 13:30',
      amount: 10800000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1052',
      priority: 'HIGH',
      status: 'PICKING',
      customer: t('미래 자동화', 'Mirae Automation'),
      product: t('PLC 확장 모듈 X2', 'PLC Expansion Module X2'),
      orderedQty: 30,
      availableQty: 30,
      warehouse: t('평택 DC', 'Pyeongtaek DC'),
      promisedAt: '2026-08-22 18:00',
      amount: 7650000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1053',
      priority: 'NORMAL',
      status: 'SHIPPED',
      customer: t('대성 로보틱스', 'Daesung Robotics'),
      product: t('엔코더 EC-12', 'Encoder EC-12'),
      orderedQty: 15,
      availableQty: 22,
      warehouse: t('이천 DC', 'Icheon DC'),
      promisedAt: '2026-08-22 11:00',
      amount: 4350000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1054',
      priority: 'URGENT',
      status: 'ON_HOLD',
      customer: t('뉴웨이브 에너지', 'New Wave Energy'),
      product: t('인버터 IV-75', 'Inverter IV-75'),
      orderedQty: 10,
      availableQty: 0,
      warehouse: t('부산 DC', 'Busan DC'),
      promisedAt: '2026-08-22 16:00',
      amount: 18900000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1055',
      priority: 'HIGH',
      status: 'PACKED',
      customer: t('정우 정밀', 'Jungwoo Precision'),
      product: t('리니어 스케일 LS-9', 'Linear Scale LS-9'),
      orderedQty: 6,
      availableQty: 9,
      warehouse: t('평택 DC', 'Pyeongtaek DC'),
      promisedAt: '2026-08-22 19:00',
      amount: 3960000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1056',
      priority: 'NORMAL',
      status: 'PICKING',
      customer: t('에이스 팩토리', 'Ace Factory'),
      product: t('비전 카메라 VC-3', 'Vision Camera VC-3'),
      orderedQty: 14,
      availableQty: 14,
      warehouse: t('이천 DC', 'Icheon DC'),
      promisedAt: '2026-08-23 09:00',
      amount: 11200000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1057',
      priority: 'HIGH',
      status: 'ON_HOLD',
      customer: t('태성 이노텍', 'Taesung Innotek'),
      product: t('안전 라이트커튼 LC-5', 'Safety Light Curtain LC-5'),
      orderedQty: 20,
      availableQty: 13,
      warehouse: t('부산 DC', 'Busan DC'),
      promisedAt: '2026-08-22 20:00',
      amount: 6800000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1058',
      priority: 'NORMAL',
      status: 'PACKED',
      customer: t('비전 솔루션', 'Vision Solution'),
      product: t('HMI 패널 H7', 'HMI Panel H7'),
      orderedQty: 5,
      availableQty: 11,
      warehouse: t('평택 DC', 'Pyeongtaek DC'),
      promisedAt: '2026-08-23 10:30',
      amount: 4750000,
    },
  },
  {
    values: {
      orderNo: 'SO-260822-1059',
      priority: 'HIGH',
      status: 'PICKING',
      customer: t('글로벌 메카', 'Global Mecca'),
      product: t('토크 센서 TS-2', 'Torque Sensor TS-2'),
      orderedQty: 16,
      availableQty: 16,
      warehouse: t('이천 DC', 'Icheon DC'),
      promisedAt: '2026-08-23 12:00',
      amount: 8320000,
    },
  },
];

const priorityView: Record<FulfillmentPriority, { label: string; className: string }> = {
  URGENT: { label: t('긴급', 'Urgent'), className: 'bg-rose-100 text-rose-700' },
  HIGH: { label: t('높음', 'High'), className: 'bg-amber-100 text-amber-700' },
  NORMAL: { label: t('보통', 'Normal'), className: 'bg-slate-100 text-slate-600' },
};

const statusView: Record<FulfillmentStatus, { label: string; className: string }> = {
  ON_HOLD: { label: t('출고 보류', 'Dispatch Pending'), className: 'bg-rose-100 text-rose-700' },
  PICKING: { label: t('피킹 중', 'Picking in Progress'), className: 'bg-blue-100 text-blue-700' },
  PACKED: { label: t('포장 완료', 'Packaging Completed'), className: 'bg-violet-100 text-violet-700' },
  SHIPPED: { label: t('출고 완료', 'Dispatch Completed'), className: 'bg-emerald-100 text-emerald-700' },
};

const initialColumns: BGridColumn<FulfillmentOrder>[] = [
  { key: 'orderNo', label: t('주문번호', 'Order Number'), width: 145 },
  {
    key: 'priority',
    label: t('우선순위', 'Priority'),
    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: t('처리상태', 'Processing Status'),
    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: t('고객사', 'Client'), width: 145 },
  { key: 'product', label: t('상품', 'Product'), width: 185 },
  {
    key: 'orderedQty',
    label: t('주문수량', 'Order Quantity'),
    width: 95,
    align: 'right',
    itemRender: ({ value }) => <>{Number(value).toLocaleString()}{t('개', 'ea')}</>,
  },
  {
    key: 'availableQty',
    label: t('가용재고', 'Available Stock'),
    width: 95,
    align: 'right',
    itemRender: ({ value, values }) => (
      <strong className={values.availableQty < values.orderedQty ? 'text-rose-600' : 'text-emerald-700'}>
        {Number(value).toLocaleString()}{t('개', 'ea')}
      </strong>
    ),
  },
  { key: 'warehouse', label: t('출고센터', 'Dispatch Center'), width: 105, align: 'center' },
  { key: 'promisedAt', label: t('출고 약속일', 'Promised Dispatch Date'), width: 155, align: 'center' },
  {
    key: 'amount',
    label: t('주문금액', 'Order Amount'),
    width: 135,
    align: 'right',
    itemRender: ({ value }) => <strong>{Number(value).toLocaleString()}{t('원', 'KRW')}</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'>{t('주문 출고 예외 관리', 'Order Dispatch Exception Management')}</strong>
          <span className='ml-2'>{t('재고 부족과 마감 임박 주문을 한 화면에서 우선 처리합니다.', 'Handle out-of-stock and impending deadline orders with priority on one screen.')}</span>
        </div>
        <span aria-live='polite'>
          {t('검토 선택', 'Selected for review')} {checkedRowKeys.length}{t('건 · 현재 주문', ' items · Current Order')} {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={containerWidth > 0 && containerWidth < 640 ? 1 : 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

[!WARNING] Responsive frozen columns on mobile screens: On narrow viewports such as mobile devices, freezing multiple columns can occupy most of the visible width, making it appear that horizontal scrolling is broken. Consider adjusting frozenColumnIndex responsively based on container width (e.g. containerWidth < 640 ? 1 : 3) to preserve sufficient scrollable area.

[!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.