Column manager
Drag to reorder, tick to show or hide, with independently locked and pinned columns and a reset — the one column editor every grid uses.
Preview
Interactive examples use the same @flux/ui build as production. Toggle theme in the header for light and dark. Motion tokens and layout patterns live under Motion and Patterns in the sidebar.
Live preview
Drag a row to reorder, or focus one and press Space, then the arrow keys.
Installation
Add the package to your app (full Next.js + Tailwind steps on Installation):
npm install @payglocal_ui/flux-uiUsage
Import
import {
ColumnManager,
useColumnPreferences,
applyColumnPreferences,
type ManagedColumn,
} from "@payglocal_ui/flux-ui";Example
// Plain-text names for the manager's list — a table header can be a node
// (an icon, a tooltip, a two-line stack), and this list needs text.
const managed: ManagedColumn[] = [
{ key: "txn", label: "Transaction ID" },
{ key: "merchant", label: "Merchant" },
{ key: "amount", label: "Amount" },
];
const DEFAULT_ORDER = managed.map((c) => c.key);
// Owns the arrangement. A storageKey remembers it across visits;
// omit it and the arrangement lasts the session, which is what a grid
// whose columns depend on the signed-in role wants.
const prefs = useColumnPreferences(DEFAULT_ORDER, { storageKey: "txn-columns" });
// Rearranges and filters the built column list to match. "action" stays
// pinned last whatever the saved order says, and a column added in a
// later release still appears for someone who saved an arrangement
// before it existed.
const columns = applyColumnPreferences(allColumns, prefs);
<ColumnManager
columns={managed}
{...prefs.managerProps} // order / hidden / reset, already wired
// Visibility and position are independent, and neither is inferred
// from the other. A column the table cannot do without is still one
// the user may want to move; a column frozen to the left edge is still
// one they may want to hide.
// Cannot be HIDDEN. Locked tick box, but still draggable.
fixedKeys={["txn"]}
fixedReason="Every row is identified by its transaction ID."
// Cannot be MOVED. Dimmed grip, but its tick box still works. The usual
// case is a frozen (sticky) column: its left offset is the running
// total of the widths of the frozen columns before it, so the block
// only works while they stay first and contiguous — drag one into the
// middle and it keeps left-0, floating over the scrolling columns.
pinnedKeys={["merchantId", "gid"]}
pinnedReason="Frozen to the left of the table."
/>
<DataTable columns={columns} data={rows} rowKey={(r) => r.id} />
// Wiring the state by hand instead of using the hook:
<ColumnManager
columns={managed}
order={order}
onOrderChange={setOrder}
hiddenKeys={hidden} // omit, with onHiddenKeysChange, for
onHiddenKeysChange={setHidden} // a reorder-only popover
onReset={() => { setOrder(DEFAULT_ORDER); setHidden([]); }}
/>
// Reordering is @dnd-kit, so the rows animate out of each other's way
// as one is dragged past them. Keyboard users reorder without a mouse:
// Space picks a row up, the arrows move it, Space drops it, Escape
// abandons it.
//
// @dnd-kit is a peer of the bundle, not inlined into it — an app that
// uses ColumnManager needs @dnd-kit/core, /sortable and /utilities
// installed, which both PayGlocal apps already do.