The Angular CDK Accordion module provides a flexible accordion component system with support for single/multiple panel expansion, programmatic control, and accessibility features following the ARIA accordion pattern.
The main accordion component that manages the state of multiple accordion items.
/**
* Directive for creating accordion containers
*/
class CdkAccordion implements OnDestroy, OnChanges {
/**
* Whether multiple accordion panels can be expanded simultaneously
*/
multi: boolean;
/**
* Unique identifier for the accordion
*/
id: string;
/**
* Whether the accordion should close all panels when destroyed
*/
closeAll: boolean;
/**
* Whether keyboard navigation is disabled
*/
hideToggle: boolean;
/**
* Whether to display visual toggle indicators
*/
displayMode: 'default' | 'flat';
/**
* Toggle position for accordion items
*/
togglePosition: 'before' | 'after';
/**
* Stream that emits when the state of the accordion changes
*/
readonly _stateChanges: Subject<SimpleChanges>;
/**
* Stream that emits true/false when openAll/closeAll is triggered
*/
readonly _openCloseAllActions: Subject<boolean>;
/**
* Opens all enabled accordion items in a multi-accordion
*/
openAll(): void;
/**
* Closes all enabled accordion items in a multi-accordion
*/
closeAll(): void;
}Usage Example:
import { Component } from '@angular/core';
import { CdkAccordionModule } from '@angular/cdk/accordion';
@Component({
template: `
<cdk-accordion
[multi]="allowMultiple"
class="example-accordion">
<cdk-accordion-item #item1="cdkAccordionItem">
<div class="accordion-item-header" (click)="item1.toggle()">
<span>Section 1</span>
<span class="toggle-icon" [class.expanded]="item1.expanded">▼</span>
</div>
<div class="accordion-item-body" *ngIf="item1.expanded">
<p>Content for section 1. This content is only visible when the accordion item is expanded.</p>
<p>You can put any content here including other components, forms, or complex layouts.</p>
</div>
</cdk-accordion-item>
<cdk-accordion-item #item2="cdkAccordionItem" [disabled]="isSecondDisabled">
<div class="accordion-item-header" (click)="item2.toggle()">
<span>Section 2 {{ isSecondDisabled ? '(Disabled)' : '' }}</span>
<span class="toggle-icon" [class.expanded]="item2.expanded">▼</span>
</div>
<div class="accordion-item-body" *ngIf="item2.expanded">
<p>Content for section 2.</p>
<button (click)="doSomething()">Action Button</button>
</div>
</cdk-accordion-item>
<cdk-accordion-item #item3="cdkAccordionItem">
<div class="accordion-item-header" (click)="item3.toggle()">
<span>Section 3</span>
<span class="toggle-icon" [class.expanded]="item3.expanded">▼</span>
</div>
<div class="accordion-item-body" *ngIf="item3.expanded">
<p>Content for section 3.</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
<li>List item 3</li>
</ul>
</div>
</cdk-accordion-item>
</cdk-accordion>
<div class="controls">
<button (click)="accordion.openAll()" [disabled]="!allowMultiple">Open All</button>
<button (click)="accordion.closeAll()">Close All</button>
<button (click)="toggleMultiple()">
{{ allowMultiple ? 'Single' : 'Multiple' }} Mode
</button>
<button (click)="toggleSecondDisabled()">
{{ isSecondDisabled ? 'Enable' : 'Disable' }} Section 2
</button>
</div>
`,
styles: [`
.example-accordion {
display: block;
max-width: 500px;
margin-bottom: 16px;
}
.accordion-item-header {
background: #f5f5f5;
border: 1px solid #ddd;
padding: 12px 16px;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.accordion-item-header:hover {
background: #eeeeee;
}
.accordion-item-body {
border: 1px solid #ddd;
border-top: none;
padding: 16px;
background: white;
}
.toggle-icon {
transition: transform 0.2s;
}
.toggle-icon.expanded {
transform: rotate(180deg);
}
.controls {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.controls button {
padding: 8px 16px;
}
`],
standalone: true,
imports: [CdkAccordionModule]
})
export class AccordionExample {
allowMultiple = false;
isSecondDisabled = false;
@ViewChild(CdkAccordion) accordion!: CdkAccordion;
toggleMultiple() {
this.allowMultiple = !this.allowMultiple;
}
toggleSecondDisabled() {
this.isSecondDisabled = !this.isSecondDisabled;
}
doSomething() {
console.log('Action button clicked in accordion item 2');
}
}Individual accordion panel component with expand/collapse functionality.
/**
* Directive for individual accordion items
*/
class CdkAccordionItem implements OnDestroy {
/**
* Whether the accordion item is expanded
*/
expanded: boolean;
/**
* Whether the accordion item is disabled
*/
disabled: boolean;
/**
* Unique identifier for the accordion item
*/
id: string;
/**
* Event emitted when the accordion item is opened
*/
readonly opened: EventEmitter<void>;
/**
* Event emitted when the accordion item is closed
*/
readonly closed: EventEmitter<void>;
/**
* Event emitted when the accordion item is destroyed
*/
readonly destroyed: EventEmitter<void>;
/**
* Event emitted when the expanded state changes
*/
readonly expandedChange: EventEmitter<boolean>;
/**
* Reference to the parent accordion
*/
accordion: CdkAccordion;
/**
* Open the accordion item
*/
open(): void;
/**
* Close the accordion item
*/
close(): void;
/**
* Toggle the accordion item's expanded state
*/
toggle(): void;
/**
* Get the expanded state
*/
getExpandedState(): boolean;
}import { Component, QueryList, ViewChildren } from '@angular/core';
import { CdkAccordionItem } from '@angular/cdk/accordion';
interface AccordionSection {
id: string;
title: string;
content: string;
disabled?: boolean;
icon?: string;
}
@Component({
template: `
<div class="accordion-container">
<div class="accordion-header">
<h2>FAQ Accordion</h2>
<div class="header-controls">
<button (click)="expandAll()" [disabled]="!multiExpand">Expand All</button>
<button (click)="collapseAll()">Collapse All</button>
<label>
<input type="checkbox" [(ngModel)]="multiExpand"> Multiple Expand
</label>
</div>
</div>
<cdk-accordion [multi]="multiExpand" class="faq-accordion">
<cdk-accordion-item
*ngFor="let section of sections; trackBy: trackBySection"
#accordionItem="cdkAccordionItem"
[disabled]="section.disabled"
(opened)="onSectionOpened(section)"
(closed)="onSectionClosed(section)"
(expandedChange)="onExpandedChange(section, $event)"
class="faq-item">
<div
class="faq-header"
[class.expanded]="accordionItem.expanded"
[class.disabled]="section.disabled"
(click)="accordionItem.toggle()"
(keydown.enter)="accordionItem.toggle()"
(keydown.space)="accordionItem.toggle(); $event.preventDefault()"
tabindex="0"
role="button"
[attr.aria-expanded]="accordionItem.expanded"
[attr.aria-controls]="'panel-' + section.id"
[attr.id]="'header-' + section.id">
<div class="faq-title">
<span class="faq-icon" *ngIf="section.icon">{{ section.icon }}</span>
<span>{{ section.title }}</span>
</div>
<div class="faq-controls">
<span class="expand-indicator" [class.rotated]="accordionItem.expanded">
▼
</span>
</div>
</div>
<div
*ngIf="accordionItem.expanded"
class="faq-content"
[attr.id]="'panel-' + section.id"
[attr.aria-labelledby]="'header-' + section.id"
role="region">
<div class="content-inner">
<p>{{ section.content }}</p>
<div class="content-actions">
<button class="link-button" (click)="markAsHelpful(section)">
👍 Helpful
</button>
<button class="link-button" (click)="reportIssue(section)">
🚩 Report Issue
</button>
</div>
</div>
</div>
</cdk-accordion-item>
</cdk-accordion>
<div class="accordion-footer">
<p>{{ getExpandedCount() }} of {{ sections.length }} sections expanded</p>
</div>
</div>
`,
styles: [`
.accordion-container {
max-width: 600px;
margin: 0 auto;
}
.accordion-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #e0e0e0;
}
.header-controls {
display: flex;
gap: 12px;
align-items: center;
}
.faq-accordion {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.faq-item:not(:last-child) {
border-bottom: 1px solid #e0e0e0;
}
.faq-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: #fafafa;
cursor: pointer;
transition: background-color 0.2s;
outline: none;
}
.faq-header:hover:not(.disabled) {
background: #f0f0f0;
}
.faq-header:focus {
background: #e3f2fd;
box-shadow: inset 0 0 0 2px #2196f3;
}
.faq-header.expanded {
background: #e8f5e8;
}
.faq-header.disabled {
opacity: 0.6;
cursor: not-allowed;
}
.faq-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.faq-icon {
font-size: 18px;
}
.expand-indicator {
transition: transform 0.2s ease;
font-size: 12px;
}
.expand-indicator.rotated {
transform: rotate(180deg);
}
.faq-content {
background: white;
}
.content-inner {
padding: 20px;
}
.content-actions {
margin-top: 12px;
display: flex;
gap: 16px;
}
.link-button {
background: none;
border: none;
color: #2196f3;
cursor: pointer;
padding: 4px 0;
font-size: 14px;
}
.link-button:hover {
text-decoration: underline;
}
.accordion-footer {
margin-top: 16px;
text-align: center;
color: #666;
font-size: 14px;
}
`]
})
export class AdvancedAccordionExample {
@ViewChildren(CdkAccordionItem) accordionItems!: QueryList<CdkAccordionItem>;
multiExpand = false;
sections: AccordionSection[] = [
{
id: '1',
title: 'How do I reset my password?',
content: 'To reset your password, click on the "Forgot Password" link on the login page and follow the instructions sent to your email.',
icon: '🔐'
},
{
id: '2',
title: 'How can I update my profile information?',
content: 'You can update your profile by going to Settings > Profile and editing the fields you want to change.',
icon: '👤'
},
{
id: '3',
title: 'What payment methods do you accept?',
content: 'We accept all major credit cards, PayPal, and bank transfers. Premium users also have access to additional payment options.',
icon: '💳'
},
{
id: '4',
title: 'How do I contact customer support?',
content: 'You can reach our customer support team through the chat widget, email at support@example.com, or by calling 1-800-SUPPORT.',
icon: '📞',
disabled: false
},
{
id: '5',
title: 'Is my data secure?',
content: 'Yes, we use industry-standard encryption and security practices to protect your data. All communications are encrypted and we never share your personal information.',
icon: '🛡️'
}
];
expandAll() {
this.accordionItems.forEach(item => {
if (!item.disabled) {
item.open();
}
});
}
collapseAll() {
this.accordionItems.forEach(item => {
item.close();
});
}
onSectionOpened(section: AccordionSection) {
console.log(`Section "${section.title}" opened`);
}
onSectionClosed(section: AccordionSection) {
console.log(`Section "${section.title}" closed`);
}
onExpandedChange(section: AccordionSection, expanded: boolean) {
console.log(`Section "${section.title}" ${expanded ? 'expanded' : 'collapsed'}`);
}
markAsHelpful(section: AccordionSection) {
console.log(`Marked section "${section.title}" as helpful`);
}
reportIssue(section: AccordionSection) {
console.log(`Reported issue with section "${section.title}"`);
}
getExpandedCount(): number {
return this.accordionItems?.filter(item => item.expanded).length || 0;
}
trackBySection(index: number, section: AccordionSection): string {
return section.id;
}
}/**
* Injection token for accordion instances
*/
const CDK_ACCORDION: InjectionToken<CdkAccordion>;/**
* Angular module that includes all CDK accordion directives
*/
class CdkAccordionModule {}