> For the complete documentation index, see [llms.txt](https://docs.aurelia.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aurelia.io/components/component-recipes/accordion.md).

# Accordion

Learn to build a simple yet powerful accordion component for collapsible content panels. Perfect for FAQs, settings panels, and content organization.

## What We're Building

An accordion that supports:

* Expand/collapse panels
* Single or multiple panels open
* Smooth animations
* Keyboard navigation
* Accessible with ARIA attributes
* Customizable styling

## Component Code

### accordion.ts

```typescript
import { bindable } from 'aurelia';

export class Accordion {
  @bindable allowMultiple = false;
  @bindable openPanels: number[] = [];

  togglePanel(index: number) {
    if (this.allowMultiple) {
      // Multiple panels can be open
      const panelIndex = this.openPanels.indexOf(index);
      if (panelIndex > -1) {
        this.openPanels.splice(panelIndex, 1);
      } else {
        this.openPanels.push(index);
      }
    } else {
      // Only one panel can be open
      if (this.isPanelOpen(index)) {
        this.openPanels = [];
      } else {
        this.openPanels = [index];
      }
    }
  }

  isPanelOpen(index: number): boolean {
    return this.openPanels.includes(index);
  }
}
```

### accordion.html

```html
<div class="accordion">
  <au-slot></au-slot>
</div>
```

### accordion-panel.ts

```typescript
import { bindable, resolve } from 'aurelia';
import { Accordion } from './accordion';

export class AccordionPanel {
  @bindable title = '';
  @bindable index = 0;

  private accordion = resolve(Accordion);

  get isOpen(): boolean {
    return this.accordion.isPanelOpen(this.index);
  }

  toggle() {
    this.accordion.togglePanel(this.index);
  }

  handleKeyDown(event: KeyboardEvent) {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      this.toggle();
    }
  }
}
```

### accordion-panel.html

```html
<div class="accordion-panel \${isOpen ? 'accordion-panel--open' : ''}">
  <button
    type="button"
    class="accordion-panel__header"
    click.trigger="toggle()"
    keydown.trigger="handleKeyDown($event)"
    aria-expanded.bind="isOpen">

    <span class="accordion-panel__title">\${title}</span>

    <svg class="accordion-panel__icon" width="16" height="16" viewBox="0 0 16 16">
      <path d="M8 12L2 6h12z" fill="currentColor"/>
    </svg>
  </button>

  <div
    class="accordion-panel__content"
    aria-hidden.bind="!isOpen">
    <div class="accordion-panel__body">
      <au-slot></au-slot>
    </div>
  </div>
</div>
```

### accordion.css

```css
.accordion {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow: hidden;
}

.accordion-panel {
  border-bottom: 1px solid #e5e7eb;
}

.accordion-panel:last-child {
  border-bottom: none;
}

.accordion-panel__header {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  background: white;
  border: none;
  cursor: pointer;
  transition: background 0.15s;
  text-align: left;
  font-size: 16px;
  font-weight: 500;
}

.accordion-panel__header:hover {
  background: #f9fafb;
}

.accordion-panel__header:focus {
  outline: 2px solid #3b82f6;
  outline-offset: -2px;
  z-index: 1;
}

.accordion-panel__title {
  color: #111827;
}

.accordion-panel__icon {
  color: #6b7280;
  transition: transform 0.2s;
  flex-shrink: 0;
}

.accordion-panel--open .accordion-panel__icon {
  transform: rotate(180deg);
}

.accordion-panel__content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease-out;
}

.accordion-panel--open .accordion-panel__content {
  max-height: 1000px; /* Adjust based on your content */
}

.accordion-panel__body {
  padding: 0 20px 16px;
  color: #374151;
  line-height: 1.6;
}
```

## Usage Examples

### Basic Accordion (Single Panel Open)

```html
<accordion>
  <accordion-panel index="0" title="What is Aurelia?">
    Aurelia is a modern JavaScript framework for building web applications.
  </accordion-panel>

  <accordion-panel index="1" title="How do I install Aurelia?">
    You can install Aurelia using npm: <code>npm install aurelia</code>
  </accordion-panel>

  <accordion-panel index="2" title="Where can I learn more?">
    Check out the official documentation at docs.aurelia.io
  </accordion-panel>
</accordion>
```

### Multiple Panels Open

```html
<accordion allow-multiple.bind="true">
  <accordion-panel index="0" title="Account Settings">
    <p>Manage your account settings here.</p>
  </accordion-panel>

  <accordion-panel index="1" title="Privacy Settings">
    <p>Control your privacy preferences.</p>
  </accordion-panel>

  <accordion-panel index="2" title="Notification Settings">
    <p>Configure your notification preferences.</p>
  </accordion-panel>
</accordion>
```

### Controlled Open Panels

```typescript
// your-component.ts
export class FAQPage {
  openPanels = [0]; // First panel open by default

  openAll() {
    this.openPanels = [0, 1, 2, 3];
  }

  closeAll() {
    this.openPanels = [];
  }
}
```

```html
<!-- your-component.html -->
<div>
  <button click.trigger="openAll()">Expand All</button>
  <button click.trigger="closeAll()">Collapse All</button>
</div>

<accordion allow-multiple.bind="true" open-panels.bind="openPanels">
  <accordion-panel index="0" title="Question 1">Answer 1</accordion-panel>
  <accordion-panel index="1" title="Question 2">Answer 2</accordion-panel>
  <accordion-panel index="2" title="Question 3">Answer 3</accordion-panel>
  <accordion-panel index="3" title="Question 4">Answer 4</accordion-panel>
</accordion>
```

### With Rich Content

```html
<accordion>
  <accordion-panel index="0" title="Product Features">
    <ul>
      <li>Feature 1: Fast performance</li>
      <li>Feature 2: Easy to use</li>
      <li>Feature 3: Highly customizable</li>
    </ul>
  </accordion-panel>

  <accordion-panel index="1" title="Pricing">
    <div class="pricing-grid">
      <div class="plan">
        <h3>Basic</h3>
        <p>$9/month</p>
      </div>
      <div class="plan">
        <h3>Pro</h3>
        <p>$29/month</p>
      </div>
    </div>
  </accordion-panel>
</accordion>
```

## Testing

```typescript
import { createFixture } from '@aurelia/testing';
import { Accordion } from './accordion';
import { AccordionPanel } from './accordion-panel';

describe('Accordion', () => {
  it('toggles panel open/closed', async () => {
    const { getAllBy, trigger, stop } = await createFixture
      .html`
        <accordion>
          <accordion-panel index="0" title="Panel 1">Content 1</accordion-panel>
        </accordion>
      `
      .deps(Accordion, AccordionPanel)
      .build()
      .started;

    const panel = getAllBy('.accordion-panel')[0];

    // Initially closed
    expect(panel.classList.contains('accordion-panel--open')).toBe(false);

    // Click to open
    trigger.click('.accordion-panel__header');
    expect(panel.classList.contains('accordion-panel--open')).toBe(true);

    // Click to close
    trigger.click('.accordion-panel__header');
    expect(panel.classList.contains('accordion-panel--open')).toBe(false);

    await stop(true);
  });

  it('allows only one panel open when allowMultiple=false', async () => {
    const { component, getAllBy, trigger, stop } = await createFixture
      .html`
        <accordion allow-multiple.bind="false">
          <accordion-panel index="0" title="Panel 1">Content 1</accordion-panel>
          <accordion-panel index="1" title="Panel 2">Content 2</accordion-panel>
        </accordion>
      `
      .deps(Accordion, AccordionPanel)
      .build()
      .started;

    // Open first panel
    trigger.click('.accordion-panel:first-child .accordion-panel__header');
    expect(component.openPanels).toEqual([0]);

    // Open second panel
    trigger.click('.accordion-panel:nth-child(2) .accordion-panel__header');
    expect(component.openPanels).toEqual([1]); // First closed, second open

    await stop(true);
  });

  it('allows multiple panels open when allowMultiple=true', async () => {
    const { component, getAllBy, trigger, stop } = await createFixture
      .html`
        <accordion allow-multiple.bind="true">
          <accordion-panel index="0" title="Panel 1">Content 1</accordion-panel>
          <accordion-panel index="1" title="Panel 2">Content 2</accordion-panel>
        </accordion>
      `
      .deps(Accordion, AccordionPanel)
      .build()
      .started;

    // Open first panel
    trigger.click('.accordion-panel:first-child .accordion-panel__header');
    expect(component.openPanels).toEqual([0]);

    // Open second panel
    trigger.click('.accordion-panel:nth-child(2) .accordion-panel__header');
    expect(component.openPanels).toEqual([0, 1]); // Both open

    await stop(true);
  });

  it('supports keyboard navigation', async () => {
    const { getAllBy, trigger, stop } = await createFixture
      .html`
        <accordion>
          <accordion-panel index="0" title="Panel 1">Content 1</accordion-panel>
        </accordion>
      `
      .deps(Accordion, AccordionPanel)
      .build()
      .started;

    const button = getAllBy('.accordion-panel__header')[0];
    const panel = getAllBy('.accordion-panel')[0];

    // Press Enter to open
    trigger.keydown(button, { key: 'Enter' });
    expect(panel.classList.contains('accordion-panel--open')).toBe(true);

    // Press Space to close
    trigger.keydown(button, { key: ' ' });
    expect(panel.classList.contains('accordion-panel--open')).toBe(false);

    await stop(true);
  });
});
```

## Accessibility Features

This accordion implements WCAG 2.1 guidelines:

* ✅ **ARIA Attributes**: `aria-expanded` indicates panel state
* ✅ **Keyboard Support**: Enter and Space keys toggle panels
* ✅ **Focus Management**: Buttons are focusable with visible focus indicators
* ✅ **Semantic HTML**: Uses `<button>` for interactive headers

## Enhancements

### 1. Add Animation Callbacks

```typescript
export class AnimatedAccordion extends Accordion {
  @bindable onBeforeOpen?: (index: number) => void;
  @bindable onAfterOpen?: (index: number) => void;

  togglePanel(index: number) {
    const wasOpen = this.isPanelOpen(index);

    if (!wasOpen && this.onBeforeOpen) {
      this.onBeforeOpen(index);
    }

    super.togglePanel(index);

    if (!wasOpen && this.onAfterOpen) {
      setTimeout(() => this.onAfterOpen!(index), 300); // After animation
    }
  }
}
```

### 2. Add Icons

```html
<accordion-panel index="0" title="Custom Icon">
  <svg au-slot="icon" width="20" height="20">
    <!-- Custom icon -->
  </svg>

  Panel content here
</accordion-panel>
```

### 3. Add Lazy Loading

```typescript
export class LazyAccordionPanel extends AccordionPanel {
  @bindable loadContent?: () => Promise<any>;
  content: any = null;
  loaded = false;

  async toggle() {
    super.toggle();

    if (this.isOpen && !this.loaded && this.loadContent) {
      this.content = await this.loadContent();
      this.loaded = true;
    }
  }
}
```

## Best Practices

1. **Animation Performance**: Use `max-height` instead of `height: auto` for smooth transitions
2. **Content Height**: Set reasonable `max-height` values or calculate dynamically
3. **Accessibility**: Always include `aria-expanded` for screen readers
4. **Focus Visible**: Ensure keyboard focus is clearly visible
5. **Mobile**: Test touch interactions and ensure adequate tap targets

## Summary

You've built a fully-functional accordion with:

* ✅ Single and multiple panel modes
* ✅ Smooth animations
* ✅ Keyboard support
* ✅ Accessible markup
* ✅ Easy customization

This accordion is ready for FAQs, settings panels, and any collapsible content!


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.aurelia.io/components/component-recipes/accordion.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
