<bpdm/ui />
Data

Order List

Reorder a collection — select items and move them with the control column or drag-and-drop; filterable, in React (@bpdm/ui) and Angular (@bpdm/ng).

The Order List lets users reorder a collection: select an item, then move it up / to top / down / to bottom with the control column, or drag it. It's controlled (value + onChange) or uncontrolled (defaultValue), filterable, and responsive (controls sit beside the list, stacking above on small screens). In Angular it's <bpdm-order-list> with [(value)] and an itemTemplate.

Order List renders every row (no virtualization), which suits small-to-medium collections — reorder queues, feature toggles, pipeline steps. For large, paged datasets reach for the Data Table instead.

Usage

Give it itemKey (a stable id) and renderItem. Provide onChange (or bind value) to receive the reordered array. Drag-and-drop is on by default.

Pipeline stages
Lint
Type-check
Unit tests
Build
Deploy
order-list-usage.tsx
import { useState } from 'react';
import { OrderList } from '@bpdm/ui/order-list';

const STAGES = ['Lint', 'Type-check', 'Unit tests', 'Build', 'Deploy'];

export function Pipeline() {
  const [items, setItems] = useState(STAGES);
  return (
    <OrderList
      value={items}
      onChange={setItems}
      itemKey={(s) => s}
      renderItem={(s) => s}
      header="Pipeline stages"
    />
  );
}

The row is an <ng-template> passed as itemTemplate; [(value)] two-way binds the ordered array.

order-list-usage.ts
import { Component, signal } from '@angular/core';
import { BpdmOrderList } from '@bpdm/ng';

@Component({
  selector: 'pipeline',
  imports: [BpdmOrderList],
  template: `
    <bpdm-order-list [(value)]="items" [itemKey]="itemKey" [itemTemplate]="row" header="Pipeline stages">
      <ng-template #row let-item>{{ item }}</ng-template>
    </bpdm-order-list>
  `,
})
export class Pipeline {
  readonly itemKey = (s: string) => s;
  readonly items = signal(['Lint', 'Type-check', 'Unit tests', 'Build', 'Deploy']);
}

Custom rows

renderItem (React) / the itemTemplate (Angular) can render anything — an avatar, a Badge, secondary text.

Backlog
Auth refactorBackend
Dark modeDesign
API referenceDocs
Onboarding flowFrontend
Rate limitingBackend
order-list-custom.tsx
import { useState } from 'react';
import { OrderList } from '@bpdm/ui/order-list';
import { Badge } from '@bpdm/ui/badge';

type Task = { id: string; name: string; tag: 'Backend' | 'Design' | 'Docs' | 'Frontend' };

const TASKS: Task[] = [
  { id: 't1', name: 'Auth refactor', tag: 'Backend' },
  { id: 't2', name: 'Dark mode', tag: 'Design' },
  { id: 't3', name: 'API reference', tag: 'Docs' },
  { id: 't4', name: 'Onboarding flow', tag: 'Frontend' },
  { id: 't5', name: 'Rate limiting', tag: 'Backend' },
];

export function Backlog() {
  const [items, setItems] = useState(TASKS);
  return (
    <OrderList
      value={items}
      onChange={setItems}
      itemKey={(t) => t.id}
      header="Backlog"
      renderItem={(t) => (
        <div className="flex flex-1 items-center justify-between gap-3">
          <span className="font-medium">{t.name}</span>
          <Badge variant="secondary" appearance="soft">
            {t.tag}
          </Badge>
        </div>
      )}
    />
  );
}
order-list-custom.ts
import { Component, signal } from '@angular/core';
import { BpdmOrderList, BpdmBadge } from '@bpdm/ng';

type Task = { id: string; name: string; tag: 'Backend' | 'Design' | 'Docs' | 'Frontend' };

