The accordion is one of those components that looks simple but has several patterns depending on the use case. Here's how I build them in Angular 21.

Simple Data-Driven Accordion

The cleanest approach for FAQ pages and help sections:

// faq-accordion.component.ts
import { Component, Input, signal } from '@angular/core'
import { CommonModule } from '@angular/common'

export interface AccordionItem {
  question: string
  answer: string
}

@Component({
  selector: 'app-faq-accordion',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="accordion" [id]="accordionId">
      @for (item of items; track item.question; let i = $index) {
        <div class="accordion-item border-0 border-bottom">
          <h3 class="accordion-header">
            <button class="accordion-button fw-semibold"
              [class.collapsed]="activeIndex() !== i"
              type="button"
              (click)="toggle(i)"
              [attr.aria-expanded]="activeIndex() === i">
              {{ item.question }}
            </button>
          </h3>
          <div class="accordion-collapse"
            [class.show]="activeIndex() === i">
            <div class="accordion-body text-muted">
              {{ item.answer }}
            </div>
          </div>
        </div>
      }
    </div>
  `,
  styles: [`
    .accordion-button { background: #fff; color: #0d0d0d; font-size: 0.95rem; }
    .accordion-button:not(.collapsed) { color: #fd4766; background: #fff8f9; box-shadow: none; }
    .accordion-button::after { filter: none; }
    .accordion-button:not(.collapsed)::after { filter: invert(30%) sepia(90%) saturate(400%) hue-rotate(310deg); }
    .accordion-collapse { display: none; }
    .accordion-collapse.show { display: block; }
  `]
})
export class FaqAccordionComponent {
  @Input() items: AccordionItem[] = []
  @Input() accordionId = 'faqAccordion'

  activeIndex = signal<number | null>(0)

  toggle(index: number) {
    this.activeIndex.update(current => current === index ? null : index)
  }
}

Always-Open Accordion (Multiple Panels)

When you want multiple panels open simultaneously:

// multi-accordion.component.ts
import { Component, Input, signal } from '@angular/core'
import { CommonModule } from '@angular/common'

@Component({
  selector: 'app-multi-accordion',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="accordion accordion-flush">
      @for (item of items; track item.id; let i = $index) {
        <div class="accordion-item">
          <h3 class="accordion-header">
            <button class="accordion-button"
              [class.collapsed]="!openItems().has(i)"
              type="button"
              (click)="toggle(i)">
              <span class="me-2">{{ item.icon }}</span>
              {{ item.title }}
            </button>
          </h3>
          <div class="accordion-collapse" [class.show]="openItems().has(i)">
            <div class="accordion-body" [innerHTML]="item.content"></div>
          </div>
        </div>
      }
    </div>
    <div class="d-flex gap-2 mt-2">
      <button class="btn btn-sm btn-outline-secondary" (click)="expandAll()">Expand All</button>
      <button class="btn btn-sm btn-outline-secondary" (click)="collapseAll()">Collapse All</button>
    </div>
  `,
  styles: [`
    .accordion-collapse { display: none; }
    .accordion-collapse.show { display: block; }
    .accordion-button:not(.collapsed) { color: #fd4766; background: #fff8f9; box-shadow: none; }
  `]
})
export class MultiAccordionComponent {
  @Input() items: Array<{ id: string; title: string; icon?: string; content: string }> = []

  openItems = signal<Set<number>>(new Set([0])) // first open by default

  toggle(index: number) {
    this.openItems.update(current => {
      const next = new Set(current)
      next.has(index) ? next.delete(index) : next.add(index)
      return next
    })
  }

  expandAll() {
    this.openItems.set(new Set(this.items.map((_, i) => i)))
  }

  collapseAll() {
    this.openItems.set(new Set())
  }
}

Using the Components

// my-page.component.ts
import { Component } from '@angular/core'
import { FaqAccordionComponent } from '../faq-accordion/faq-accordion.component'

@Component({
  standalone: true,
  imports: [FaqAccordionComponent],
  template: `
    <div class="container py-5">
      <h2 class="fw-bold mb-4">Frequently Asked Questions</h2>
      <app-faq-accordion [items]="faqItems" />
    </div>
  `
})
export class FaqPageComponent {
  faqItems = [
    {
      question: 'What frameworks does BootstrapPlanet cover?',
      answer: 'Bootstrap 5, React with Tailwind and shadcn/ui, and Angular 21 with Bootstrap 5.'
    },
    {
      question: 'Are all components free to use?',
      answer: 'Yes. All components on BootstrapPlanet are free with copy-paste code examples.'
    },
    {
      question: 'Do you have admin dashboard templates?',
      answer: 'Yes. Premium templates are available at LettStartDesign.com. Use code FIRST30 for 30% off.'
    }
  ]
}

The component is fully reusable — pass any array of question/answer objects and it renders. No Bootstrap JS dependency, no change detection issues.

Frequently Asked Questions

For simple accordions, Bootstrap's data-bs-toggle approach works fine in Angular. For accordions driven by dynamic data, a pure Angular implementation with signals is cleaner — no Bootstrap JS needed and it works better with Angular's change detection.
Track open state per item rather than the active index. Each item has its own isOpen property. The toggle method flips item.isOpen without closing other items. In the template use *ngIf or [@expandCollapse] based on item.isOpen.
If using index-based active tracking: this.activeIndex = 2 to open the third panel. If using item-based tracking: this.items[2].isOpen = true. Expose a method in the component if you need to control it from a parent.

Related

Need a Full Angular + Bootstrap Admin Dashboard?

Marvel Angular Dashboard — Angular 21 + Bootstrap 5 with 50+ components and dark mode.

Browse Templates →

Use code FIRST30 for 30% off.