ColumnPicker
Chooses the columns of a grid. A field picker sits over the list of chosen paths; picking a field appends it, and each row is dragged into the order the columns will be drawn in.
It sits on the Popover rather than the shared picker control, for the same reason the sort picker does: its popup is an editor over an ordered list. It keeps the picker contract of design rule 7 all the same, and a drive over both checks it.
Single and multi: this picker is the ordered multi of FieldPicker, an array of paths ordered by drag, where the field picker takes one path.
Install
Section titled “Install”pnpm dlx shadcn@latest add https://sg-widgets.vercel.app/r/react/column-picker.jsonpnpm dlx shadcn-svelte@latest add https://sg-widgets.vercel.app/r/svelte/column-picker.jsonReact
Loading the React demo…
Svelte
Loading the Svelte demo…
| prop | type | default | meaning |
|---|---|---|---|
context | SgContext | required | The widget context. The schema is read through it, once per page. |
entityType | string | required | The type every path starts on. |
value | string[] | [] | The chosen dotted paths, in the order they are shown. |
layout | 'list' | 'dual' | 'list' | list is the field picker over the ordered list; dual is the two lists side by side. |
showCount | boolean | false | Show how many columns are chosen under the list. |
deepLinks | boolean | true | Allow descending through entity fields. |
maxDepth | number | 2 | How many hops a path may take. |
dataTypes | string | string[] | — | Data types a field must have to be selected. |
validTypes | string[] | — | A field is selectable only if it links one of these. |
exclude | string[] | — | Full dotted paths to drop. |
hidePaths | string[] | — | Dotted prefixes to drop, along with everything beneath them. |
filterableOnly | boolean | false | Drop the data types the API refuses in a filter. |
extraFields | { name, displayName? }[] | — | Synthetic entries offered at the root only. |
filter | (field, path) => boolean | — | Your own visibility test over the schema and the candidate's full path. |
placeholder | string | 'Add a column' | Placeholder of the field picker. |
searchPlaceholder | string | 'Search fields…' | Shown in the search box. |
emptyLabel | string | 'Nothing chosen' | Shown while no column is chosen. |
noMatchLabel | string | 'No match' | Shown when the search over the fields on offer matches nothing. |
loadingLabel | string | 'Loading…' | Names the skeletons a read stands behind, for a screen reader. |
errorLabel | string | — | Shown in place of what the failed read said. |
availableLabel | string | 'Available' | Heading of the left list, dual only. |
chosenLabel | string | 'Columns' | Heading of the right list, dual only. |
readonly | boolean | false | Shows the chosen list alone, with no controls. |
disabled | boolean | false | |
invalid | boolean | false | Puts the invalid ring on the field picker, or on both lists under dual. |
size | 'sm' | 'md' | 'lg' | 'md' | Row height. |
className / class | string | — | Merged after the widget's own classes. |
Every other attribute is spread onto the root: id, aria-*, data-*, key handlers and a
ref to the root element.
Picking a field appends its path to the end of the list and clears the picker, which takes focus back for the next one. A path already chosen is off the list, so a column is never added twice.
Each row carries a grip, the friendly path with the raw path in its tooltip, and a remove button. A row is reordered by dragging its grip, which lifts the row and opens a gap where it will land, or from the keyboard: Space on the grip picks the row up, the arrow keys move it, and Space drops it. Alt with an arrow key moves the row holding focus one place. Every step is announced in a live region. Reduced motion keeps the reordering and drops the movement.
A link field carries a chevron that descends into the type it points at, and the breadcrumb above
the search box goes back one level or all the way to the root. A link declaring several target
types asks which one first. dataTypes and validTypes bind what may be chosen, not what may be
walked through, so a picker restricted to dates still reaches a date behind a link.
layout="dual" puts the fields of the type in a checked list beside the chosen paths. Checking a
row appends its path; unchecking removes it. The two lists sit side by side from a width of 32rem
and stack under it, measured on the widget rather than on the window, so a column picker in a
narrow panel stacks on a wide page.
Events
Section titled “Events”| event | payload | when |
|---|---|---|
onValueChange | string[] | A column is added, removed or moved. |
None.
Keyboard
Section titled “Keyboard”| key | does |
|---|---|
Enter, Space on the picker | Open the field list. |
Up, Down | Walk the field list. |
Enter | Add the field under the cursor. A link that cannot be chosen descends instead. |
Right | Descend into the link under the cursor. |
Left | Back one level. |
Escape | Close the field list. |
Tab | Moves through the picker, then the grip and remove button of each chosen row. |
Space, Enter on the grip | Picks the row up for the arrow keys. |
Up, Down while carrying a row | Move it one place. |
Space, Enter while carrying a row | Drop it where it is. |
Escape while carrying a row | Put it back where it was. |
Alt + Up | Move the chosen row holding focus one place earlier. |
Alt + Down | Move it one place later. |
Delete, Backspace | Remove it. |
Dragging a grip moves the row under the pointer once it has travelled four pixels; the list scrolls
when the pointer nears an edge, and Escape cancels the drag.
API behaviour
Section titled “API behaviour”GET /schema/<Type>/fields is 48KB and about 330ms per type, so the schema service caches it: a hop
back to a type already visited costs nothing, and a list of ten paths over three types costs three
reads (002_schema).
Only single entity fields are descended into. A dotted path through a multi-entity field reads back
nothing, 200 with the key absent from attributes (probe 016). A type already on the path is never
offered again, so a path cannot loop.
Every path is resolved for display through the schema of each type it travels, and a path the schema no longer holds stays readable as itself rather than disappearing.
Reference
Section titled “Reference”Reordering follows ReUI’s Sortable 2.5.2 for the grip and the lifted row, and Dice UI’s Sortable, its shadcn registry item, for the live-region copy and the keyboard model. Neither is installed. The pointer behaviour, a four-pixel activation distance with midpoint hit-testing and edge auto-scroll, follows dnd-kit 6.3.1.
The list is one engine in both frameworks: it is always open, always in place and always holds a highlight. It fades at whichever edge has more content past it, holds a gutter for its scrollbar, and carries a live region saying what it is doing. The pattern is coss.com/ui at e937bec, read as a reference and not installed.