Askr documentation
UI & Components

Collections

Collections: anatomy, keyboard behavior, state, and theming in Askr.

Example

import { state } from '@askrjs/askr';
import { For } from '@askrjs/askr/control';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@askrjs/themes/components';

function ProjectStatusSelect() {
  const [status, setStatus] = state('active');
  return <Select value={status()} onValueChange={setStatus}>
    <SelectTrigger aria-label="Project status"><SelectValue /></SelectTrigger>
    <SelectContent>
      <For each={statuses} by={(item) => item.id}>
        {(item) => <SelectItem value={item.id}>{item.label}</SelectItem>}
      </For>
    </SelectContent>
  </Select>;
}

Purpose

Several Askr primitives manage a set of related items instead of a single value: RadioGroup, ToggleGroup, Menu, Menubar, Select, and Accordion all coordinate a list of child items under one parent. This page collects the patterns that recur across those list-shaped components — orientation, roving focus, and looping — rather than a single 'Collection' component, because @askrjs/ui doesn't export one. VirtualList and VirtualTable cover the separate case of rendering very large item sets efficiently.

Install and import

There's no dedicated collections package or import — you pull the pattern in through whichever component you're using, e.g. import { RadioGroup, RadioGroupItem } from '@askrjs/ui' or import { Menu, MenuItem } from '@askrjs/ui'. For large lists specifically, import VirtualList or VirtualTable from the same root package; both ship from @askrjs/ui with no separate subpath split between headless and virtualized variants.

Live examples

A RadioGroup with orientation="vertical" demonstrates arrow-key navigation between items without leaving the group's tab stop. A Menu opened from a Dropdown or Menubar trigger shows the same roving-item behavior applied to a transient overlay list. A VirtualList rendering several thousand rows with a fixed rowHeight shows the windowing approach that keeps DOM size bounded regardless of item count.

Anatomy

List-shaped components split into a root that owns the collection (RadioGroup, ToggleGroup, Menu, Accordion) and item children that register themselves with it (RadioGroupItem, ToggleGroupItem, MenuItem, AccordionItem). Grouping and labeling wrappers like MenuGroup, MenuLabel, and MenuSeparator exist for Menu and Menubar so long lists can be chunked visually without changing the item contract. VirtualList and VirtualTable instead take a flat items array plus a rowComponent (or column set for the table) and render only what's visible.

State model

Single-select collections (RadioGroup, single-mode ToggleGroup) track one selected value; multi-select variants (multi-mode ToggleGroup, multi-mode Accordion) track an array. Selection state follows the same value/defaultValue/onValueChange shape used everywhere else in the library, so switching a group from uncontrolled to controlled doesn't change any other prop. VirtualList and VirtualTable track a different kind of state — scroll position and visible range, exposed through an apiRef object with methods like scrollToIndex, getVisibleRange, and isAtBottom.

Keyboard and accessibility

Grouped items share orientation ('horizontal' | 'vertical') and loop props that control whether arrow keys wrap from the last item back to the first. Disabling the whole group via disabled removes every item from the tab sequence at once rather than requiring you to disable each item individually. Menu and Menubar layer submenu and typeahead navigation on top of the same roving-focus base used by RadioGroup and ToggleGroup.

Styling and tokens

Item state is exposed through data-state and data-disabled attributes on each item, so a theme can style the selected radio, the checked toggle, or the open accordion item purely from CSS without reaching into JavaScript state. @askrjs/themes styles the concrete list components (radio group, toggle group, menu, accordion) that build on these headless primitives; VirtualList and VirtualTable are unstyled by design since their content is arbitrary rows you supply.

API

Group-level props worth knowing across components: orientation?: 'horizontal' | 'vertical', loop?: boolean, and disabled?: boolean on the root; value/disabled on each item. VirtualListProps<Item> takes items: readonly Item[], rowHeight: number, getKey: (item, index) => string | number, rowComponent, an optional overscan, and apiRef?: Ref<VirtualListApi<Item> | null> for imperative scroll control. VirtualTable mirrors this shape with a columns array instead of a single row renderer.

Edge cases

An empty items array is a normal, supported state for VirtualList and VirtualTable — they just render zero rows rather than needing a special empty-state prop. If every item in a RadioGroup or ToggleGroup is disabled, focus can't enter the group at all, which is expected APG behavior rather than a bug to work around. Because getKey drives row identity in virtualized lists, returning a key that isn't stable across renders (e.g. an array index when the array is filtered) will cause visible remounts and lost scroll position.

See Focus and Dismissal for how roving focus interacts with FocusScope inside overlay-hosted lists like Menu and Dropdown. See Controlled State for the full value/defaultValue/onValueChange convention these components share with every other stateful primitive in @askrjs/ui.