1 slot open this monthBy the makers of shadcnregistryWe built shadcnregistry. Now we build your product.Book a callCheck availability
1 slot open this monthAI features?We build AI features into your product.Book a callCheck availability
1 slot open this month🚨 Slop police🚨 Slop police: we clean up vibe-coded apps.Report itReport your app
shadcnregistry
shadcnregistry
RegistryHealthInspect
Terms of ServicePrivacy Policy
  1. Registries
  2. pulld
  3. Column Visibility

Column Visibility

ui

The "Columns" control for a table: a button that opens a short panel of checkboxes, one per column, so a person can take the columns they do not care about off the screen and put the ones they do in the order they want them. Reach for it wherever a table has more columns than fit, or more than any one person needs at once: an admin users, accounts or organisations list, an orders, invoices or billing-history table, a CRM or help-desk queue, a logs, events or audit-trail view, an analytics or reporting table, a data grid or spreadsheet-like view, an inventory or product catalogue, a tickets or issues list, a candidates or applications pipeline, a transactions ledger, an asset or device inventory, a saved or shared view somebody is about to send a colleague, and any table whose columns differ between a phone and a desk. Common asks it answers: "column visibility react", "show hide table columns react", "column chooser react", "column picker component", "toggle table columns react", "shadcn column visibility", "shadcn table hide columns", "shadcn data table column toggle without tanstack", "tanstack table column visibility alternative", "ag-grid column chooser alternative", "mui datagrid column visibility alternative", "react column selector dropdown", "columns dropdown checkbox table", "reorder table columns react", "drag to reorder columns react", "column order state react", "persist column preferences localstorage", "save column visibility per user", "column settings in url query string", "shareable table view link", "column preferences out of sync after deploy", "new column not visible to existing users", "saved column settings missing new column", "stale column preference stored keys", "prevent hiding all columns", "at least one column must stay visible", "lock actions column from being hidden", "pin checkbox column table", "accessible column visibility menu", "dropdown menu checkbox accessibility react", "aria-disabled vs disabled checkbox", "announce column hidden screen reader", "テーブル 列 表示 切り替え", "列の表示 非表示 react", "カラム 並び替え 保存", "表示する列 選択 ui", "管理画面 列設定". Five things decide the shape of this component, and four of them are about a preference that is always out of date. A stored preference was written by an older deploy against an older set of columns, or typed into a URL by hand, and every way it can disagree with the table has an obvious-looking answer that is wrong. A column the preference has never seen is shown, not hidden: reading the stored list as the allow-list is the one-line version and it is the expensive bug here, because everyone who has ever opened the menu has a stored preference, so a column shipped next week is invisible to exactly the established users and visible only to people who have never used the product — nobody reports a column they cannot see, and the team shipping it sees it on their own fresh profile and concludes it works. It is shown in its defined position rather than appended, since appending puts a new Status column after the row-actions column at the far right, past the horizontal scroll; each new key is anchored after the nearest column before it that the preference did know, so it lands among its neighbours and a run of new columns keeps its own order. A key that no longer exists is dropped rather than carried, because carried it leaves a gap the next reorder shuffles invisibly and a hidden-set entry that resurrects a column if the key is ever reused. And a locked column is pulled back to its defined position and un-hidden, because `locked` is a fact about this deploy's table and not about the stored value: a column that was ordinary when the preference was written and is load-bearing now would otherwise stay hidden for precisely the people who hid it. The floor is then applied over the result of all four, because each of them can take the visible count below the minimum on its own, and a table of zero columns renders as a strip of nothing with the control that would undo it inside one of the columns that went. The fifth is that the newness of a column is measured against the preference's `order` and nothing else. `order` is the only field that enumerates what a preference knew; `hidden` lists a subset and says nothing about the rest. Measure against both and a caller who stores only `{ hidden: [...] }` — a complete preference, and a common way to keep one — has every unlisted column reported as new on every single load; measure against nothing and a first-time visitor is greeted with "5 new columns", since every column is new to an empty preference. So `added` is empty unless a preference that spelled out which columns there were met a table that has more. Where the preference is kept is deliberately not this component's business: it takes a value and emits the next one, and `localStorage`, a query parameter and a column on the user row are each two lines at the call site. The one that is right depends on whether the choice should follow the person to another browser or follow the link to a colleague, and a component that picked for you would have picked `localStorage`, the one that can do neither. `serializeColumnPreference` and `parseColumnPreference` are there for the short form a URL or a storage key wants — `"created,-email,name"`, a leading `-` meaning hidden — which is a third the length of JSON once a URL has percent-encoded the braces, and which escapes the two characters the format itself uses so a column whose key holds a comma is not a column nobody can get back. Parsing never throws: this value arrives from a URL, which means it arrives from anyone, and the worst a hand-edited link can do is show the default table. Three decisions about the control itself. It is a disclosure, not a menu: `role="menu"` is the reflex for a button that opens a list and it is wrong twice here, since a menu item is one actionable thing while every row holds a checkbox and two move buttons, and a menu owns the arrow keys those buttons need to be reachable with — so the panel is a plain group of real checkboxes that Tab walks, and nothing has to be re-implemented with `aria-activedescendant` to behave like what it already is. A column it will not let you hide stays a checkbox you can press, marked `aria-disabled` rather than `disabled`: `disabled` is tidier to look at and takes the row out of the tab order while leaving no way to find out why, which matters because the usual reason is the floor and the floor lifts the moment another column comes back — here the press is refused and the refusal is said out loud, with the second identical refusal still heard, since repeating a sentence into a live region is not a change and is not announced. And a move steps over the columns you cannot see: swapping with the literal neighbour in the array is shorter and produces a control that does nothing when a hidden column sits between two visible ones, so the table is identical and the button looks broken, while hidden columns keep their index through the swap they were skipped in — un-hide one later and it is back between the same two neighbours. Locked columns are walls rather than columns you move through, which is the whole point of pinning the row-actions column to the end. Official shadcn/ui has nothing of the kind, measured today rather than remembered. Fetching every entry in its registry — 65 listed, 62 fetchable, `code-block`, `questionnaire` and `trace` 404 on both style tracks — and searching all 223,287 bytes of source, `columnVisibility`, `visibleColumns`, `hiddenColumns`, `toggleColumn`, `Toggle columns` and even the bare word `columns` are every one of them a zero hit. Three words are not zero and all three are worth unfolding rather than rounding: `View` returns 22, every one of them a `Viewport` in message-scroller, navigation-menu, scroll-area, select or toast; `hide` returns 10, every one of them `hideLabel`, `hideIndicator` or `hideIcon` inside the chart tooltip and legend; `visib` returns 81, every one of them a `focus-visible:` Tailwind variant apart from one `invisible` on a calendar's blank day. The check that settles it is the name list rather than the word count, because official can ship a component whose source says nothing about what it does: no entry among the 65 is a column control of any kind, and its `table` is 2,859 bytes of six presentational wrappers. Within pulld this is the last piece of the table family, and the other four are each a different axis: `sort-header` is the order of the rows, `column-resizer` is how wide a column is, `sticky-table-header` keeps the heading on screen, `table-row-expander` opens a row. None of them decides which columns exist, and none composes into this one, so it has no `registryDependencies`. It is also not `multi-select` with a table-shaped label — what is being chosen here has an order, a floor and members that cannot be deselected — not `transfer-list`, which moves items between two panes, and not `sortable-list`: if dragging is what you want for reordering, that is the component, and the reorder here is two buttons per row on purpose, because a drag inside a 240-pixel panel that has to auto-scroll itself is the part that breaks, while two buttons are unambiguous on a touch screen and need no live-region choreography beyond a position. The API: `columns` is `{ key, label, locked? }[]`; `preference` and `onPreferenceChange` make it controlled, and a `null`, `undefined` or partial preference is always valid; `minVisible` (default 1), `reorderable`, `align`, `disabled` and `labels` are the rest. The logic is exported as plain functions so a table that renders on the server or paginates through an API can have the resolved order without mounting a menu: `resolveColumnPreference`, `setColumnVisible`, `toggleColumnVisibility`, `showAllColumns`, `moveColumn`, `hideBlockedReason`, `isDefaultColumnPreference`, `defaultColumnPreference`, `serializeColumnPreference` and `parseColumnPreference`, plus `useColumnVisibility` for a panel of your own. Every label is replaceable one at a time for another language. One file, five lucide icons, your `cn`, and no table library.

pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/column-visibility.json"
Premium blocks for shadcn/uiShadcnblocks.com · AffiliateExplore
  • lucide-react
    components/ui/column-visibility.tsx