Askr
UI & Components

Collections

Use collections with the current published Askr packages.

  • @askrjs/ui0.0.13
  • @askrjs/themes0.0.13

How to use collections

Collections should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.

  1. Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
  2. Keep collections labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
  3. Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@askrjs/themes/components';

<Select value={status()} onValueChange={status.set}>
  <SelectTrigger aria-label="Project status"><SelectValue /></SelectTrigger>
  <SelectContent>
    {statuses.map((item) => <SelectItem value={item.id}>{item.label}</SelectItem>)}
  </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.