Skip to content

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.

Terminal window
pnpm dlx shadcn@latest add https://sg-widgets.vercel.app/r/react/column-picker.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://sg-widgets.vercel.app/r/svelte/column-picker.json
ColumnPicker: the default list, a picker restricted to dates, the dual layout, disabled and read-only

React

Loading the React demo…

Svelte

Loading the Svelte demo…

proptypedefaultmeaning
contextSgContextrequiredThe widget context. The schema is read through it, once per page.
entityTypestringrequiredThe type every path starts on.
valuestring[][]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.
showCountbooleanfalseShow how many columns are chosen under the list.
maxDepthnumber2How many hops a path may take.
dataTypesstring | string[]—Data types a field must have to be selected.
validTypesstring[]—A field is selectable only if it links one of these.
excludestring[]—Full dotted paths to drop.
hidePathsstring[]—Dotted prefixes to drop, along with everything beneath them.
filterableOnlybooleanfalseDrop 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.
placeholderstring'Add a column'Placeholder of the field picker.
searchPlaceholderstring'Search fields…'Shown in the search box.
emptyLabelstring'Nothing chosen'Shown while no column is chosen.
noMatchLabelstring'No match'Shown when the search over the fields on offer matches nothing.
loadingLabelstring'Loading…'Names the skeletons a read stands behind, for a screen reader.
errorLabelstring—Shown in place of what the failed read said.
availableLabelstring'Available'Heading of the left list, dual only.
chosenLabelstring'Columns'Heading of the right list, dual only.
readonlybooleanfalseShows the chosen list alone, with no controls.
disabledbooleanfalse
invalidbooleanfalsePuts the invalid ring on the field picker, or on both lists under dual.
size'sm' | 'md' | 'lg''md'Row height.
className / classstring—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.

eventpayloadwhen
onValueChangestring[]A column is added, removed or moved.

None.

keydoes
Enter, Space on the pickerOpen the field list.
Up, DownWalk the field list.
EnterAdd the field under the cursor. A link that cannot be chosen descends instead.
RightDescend into the link under the cursor.
LeftBack one level.
EscapeClose the field list.
TabMoves through the picker, then the grip and remove button of each chosen row.
Space, Enter on the gripPicks the row up for the arrow keys.
Up, Down while carrying a rowMove it one place.
Space, Enter while carrying a rowDrop it where it is.
Escape while carrying a rowPut it back where it was.
Alt + UpMove the chosen row holding focus one place earlier.
Alt + DownMove it one place later.
Delete, BackspaceRemove 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.

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.

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.