@Component({
  selector: 'backlog',
  imports: [BpdmOrderList, BpdmBadge],
  template: `
    <bpdm-order-list [(value)]="items" [itemKey]="itemKey" [itemTemplate]="row" header="Backlog">
      <ng-template #row let-t>
        <div class="flex flex-1 items-center justify-between gap-3">
          <span class="font-medium">{{ t.name }}</span>
          <bpdm-badge variant="secondary" appearance="soft">{{ t.tag }}</bpdm-badge>
        </div>
      </ng-template>
    </bpdm-order-list>
  `,
})
export class Backlog {
  readonly itemKey = (t: Task) => t.id;
  readonly items = signal<Task[]>([
    { id: 't1', name: 'Auth refactor', tag: 'Backend' },
    { id: 't2', name: 'Dark mode', tag: 'Design' },
    { id: 't3', name: 'API reference', tag: 'Docs' },
    { id: 't4', name: 'Onboarding flow', tag: 'Frontend' },
    { id: 't5', name: 'Rate limiting', tag: 'Backend' },
  ]);
}

Filtering

Pass filterBy (an accessor returning each item's searchable text) to show a filter box. Drag is disabled while a filter is active (order would be ambiguous); the control column still moves the visible selection.

Auth refactorBackend
Dark modeDesign
API referenceDocs
Onboarding flowFrontend
Rate limitingBackend
order-list-filter.tsx
import { useState } from 'react';
import { OrderList } from '@bpdm/ui/order-list';
import { Badge } from '@bpdm/ui/badge';

type Task = { id: string; name: string; tag: 'Backend' | 'Design' | 'Docs' | 'Frontend' };

const TASKS: Task[] = [
  { id: 't1', name: 'Auth refactor', tag: 'Backend' },
  { id: 't2', name: 'Dark mode', tag: 'Design' },
  { id: 't3', name: 'API reference', tag: 'Docs' },
  { id: 't4', name: 'Onboarding flow', tag: 'Frontend' },
  { id: 't5', name: 'Rate limiting', tag: 'Backend' },
];

export function FilterableBacklog() {
  const [items, setItems] = useState(TASKS);
  return (
    <OrderList
      value={items}
      onChange={setItems}
      itemKey={(t) => t.id}
      filterBy={(t) => t.name}
      filterPlaceholder="Filter tasks…"
      renderItem={(t) => (
        <div className="flex flex-1 items-center justify-between gap-3">
          <span className="font-medium">{t.name}</span>
          <Badge variant="secondary" appearance="soft">
            {t.tag}
          </Badge>
        </div>
      )}
    />
  );
}
order-list-filter.ts
import { Component, signal } from '@angular/core';
import { BpdmOrderList, BpdmBadge } from '@bpdm/ng';

type Task = { id: string; name: string; tag: 'Backend' | 'Design' | 'Docs' | 'Frontend' };

@Component({
  selector: 'filterable-backlog',
  imports: [BpdmOrderList, BpdmBadge],
  template: `
    <bpdm-order-list
      [(value)]="items"
      [itemKey]="itemKey"
      [itemTemplate]="row"
      [filterBy]="filterBy"
      filterPlaceholder="Filter tasks…"
    >
      <ng-template #row let-t>
        <div class="flex flex-1 items-center justify-between gap-3">
          <span class="font-medium">{{ t.name }}</span>
          <bpdm-badge variant="secondary" appearance="soft">{{ t.tag }}</bpdm-badge>
        </div>
      </ng-template>
    </bpdm-order-list>
  `,
})
export class FilterableBacklog {
  readonly itemKey = (t: Task) => t.id;
  readonly filterBy = (t: Task) => t.name;
  readonly items = signal<Task[]>([
    { id: 't1', name: 'Auth refactor', tag: 'Backend' },
    { id: 't2', name: 'Dark mode', tag: 'Design' },
    { id: 't3', name: 'API reference', tag: 'Docs' },
    { id: 't4', name: 'Onboarding flow', tag: 'Frontend' },
    { id: 't5', name: 'Rate limiting', tag: 'Backend' },
  ]);
}

Multiple selection

selectionMode="multiple" lets users select several items and move them together with the control column. (A drag still moves a single item.)

Select several, move together
Lint
Type-check
Unit tests
Build
Deploy
order-list-multiple.tsx
import { useState } from 'react';
import { OrderList } from '@bpdm/ui/order-list';

const STAGES = ['Lint', 'Type-check', 'Unit tests', 'Build', 'Deploy'];

export function MultiMove() {
  const [items, setItems] = useState(STAGES);
  return (
    <OrderList
      value={items}
      onChange={setItems}
      itemKey={(s) => s}
      renderItem={(s) => s}
      selectionMode="multiple"
      header="Select several, move together"
    />
  );
}
order-list-multiple.ts
import { Component, signal } from '@angular/core';
import { BpdmOrderList } from '@bpdm/ng';

@Component({
  selector: 'multi-move',
  imports: [BpdmOrderList],
  template: `
    <bpdm-order-list
      [(value)]="items"
      [itemKey]="itemKey"
      [itemTemplate]="row"
      selectionMode="multiple"
      header="Select several, move together"
    >
      <ng-template #row let-item>{{ item }}</ng-template>
    </bpdm-order-list>
  `,
})
export class MultiMove {
  readonly itemKey = (s: string) => s;
  readonly items = signal(['Lint', 'Type-check', 'Unit tests', 'Build', 'Deploy']);
}

Controls only

Set dragdrop={false} to reorder purely with the control column — handy on touch, or when drag would conflict with other gestures.

Controls only (no drag)
Lint
Type-check
Unit tests
Build
Deploy
order-list-controls.tsx
import { useState } from 'react';
import { OrderList } from '@bpdm/ui/order-list';

const STAGES = ['Lint', 'Type-check', 'Unit tests', 'Build', 'Deploy'];

export function ControlsOnly() {
  const [items, setItems] = useState(STAGES);
  return (
    <OrderList
      value={items}
      onChange={setItems}
      itemKey={(s) => s}
      renderItem={(s) => s}
      dragdrop={false}
      header="Controls only (no drag)"
    />
  );
}
order-list-controls.ts
import { Component, signal } from '@angular/core';
import { BpdmOrderList } from '@bpdm/ng';

@Component({
  selector: 'controls-only',
  imports: [BpdmOrderList],
  template: `
    <bpdm-order-list
      [(value)]="items"
      [itemKey]="itemKey"
      [itemTemplate]="row"
      [dragdrop]="false"
      header="Controls only (no drag)"
    >
      <ng-template #row let-item>{{ item }}</ng-template>
    </bpdm-order-list>
  `,
})
export class ControlsOnly {
  readonly itemKey = (s: string) => s;
  readonly items = signal(['Lint', 'Type-check', 'Unit tests', 'Build', 'Deploy']);
}

Internationalization

Every built-in string is translatable. header, filterPlaceholder, and each item's rendered content are yours already; the messages prop (React) / [messages] input (Angular) overrides the rest — the reorder button + group labels, the move announcements read out by the live region, the empty-state text, and the listbox's fallback name. Pass a partial; anything you omit keeps its English default.

Pipeline-Phasen
Analyse
Erstellung
Tests
Bereitstellung
order-list-i18n.tsx
import { useState } from 'react';
import { OrderList } from '@bpdm/ui/order-list';

const STUFEN = ['Analyse', 'Erstellung', 'Tests', 'Bereitstellung'];

export function Pipeline() {
  const [items, setItems] = useState(STUFEN);
  return (
    <OrderList
      value={items}
      onChange={setItems}
      itemKey={(s) => s}
      renderItem={(s) => s}
      header="Pipeline-Phasen"
      messages={{
        reorderGroup: 'Neu ordnen',
        moveUp: 'Nach oben',
        moveToTop: 'Ganz nach oben',
        moveDown: 'Nach unten',
        moveToBottom: 'Ganz nach unten',
        movedUp: 'Nach oben verschoben',
        movedToTop: 'Ganz nach oben verschoben',
        movedDown: 'Nach unten verschoben',
        movedToBottom: 'Ganz nach unten verschoben',
        empty: 'Keine Einträge',
        listLabel: 'Sortierbare Liste',
      }}
    />
  );
}
order-list-i18n.ts
import { Component, signal } from '@angular/core';
import { BpdmOrderList, type OrderListMessages } from '@bpdm/ng';

@Component({
  selector: 'pipeline',
  imports: [BpdmOrderList],
  template: `
    <bpdm-order-list
      [(value)]="items"
      [itemKey]="itemKey"
      [itemTemplate]="row"
      header="Pipeline-Phasen"
      [messages]="messages"
    >
      <ng-template #row let-item>{{ item }}</ng-template>
    </bpdm-order-list>
  `,
})
export class Pipeline {
  readonly itemKey = (s: string) => s;
  readonly items = signal(['Analyse', 'Erstellung', 'Tests', 'Bereitstellung']);
  readonly messages: Partial<OrderListMessages> = {
    reorderGroup: 'Neu ordnen',
    moveUp: 'Nach oben',
    moveToTop: 'Ganz nach oben',
    moveDown: 'Nach unten',
    moveToBottom: 'Ganz nach unten',
    movedUp: 'Nach oben verschoben',
    movedToTop: 'Ganz nach oben verschoben',
    movedDown: 'Nach unten verschoben',
    movedToBottom: 'Ganz nach unten verschoben',
    empty: 'Keine Einträge',
    listLabel: 'Sortierbare Liste',
  };
}

The messages prop covers every reorder button + group label, the move announcements, the empty-state, and the listbox name — in both frameworks. Anything you omit falls back to the English default.

See the Internationalization guide for the shared conventions.

API

OrderList / <bpdm-order-list>

PropTypeDefaultDescription
value / defaultValue / onChangeT[] / T[] / (next) => voidThe ordered array — controlled or uncontrolled. Angular: [(value)].
itemKey(item) => string | numberRequired. Stable id per item.
renderItem (Angular itemTemplate)(item) => ReactNode / TemplateRefRequired. How each row renders.
headerReactNode / stringOptional list header.
filterBy / filterPlaceholder(item) => string / stringShow a filter box matching this text.
dragdropbooleantrueEnable drag-to-reorder.
selectionModesingle | multiplesingleMove one, or several together via the controls.
scrollHeightstring18remMax body height before the list scrolls.
ariaLabelstringOrderable listAccessible name for the listbox when there's no visible header.
messagesPartial<OrderListMessages>EnglishOverride every built-in screen-reader string for i18n — see Internationalization.
classNamestring (Angular class)Extra classes.

itemKey and renderItem (Angular itemTemplate) are the only required props; every other prop is optional. The component is controlled when you pass value, uncontrolled when you pass defaultValue (or nothing).

Accessibility

  • The list is a WAI-ARIA listbox: it's a single tab stop and a roving aria-activedescendant tracks the active option. ↑ / ↓ move it, Home / End jump to the ends, and Enter / Space toggle selection.
  • Each row is a role="option" exposing aria-selected; in selectionMode="multiple" the listbox sets aria-multiselectable.
  • The listbox is named by its header (wired via aria-labelledby). With no header, pass ariaLabel — it falls back to "Orderable list".
  • The control buttons (Move up / to top / down / to bottom) are real <button>s with labels, grouped under a role="group", and disabled at the list's edges. If the pressed button disables after a move, focus is kept within the group.
  • Each move is announced through a polite live region, so screen-reader users hear the result. Drag-and-drop is a purely visual enhancement — every reorder is also reachable from the keyboard via the controls.
  • Layout uses logical properties, so the selection accent and control column mirror correctly in RTL.

On this page