Column Visibility
Hide columns from the header toolbox, restore them from another column menu, or show every hidden column at once.
import * as React from 'react';
import { useMemo, useState } from 'react';
import { Button, Tag } from 'antd';
import { Eye, RotateCcw } from 'lucide-react';
import { BGrid } from 'beautiful-grid';
import type { BGridColumn, BGridColumnGroupNode, BGridDataQuery } from 'beautiful-grid';
import DataGridContainer from '../components/DataGridContainer';
import { useContainerSize } from '../hooks/useContainerSize';
import { t, exampleMsg } from './i18n';
interface OrderRow {
orderNo: string;
customer: string;
region: string;
status: string;
owner: string;
dueDate: string;
amount: number;
}
const customers = ['Northwind', 'Contoso', 'Fabrikam', 'Adventure Works', 'Tailspin'];
const regions = [t('서울', 'Seoul'), t('부산', 'Busan'), t('대전', 'Daejeon'), t('광주', 'Gwangju')];
const statuses = [t('접수', 'Received'), t('처리 중', 'Processing'), t('출고', 'Shipped')];
const owners = [t('김서준', 'Alex Kim'), t('이하은', 'Jamie Lee'), t('박지후', 'Chris Park')];
const rows = Array.from({ length: 24 }, (_, index) => ({
values: {
orderNo: `ORD-${String(index + 1).padStart(4, '0')}`,
customer: customers[index % customers.length],
region: regions[index % regions.length],
status: statuses[index % statuses.length],
owner: owners[index % owners.length],
dueDate: `2026-09-${String((index % 20) + 8).padStart(2, '0')}`,
amount: 128000 + index * 17500,
},
}));
const groups: BGridColumnGroupNode[] = [
{
id: 'order',
label: t('주문 정보', 'Order'),
children: ['orderNo', 'customer'],
},
{
id: 'delivery',
label: t('처리 정보', 'Fulfillment'),
children: ['region', 'status', 'owner', 'dueDate'],
},
];
export default function ColumnVisibilityExample() {
const containerRef = React.useRef<HTMLDivElement>(null);
const { width, height } = useContainerSize(containerRef);
const [hiddenColumnIds, setHiddenColumnIds] = useState<string[]>(['owner']);
const [query, setQuery] = useState<BGridDataQuery>({
sortParams: [],
filterParams: [],
});
const columns = useMemo<BGridColumn<OrderRow>[]>(
() => [
{
id: 'orderNo',
key: 'orderNo',
label: t('주문 번호', 'Order No.'),
width: 120,
hideable: false,
toolbox: true,
filter: { type: 'text' },
},
{
id: 'customer',
key: 'customer',
label: t('고객사', 'Customer'),
width: 150,
toolbox: true,
filter: { type: 'text' },
},
{
id: 'region',
key: 'region',
label: t('권역', 'Region'),
width: 100,
align: 'center',
toolbox: true,
filter: { type: 'values' },
},
{
id: 'status',
key: 'status',
label: t('상태', 'Status'),
width: 105,
align: 'center',
toolbox: true,
filter: { type: 'values' },
},
{
id: 'owner',
key: 'owner',
label: t('담당자', 'Owner'),
width: 110,
align: 'center',
toolbox: true,
filter: { type: 'values' },
},
{
id: 'dueDate',
key: 'dueDate',
label: t('출고 예정일', 'Due date'),
width: 120,
align: 'center',
toolbox: true,
filter: { type: 'text' },
},
{
id: 'amount',
key: 'amount',
label: t('주문 금액', 'Amount'),
width: 130,
align: 'right',
toolbox: true,
filter: { type: 'number' },
itemRender: ({ value }) => `${Number(value).toLocaleString()}${t('원', ' KRW')}`,
},
],
[],
);
return (
<>
<div className='flex flex-wrap items-center justify-between gap-2 p-3 bg-slate-50 border border-slate-200 rounded-lg text-sm'>
<div className='flex flex-wrap items-center gap-2'>
<Tag
data-testid='hidden-column-status'
className='inline-flex items-center gap-1'
color={hiddenColumnIds.length ? 'blue' : 'default'}
>
<Eye size={13} aria-hidden='true' />
<span>{t('숨긴 컬럼', 'Hidden')} {hiddenColumnIds.length}</span>
</Tag>
<Tag>{t('정렬', 'Sort')} {query.sortParams.length}</Tag>
<Tag>{t('필터', 'Filter')} {query.filterParams.length}</Tag>
<span className='text-slate-500'>
{hiddenColumnIds.length
? hiddenColumnIds.map(id => columns.find(column => column.id === id)?.label ?? id).join(', ')
: t('모든 컬럼 표시 중', 'All columns visible')}
</span>
</div>
<Button
size='small'
icon={<RotateCcw size={14} />}
disabled={hiddenColumnIds.length === 0}
onClick={() => setHiddenColumnIds([])}
>
{t('모두 표시', 'Show all')}
</Button>
</div>
<DataGridContainer ref={containerRef}>
<BGrid
width={width}
height={height}
data={rows}
columns={columns}
msg={exampleMsg}
columnGroups={groups}
headerHeight={54}
frozenColumnIndex={2}
columnVisibility={{
hiddenColumnIds,
onChange: setHiddenColumnIds,
}}
dataControl={{
mode: 'client',
query,
onChange: setQuery,
multiSort: true,
}}
rowKey='orderNo'
variant='vertical-bordered'
/>
</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,
};
}Basic usage
Column hiding and restoration are available starting with BeautifulGrid 1.0.6.
Enable columnVisibility to add a Hide this column action to every eligible header toolbox. Restore hidden columns from any remaining column menu. The final visible column cannot be hidden. As shown in the live demo, enabling toolbox and filter on a column puts sorting, filtering, and visibility actions in the same menu.
<BGrid
width={800}
height={420}
columns={columns}
data={data}
columnVisibility
/>
Persisting visibility
Give every column a unique id and use controlled state when visibility belongs to a user preference or saved view.
const [hiddenColumnIds, setHiddenColumnIds] = useState<string[]>(['owner']);
<BGrid
width={800}
height={420}
columns={columns}
data={data}
columnVisibility={{
hiddenColumnIds,
onChange: setHiddenColumnIds,
}}
/>
Set hideable: false on an essential column. Hiding does not remove its active sort or filter conditions, so they remain intact when the column is restored.
const columns = [
{ id: 'orderNo', key: 'orderNo', label: 'Order No.', width: 120, hideable: false },
{ id: 'customer', key: 'customer', label: 'Customer', width: 160 },
{ id: 'owner', key: 'owner', label: 'Owner', width: 120 },
];
Column drag reordering is disabled while at least one column is hidden. The configured columnSortable behavior returns after every column is shown again.