Row Reorder

Move rows by pointer or keyboard from the handle on the left and safely persist the order with virtual scrolling, selection, and merged cells.

#reorder#drag-handle#onReorder#pointer-drag#keyboard#virtual-scroll
Last reviewed: 2026-08-23
GitHub
import * as React from 'react';
import { BGrid, BGridColumn, BGridDataItem } from 'beautiful-grid';
import { useContainerSize } from '../hooks/useContainerSize';
import DataGridContainer from '../components/DataGridContainer';

interface BannerPriority {
  bannerCode: string;
  title: string;
  placement: string;
  audience: string;
  period: string;
  status: '노출 중' | '예약' | '비활성';
}

const bannerTitles = [
  '여름 시즌 최대 40% 프로모션',
  '신규 회원 웰컴 쿠폰 안내',
  '프리미엄 멤버십 오픈',
  '오늘 주문 내일 도착 캠페인',
  '카카오페이 즉시 할인',
  '리뷰 작성 포인트 두 배 적립',
  '주말 한정 타임 세일',
  '친구 초대 리워드 이벤트',
];
const placements = ['메인 히어로', '홈 중단', '카테고리 상단', '앱 팝업'];
const audiences = ['전체 고객', '신규 회원', 'VIP 회원', '최근 구매 고객'];

const initialBanners: BGridDataItem<BannerPriority>[] = Array.from({ length: 24 }, (_, index) => ({
  values: {
    bannerCode: `BNR-${String(index + 1).padStart(3, '0')}`,
    title: bannerTitles[index % bannerTitles.length],
    placement: placements[index % placements.length],
    audience: audiences[index % audiences.length],
    period: `2026-08-${String((index % 20) + 1).padStart(2, '0')} ~ 2026-09-${String((index % 9) + 1).padStart(2, '0')}`,
    status: index % 6 === 5 ? '비활성' : index % 4 === 3 ? '예약' : '노출 중',
  },
}));

export default function ReorderExample() {
  const [data, setData] = React.useState(initialBanners);
  const [columns, setColumns] = React.useState<BGridColumn<BannerPriority>[]>([
    { key: 'bannerCode', label: '배너코드', width: 100, align: 'center' },
    { key: 'title', label: '배너 제목', width: 260 },
    { key: 'placement', label: '노출 위치', width: 120, align: 'center' },
    { key: 'audience', label: '대상 고객', width: 120, align: 'center' },
    { key: 'period', label: '노출 기간', width: 210, align: 'center' },
    {
      key: 'status',
      label: '노출 상태',
      width: 100,
      align: 'center',
      itemRender: ({ values }) => <strong>{values.status}</strong>,
    },
  ]);
  const containerRef = React.useRef<HTMLDivElement>(null);
  const { width, height } = useContainerSize(containerRef);

  return (
    <DataGridContainer ref={containerRef}>
      <BGrid<BannerPriority>
        width={width}
        height={height}
        data={data}
        columns={columns}
        rowKey='bannerCode'
        showLineNumber
        columnSortable={false}
        onChangeColumns={(_columnIndex, { columns }) => setColumns(columns)}
        reorder={{
          enabled: true,
          onReorder: reorderedData => {
            setData(reorderedData);
            return true;
          },
        }}
      />
    </DataGridContainer>
  );
}

1. When and why should you use it?

Drag-and-drop row reordering is essential wherever users must arrange and save items themselves—for example, menu trees, banner priorities, and task lists.

When you enable BeautifulGrid’s reorder feature:

  • A dedicated drag handle (grip-vertical) appears automatically at the left of the row-number area.
  • The source row and intervening rows animate with a 150 ms transform, previewing the destination before the drop.
  • After the drop animation finishes, the grid commits the new array once and passes it to onReorder.
  • Virtual scrolling supports automatic edge scrolling and a preview for a source row that moves off screen.
  • Keyboard users can focus the handle, pick up the row with Space or Enter, move it with the arrow keys, and drop it with Enter.

2. Complete example: managing banner display order

import React, { useState } from 'react';
import { BGrid, type BGridColumn, type BGridDataItem } from 'beautiful-grid';

