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.
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.
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.
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>
)}
/>
);
}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.
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>
)}
/>
);
}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.)
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"
/>
);
}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.
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)"
/>
);
}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.
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',
}}
/>
);
}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>
| Prop | Type | Default | Description |
|---|---|---|---|
value / defaultValue / onChange | T[] / T[] / (next) => void | — | The ordered array — controlled or uncontrolled. Angular: [(value)]. |
itemKey | (item) => string | number | — | Required. Stable id per item. |
renderItem (Angular itemTemplate) | (item) => ReactNode / TemplateRef | — | Required. How each row renders. |
header | ReactNode / string | — | Optional list header. |
filterBy / filterPlaceholder | (item) => string / string | — | Show a filter box matching this text. |
dragdrop | boolean | true | Enable drag-to-reorder. |
selectionMode | single | multiple | single | Move one, or several together via the controls. |
scrollHeight | string | 18rem | Max body height before the list scrolls. |
ariaLabel | string | Orderable list | Accessible name for the listbox when there's no visible header. |
messages | Partial<OrderListMessages> | English | Override every built-in screen-reader string for i18n — see Internationalization. |
className | string (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 rovingaria-activedescendanttracks the active option. ↑ / ↓ move it, Home / End jump to the ends, and Enter / Space toggle selection. - Each row is a
role="option"exposingaria-selected; inselectionMode="multiple"the listbox setsaria-multiselectable. - The listbox is named by its
header(wired viaaria-labelledby). With no header, passariaLabel— it falls back to "Orderable list". - The control buttons (Move up / to top / down / to bottom) are real
<button>s with labels, grouped under arole="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.