Accordion
An accessible accordion — single or multiple open, three looks, icons, smooth height animation, in React (@bpdm/ui) and Angular (@bpdm/ng).
The Accordion collapses content into expandable sections — an FAQ, settings
groups, filters. It's accessible by default (full keyboard + ARIA), with a smooth
height animation and a rotating chevron. It's data-driven via items; open one
at a time (single) or several (multiple), in three looks. In Angular it's
<bpdm-accordion> with the same inputs; each item's content is an <ng-template>.
Usage
Pass items — each with a value, title, and content. type="single"
(default) opens one at a time; collapsible (default) lets you close it.
import { Accordion, type AccordionItemData } from '@bpdm/ui/accordion';
const items: AccordionItemData[] = [
{ value: 'deploys', title: 'How are deploys triggered?', content: 'Every push to main runs CI; a green build promotes to staging automatically.' },
{ value: 'rollback', title: 'Can I roll back a release?', content: 'Yes — pick any previous build in the deploys panel and click Promote.' },
{ value: 'logs', title: 'Where are build logs stored?', content: 'Logs are kept for 30 days and are downloadable from each run.' },
];
export function Faq() {
return <Accordion type="single" collapsible defaultValue="deploys" items={items} />;
}Each item's content is a <ng-template> referenced from the items array.
import { Component, viewChild, computed, type TemplateRef } from '@angular/core';
import { BpdmAccordion, type AccordionItemData } from '@bpdm/ng';
@Component({
selector: 'faq',
imports: [BpdmAccordion],
templateUrl: './faq.html',
})
export class Faq {
readonly deploys = viewChild.required<TemplateRef<unknown>>('deploys');
readonly rollback = viewChild.required<TemplateRef<unknown>>('rollback');
readonly logs = viewChild.required<TemplateRef<unknown>>('logs');
readonly items = computed<AccordionItemData[]>(() => [
{ value: 'deploys', title: 'How are deploys triggered?', content: this.deploys() },
{ value: 'rollback', title: 'Can I roll back a release?', content: this.rollback() },
{ value: 'logs', title: 'Where are build logs stored?', content: this.logs() },
]);
}<bpdm-accordion [items]="items()" type="single" [collapsible]="true" defaultValue="deploys" />
<ng-template #deploys>Every push to main runs CI; a green build promotes to staging automatically.</ng-template>
<ng-template #rollback>Yes — pick any previous build in the deploys panel and click Promote.</ng-template>
<ng-template #logs>Logs are kept for 30 days and are downloadable from each run.</ng-template>Variants
variant gives three looks: default (a bordered list), separated (each item
a card), and borderless (dividers only). The preview toggles them with our Tabs.
import { Accordion, type AccordionItemData } from '@bpdm/ui/accordion';
const items: AccordionItemData[] = [
{ value: 'deploys', title: 'How are deploys triggered?', content: 'Every push to main runs CI; a green build promotes to staging automatically.' },
{ value: 'rollback', title: 'Can I roll back a release?', content: 'Yes — pick any previous build in the deploys panel and click Promote.' },
{ value: 'logs', title: 'Where are build logs stored?', content: 'Logs are kept for 30 days and are downloadable from each run.' },
];
// variant: "default" | "separated" | "borderless"
export function SeparatedFaq() {
return <Accordion type="single" collapsible defaultValue="deploys" variant="separated" items={items} />;
}import { Component, viewChild, computed, type TemplateRef } from '@angular/core';
import { BpdmAccordion, type AccordionItemData } from '@bpdm/ng';
@Component({
selector: 'separated-faq',
imports: [BpdmAccordion],
templateUrl: './variants.html',
})
export class SeparatedFaq {
readonly deploys = viewChild.required<TemplateRef<unknown>>('deploys');
readonly rollback = viewChild.required<TemplateRef<unknown>>('rollback');
readonly logs = viewChild.required<TemplateRef<unknown>>('logs');
readonly items = computed<AccordionItemData[]>(() => [
{ value: 'deploys', title: 'How are deploys triggered?', content: this.deploys() },
{ value: 'rollback', title: 'Can I roll back a release?', content: this.rollback() },
{ value: 'logs', title: 'Where are build logs stored?', content: this.logs() },
]);
}<bpdm-accordion [items]="items()" type="single" [collapsible]="true" defaultValue="deploys" variant="separated" />
<ng-template #deploys>Every push to main runs CI; a green build promotes to staging automatically.</ng-template>
<ng-template #rollback>Yes — pick any previous build in the deploys panel and click Promote.</ng-template>
<ng-template #logs>Logs are kept for 30 days and are downloadable from each run.</ng-template>Single vs multiple
type="single" (the default) keeps one section open at a time; collapsible
(default true) lets you close the open one so none are showing. type="multiple"
lets several sections stay open at once — here defaultValue is an array.
import { Accordion, type AccordionItemData } from '@bpdm/ui/accordion';
const items: AccordionItemData[] = [
{ value: 'deploys', title: 'How are deploys triggered?', content: 'Every push to main runs CI; a green build promotes to staging automatically.' },
{ value: 'rollback', title: 'Can I roll back a release?', content: 'Yes — pick any previous build in the deploys panel and click Promote.' },
{ value: 'logs', title: 'Where are build logs stored?', content: 'Logs are kept for 30 days and are downloadable from each run.' },
];
export function MultiFaq() {
return <Accordion type="multiple" defaultValue={['deploys', 'logs']} items={items} />;
}import { Component, viewChild, computed, type TemplateRef } from '@angular/core';
import { BpdmAccordion, type AccordionItemData } from '@bpdm/ng';
@Component({
selector: 'multi-faq',
imports: [BpdmAccordion],
templateUrl: './multiple.html',
})
export class MultiFaq {
readonly deploys = viewChild.required<TemplateRef<unknown>>('deploys');
readonly rollback = viewChild.required<TemplateRef<unknown>>('rollback');
readonly logs = viewChild.required<TemplateRef<unknown>>('logs');
readonly items = computed<AccordionItemData[]>(() => [
{ value: 'deploys', title: 'How are deploys triggered?', content: this.deploys() },
{ value: 'rollback', title: 'Can I roll back a release?', content: this.rollback() },
{ value: 'logs', title: 'Where are build logs stored?', content: this.logs() },
]);
}<bpdm-accordion [items]="items()" type="multiple" [defaultValue]="['deploys', 'logs']" />
<ng-template #deploys>Every push to main runs CI; a green build promotes to staging automatically.</ng-template>
<ng-template #rollback>Yes — pick any previous build in the deploys panel and click Promote.</ng-template>
<ng-template #logs>Logs are kept for 30 days and are downloadable from each run.</ng-template>With icons
Give an item an icon for a leading glyph.
In React the icon is any node — here a lucide-react glyph.
import { Accordion, type AccordionItemData } from '@bpdm/ui/accordion';
import { Rocket, RotateCcw, FileText } from 'lucide-react';
const items: AccordionItemData[] = [
{ value: 'deploys', title: 'How are deploys triggered?', content: 'Every push to main runs CI; a green build promotes to staging automatically.', icon: <Rocket /> },
{ value: 'rollback', title: 'Can I roll back a release?', content: 'Yes — pick any previous build in the deploys panel and click Promote.', icon: <RotateCcw /> },
{ value: 'logs', title: 'Where are build logs stored?', content: 'Logs are kept for 30 days and are downloadable from each run.', icon: <FileText /> },
];
export function IconFaq() {
return <Accordion type="single" collapsible defaultValue="deploys" items={items} />;
}In Angular the icon is a <ng-template> too — reference it from the items array.
import { Component, viewChild, computed, type TemplateRef } from '@angular/core';
import { BpdmAccordion, type AccordionItemData } from '@bpdm/ng';
@Component({
selector: 'icon-faq',
imports: [BpdmAccordion],
templateUrl: './icons.html',
})
export class IconFaq {
readonly deploys = viewChild.required<TemplateRef<unknown>>('deploys');
readonly rollback = viewChild.required<TemplateRef<unknown>>('rollback');
readonly logs = viewChild.required<TemplateRef<unknown>>('logs');
readonly rocket = viewChild.required<TemplateRef<unknown>>('rocket');
readonly items = computed<AccordionItemData[]>(() => [
{ value: 'deploys', title: 'How are deploys triggered?', content: this.deploys(), icon: this.rocket() },
{ value: 'rollback', title: 'Can I roll back a release?', content: this.rollback() },
{ value: 'logs', title: 'Where are build logs stored?', content: this.logs() },
]);
}<bpdm-accordion [items]="items()" type="single" [collapsible]="true" defaultValue="deploys" />
<ng-template #deploys>Every push to main runs CI; a green build promotes to staging automatically.</ng-template>
<ng-template #rollback>Yes — pick any previous build in the deploys panel and click Promote.</ng-template>
<ng-template #logs>Logs are kept for 30 days and are downloadable from each run.</ng-template>
<ng-template #rocket><!-- your icon svg --></ng-template>Disabled item
Set disabled on an item to make it non-interactive.
import { Accordion, type AccordionItemData } from '@bpdm/ui/accordion';
const items: AccordionItemData[] = [
{ value: 'deploys', title: 'How are deploys triggered?', content: 'Every push to main runs CI; a green build promotes to staging automatically.' },
{ value: 'rollback', title: 'Can I roll back a release?', content: 'Yes — pick any previous build in the deploys panel and click Promote.' },
{ value: 'logs', title: 'Where are build logs stored?', content: 'Logs are kept for 30 days and are downloadable from each run.', disabled: true },
];
export function DisabledFaq() {
return <Accordion type="single" collapsible items={items} />;
}import { Component, viewChild, computed, type TemplateRef } from '@angular/core';
import { BpdmAccordion, type AccordionItemData } from '@bpdm/ng';
@Component({
selector: 'disabled-faq',
imports: [BpdmAccordion],
templateUrl: './disabled.html',
})
export class DisabledFaq {
readonly deploys = viewChild.required<TemplateRef<unknown>>('deploys');
readonly rollback = viewChild.required<TemplateRef<unknown>>('rollback');
readonly logs = viewChild.required<TemplateRef<unknown>>('logs');
readonly items = computed<AccordionItemData[]>(() => [
{ value: 'deploys', title: 'How are deploys triggered?', content: this.deploys() },
{ value: 'rollback', title: 'Can I roll back a release?', content: this.rollback() },
{ value: 'logs', title: 'Where are build logs stored?', content: this.logs(), disabled: true },
]);
}<bpdm-accordion [items]="items()" type="single" [collapsible]="true" />
<ng-template #deploys>Every push to main runs CI; a green build promotes to staging automatically.</ng-template>
<ng-template #rollback>Yes — pick any previous build in the deploys panel and click Promote.</ng-template>
<ng-template #logs>Logs are kept for 30 days and are downloadable from each run.</ng-template>Internationalization
- The accordion renders no copy of its own — every string (an item's
title, itscontent, an optionalicon) comes from theitemsyou pass. So you localize at the data level: translate youritemsand the accordion renders them as-is. There is nothing to translate on the component itself and nomessagesprop to set. - RTL-safe by construction: layout uses logical properties (
text-start, inline-start padding), so it mirrors automatically underdir="rtl"— the chevron moves to the inline-start edge and the text aligns to the start, with no prop required. The German example below is left-to-right; for a right-to-left locale, render it inside adir="rtl"container (or inheritdirfrom the page). See the Internationalization guide.
import { Accordion, type AccordionItemData } from '@bpdm/ui/accordion';
// German (de-DE) FAQ — localize the items you pass in
const items: AccordionItemData[] = [
{
value: 'projekt',
title: 'Wie erstelle ich ein Projekt?',
content: 'Öffnen Sie das Dashboard, klicken Sie auf „Neues Projekt" und geben Sie einen Namen ein.',
},
{
value: 'mitglieder',
title: 'Wie lade ich Teammitglieder ein?',
content: 'Gehen Sie zu „Mitglieder", geben Sie eine E-Mail-Adresse ein und wählen Sie eine Rolle aus.',
},
{
value: 'berechtigungen',
title: 'Wie verwalte ich Berechtigungen?',
content: 'Berechtigungen werden pro Rolle vergeben; passen Sie sie jederzeit unter „Einstellungen" an.',
},
];
export function GermanFaq() {
return <Accordion type="single" collapsible defaultValue="projekt" items={items} />;
}import { Component, viewChild, computed, type TemplateRef } from '@angular/core';
import { BpdmAccordion, type AccordionItemData } from '@bpdm/ng';
@Component({
selector: 'german-faq',
imports: [BpdmAccordion],
templateUrl: './faq-de.html',
})
export class GermanFaq {
readonly projekt = viewChild.required<TemplateRef<unknown>>('projekt');
readonly mitglieder = viewChild.required<TemplateRef<unknown>>('mitglieder');
readonly berechtigungen = viewChild.required<TemplateRef<unknown>>('berechtigungen');
// German (de-DE) FAQ — localize the items you pass in
readonly items = computed<AccordionItemData[]>(() => [
{ value: 'projekt', title: 'Wie erstelle ich ein Projekt?', content: this.projekt() },
{ value: 'mitglieder', title: 'Wie lade ich Teammitglieder ein?', content: this.mitglieder() },
{ value: 'berechtigungen', title: 'Wie verwalte ich Berechtigungen?', content: this.berechtigungen() },
]);
}<bpdm-accordion [items]="items()" type="single" [collapsible]="true" defaultValue="projekt" />
<ng-template #projekt>Öffnen Sie das Dashboard, klicken Sie auf „Neues Projekt" und geben Sie einen Namen ein.</ng-template>
<ng-template #mitglieder>Gehen Sie zu „Mitglieder", geben Sie eine E-Mail-Adresse ein und wählen Sie eine Rolle aus.</ng-template>
<ng-template #berechtigungen>Berechtigungen werden pro Rolle vergeben; passen Sie sie jederzeit unter „Einstellungen" an.</ng-template>API
Accordion / <bpdm-accordion>
| Prop | Type | Default | Description |
|---|---|---|---|
items | AccordionItemData[] | — | Required. The sections. |
type | single | multiple | single | Open one at a time, or several. |
collapsible | boolean | true | In single mode, allow closing the open item. |
variant | default | separated | borderless | default | The look. |
headingLevel | 2 | 3 | 4 | 5 | 6 | 3 | Heading level for each item's header, for correct document outline. |
defaultValue | string (or string[] for multiple) | — | Which section(s) start open — uncontrolled. |
value / onValueChange | string (or string[] for multiple) | — | Controlled open section(s) (React only). |
className | string (Angular class) | — | Extra classes. |
AccordionItemData
| Field | Type | Description |
|---|---|---|
value | string | Unique id for the section. |
title | ReactNode (Angular string) | The trigger label. |
content | ReactNode (Angular TemplateRef) | The collapsible body. |
icon | ReactNode (Angular TemplateRef) | Optional leading icon. |
disabled | boolean | Make the section non-interactive. |
Prefer composition? Import AccordionRoot / AccordionItem / AccordionTrigger
/ AccordionContent and build the tree yourself instead of passing items.
Accessibility
Both frameworks implement the same WAI-ARIA accordion pattern:
- Real heading per item. Each item's header is a heading at the level you set
via
headingLevel(default3), so the accordion sits correctly in the document outline — use4when it lives under an<h3>section, and so on. - Trigger is a
<button>carryingaria-expanded(open/closed) andaria-controlspointing at the panel it toggles. - Panel is a
role="region"labelled by its trigger viaaria-labelledby, so a screen reader announces which section the content belongs to. - A collapsed panel is removed from the tab order and hidden from assistive tech, so focus and the reading order skip content that isn't visible.
- Keyboard: Enter / Space toggle the focused header; Arrow Down / Arrow Up move between headers; Home / End jump to the first / last header. Focused triggers show a visible focus-visible ring.
- Disabled items are not focusable and cannot be toggled.
- State is not colour-only — the rotating chevron reflects open/closed alongside
aria-expanded, and the open/close height animation is CSS-driven and respectsprefers-reduced-motion(the transition is dropped when reduced motion is requested).
Status Timeline
A vertical status timeline for lifecycles — complete / current / pending / failed steps with timestamps, in React (@bpdm/ui) and Angular (@bpdm/ng).
Card
A composable surface — media, header/title/description, content and footer; three variants, hover-lift and fully-clickable modes, in React (@bpdm/ui) and Angular (@bpdm/ng).