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.
Transaction IDMerchantAmountMethodStatus
TXN-88401Acme Corp₹48,200CardCaptured
TXN-88402Globex₹12,750UPIPending
TXN-88403Initech₹93,400NetbankingCaptured

Installation

Add the package to your app (full Next.js + Tailwind steps on Installation):

npm install @payglocal_ui/flux-ui

Usage

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.