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.
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>
);
}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 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
SpaceorEnter, move it with the arrow keys, and drop it withEnter.
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
rowKeyso 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
dataorder changes during a drag, the current reorder session is canceled and the callback is not called.
6. Restrictions and safety fallback
- Both
showLineNumberandreorder.enabledare 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
rowspancells 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.