interface BannerItem {
  id: number;
  title: string;
  linkUrl: string;
  active: boolean;
}

export default function BannerReorderGrid() {
  const [data, setData] = useState<BGridDataItem<BannerItem>[]>([
    { values: { id: 1, title: 'Summer promotion banner', linkUrl: '/events/summer', active: true } },
    { values: { id: 2, title: '10% welcome coupon for new members', linkUrl: '/welcome', active: true } },
    { values: { id: 3, title: '$5 instant discount with Kakao Pay', linkUrl: '/events/kakaopay', active: false } },
    { values: { id: 4, title: 'Premium membership launch event', linkUrl: '/membership', active: true } },
  ]);

  const columns: BGridColumn<BannerItem>[] = [
    { key: 'id', label: 'ID', width: 60, align: 'center' },
    { key: 'title', label: 'Banner Title', width: 280 },
    { key: 'linkUrl', label: 'Link URL', width: 180 },
    {
      key: 'active',
      label: 'Visible',
      width: 90,
      align: 'center',
      itemRender: ({ values }) => (
        <span style={{ color: values.active ? '#16a34a' : '#94a3b8', fontWeight: 600 }}>
          {values.active ? 'Visible' : 'Inactive'}
        </span>
      ),
    },
  ];

  return (
    <div>
      <div style={{ marginBottom: 10, fontSize: 13, color: '#475569' }}>
        💡 Drag the six-dot handle at the left of a row up or down to change its position.
      </div>

      <BGrid<BannerItem>
        width={720}
        height={260}
        columns={columns}
        data={data}
        rowKey='id'
        showLineNumber={true} // Show the row-number area (required)
        reorder={{
          enabled: true, // Enable drag-and-drop row reordering
          onReorder: (newData: BGridDataItem<BannerItem>[]) => {
            console.log('Reorder complete:', newData.map(d => d.values.title));
            setData(newData);
            return true; // Return true on success
          },
        }}
      />
    </div>
  );
}

3. Input methods

Input Action
Mouse, pen, or touch Hold the handle, move it up or down, and release it to confirm the new position.
Space / Enter Pick up the row with the focused handle, or drop it at the current position.
ArrowUp / ArrowDown Move the keyboard destination one row at a time.
Escape Cancel and return the row to its original position.

A short click that does not start a drag leaves the data order unchanged. onReorder is not called while the row is moving; it is called exactly once with the final array after the rows settle. When prefers-reduced-motion: reduce is active, both the animation and the commit delay are removed.

4. The onReorder contract and save failures

onReorder is a synchronous callback. Return true or void to keep the new order; return false to restore the original internal data, checkbox state, and active-cell state. If the callback throws, the grid performs the same cleanup and rollback. The callback does not support a pending state that waits for a server-save Promise, so screens that persist the order remotely should implement optimistic updates and recovery from save failures in application state.

The moved wrapper is marked with status: edit. The input data array and existing row wrappers are not mutated directly.

5. Selection, editing and action identifiers

  • Specify rowKey so the grid can reliably determine whether the row order is still the same if an external render occurs during a drag.
  • Checkbox state and the active cell are remapped by the moved data item, not by index. The multi-cell selection range is cleared after a successful reorder so it cannot point to the wrong cells.
  • While a cell editor is open, the handle is disabled so an unsaved draft is not discarded silently.
  • If the external data order changes during a drag, the current reorder session is canceled and the callback is not called.

6. Restrictions and safety fallback

  • Both showLineNumber and reorder.enabled are required.
  • Client-side sorting or filtering produces a display order that differs from the source array, so reordering is disabled automatically.
  • Reordering is also disabled when rows are frozen or pivot results are being rendered.
  • Transforming rowspan cells in a merged table can make them overlap. In that case, the grid leaves the row cells in place, marks the destination with a lightweight preview and insertion guide, and then applies the same data permutation.

You can customize the interaction by overriding --bgrid-row-reorder-duration, --bgrid-row-reorder-easing, --bgrid-row-reorder-guide-color, --bgrid-row-reorder-preview-bg, and --bgrid-row-reorder-preview-shadow in your theme.