AvesraAvesrabeta

Accordion

A collapsible content panel for organizing information in a compact space

Import

import { AvAccordionImports } from '@avesra/angular';

Usage

import { Component } from '@angular/core';
import { AvAccordionImports } from '@avesra/angular';

import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-accordion-basic-demo',
  imports: [
    AvAccordionImports,
    AppIconComponent,
  ],
  template: `<div class="mx-auto w-full max-w-md">
      <av-accordion class="w-full max-w-md">
        @for (item of defaultItems; track item.title; let index = $index) {
          <av-accordion-item [id]="'default-' + index">
            <h3 av-accordion-heading>
              <button av-accordion-trigger>
                @if (item.icon) {
                  <span class="me-3 inline-flex size-4 shrink-0 text-muted" aria-hidden="true">
                    <app-icon [icon]="item.icon" size="16" />
                  </span>
                }
                {{ item.title }}
                <svg av-accordion-indicator></svg>
              </button>
            </h3>
            <div av-accordion-panel>
              <div av-accordion-body>{{ item.content }}</div>
            </div>
          </av-accordion-item>
        }
      </av-accordion>
    </div>`,
})
export class AccordionBasicDemo {
  readonly defaultItems = [
    {
      title: 'How do I place an order?',
      content:
        "Browse our products, add items to your cart, and proceed to checkout. You'll need to provide shipping and payment information to complete your purchase.",
      icon: 'solar:bag-linear',
    },
    {
      title: 'Can I modify or cancel my order?',
      content:
        "Yes, you can modify or cancel your order before it's shipped. Once your order is processed, you can't make changes.",
      icon: 'solar:bill-list-linear',
    },
    {
      title: 'What payment methods do you accept?',
      content: 'We accept all major credit cards, including Visa, Mastercard, and American Express.',
      icon: 'solar:card-linear',
    },
    {
      title: 'How much does shipping cost?',
      content:
        'Shipping costs vary based on your location and the size of your order. We offer free shipping for orders over $50.',
      icon: 'solar:box-linear',
    },
    {
      title: 'Do you ship internationally?',
      content:
        'Yes, we ship to most countries. Please check our shipping rates and policies for more information.',
      icon: 'solar:global-linear',
    },
    {
      title: 'How do I request a refund?',
      content:
        "If you're not satisfied with your purchase, you can request a refund within 30 days of purchase. Please contact our customer support team for assistance.",
      icon: 'solar:refresh-linear',
    },
  ];
}

Anatomy

<av-accordion>
  <av-accordion-item id="item-1">
    <h3 av-accordion-heading>
      <button av-accordion-trigger>
        Section title
        <svg av-accordion-indicator></svg>
      </button>
    </h3>
    <div av-accordion-panel>
      <div av-accordion-body></div>
    </div>
  </av-accordion-item>
</av-accordion>

Examples

Surface

import { Component } from '@angular/core';
import { AvAccordionImports } from '@avesra/angular';

import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-accordion-surface-demo',
  imports: [
    AvAccordionImports,
    AppIconComponent,
  ],
  template: `<div class="mx-auto w-full max-w-md">
      <av-accordion class="w-full max-w-md" variant="surface">
        @for (item of defaultItems; track item.title; let index = $index) {
          <av-accordion-item [id]="'surface-' + index">
            <h3 av-accordion-heading>
              <button av-accordion-trigger>
                @if (item.icon) {
                  <span class="me-3 inline-flex size-4 shrink-0 text-muted" aria-hidden="true">
                    <app-icon [icon]="item.icon" size="16" />
                  </span>
                }
                {{ item.title }}
                <svg av-accordion-indicator></svg>
              </button>
            </h3>
            <div av-accordion-panel>
              <div av-accordion-body>{{ item.content }}</div>
            </div>
          </av-accordion-item>
        }
      </av-accordion>
    </div>`,
})
export class AccordionSurfaceDemo {
  readonly defaultItems = [
    {
      title: 'How do I place an order?',
      content:
        "Browse our products, add items to your cart, and proceed to checkout. You'll need to provide shipping and payment information to complete your purchase.",
      icon: 'solar:bag-linear',
    },
    {
      title: 'Can I modify or cancel my order?',
      content:
        "Yes, you can modify or cancel your order before it's shipped. Once your order is processed, you can't make changes.",
      icon: 'solar:bill-list-linear',
    },
    {
      title: 'What payment methods do you accept?',
      content: 'We accept all major credit cards, including Visa, Mastercard, and American Express.',
      icon: 'solar:card-linear',
    },
    {
      title: 'How much does shipping cost?',
      content:
        'Shipping costs vary based on your location and the size of your order. We offer free shipping for orders over $50.',
      icon: 'solar:box-linear',
    },
    {
      title: 'Do you ship internationally?',
      content:
        'Yes, we ship to most countries. Please check our shipping rates and policies for more information.',
      icon: 'solar:global-linear',
    },
    {
      title: 'How do I request a refund?',
      content:
        "If you're not satisfied with your purchase, you can request a refund within 30 days of purchase. Please contact our customer support team for assistance.",
      icon: 'solar:refresh-linear',
    },
  ];
}

Without Separator

import { Component } from '@angular/core';
import { AvAccordionImports } from '@avesra/angular';

import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-accordion-without-separator-demo',
  imports: [
    AvAccordionImports,
    AppIconComponent,
  ],
  template: `<div class="mx-auto w-full max-w-md">
      <av-accordion hide-separator>
        @for (item of defaultItems; track item.title; let index = $index) {
          <av-accordion-item [id]="'no-sep-' + index">
            <h3 av-accordion-heading>
              <button av-accordion-trigger>
                @if (item.icon) {
                  <span class="me-3 inline-flex size-4 shrink-0 text-muted" aria-hidden="true">
                    <app-icon [icon]="item.icon" size="16" />
                  </span>
                }
                {{ item.title }}
                <svg av-accordion-indicator></svg>
              </button>
            </h3>
            <div av-accordion-panel>
              <div av-accordion-body>{{ item.content }}</div>
            </div>
          </av-accordion-item>
        }
      </av-accordion>
    </div>`,
})
export class AccordionWithoutSeparatorDemo {
  readonly defaultItems = [
    {
      title: 'How do I place an order?',
      content:
        "Browse our products, add items to your cart, and proceed to checkout. You'll need to provide shipping and payment information to complete your purchase.",
      icon: 'solar:bag-linear',
    },
    {
      title: 'Can I modify or cancel my order?',
      content:
        "Yes, you can modify or cancel your order before it's shipped. Once your order is processed, you can't make changes.",
      icon: 'solar:bill-list-linear',
    },
    {
      title: 'What payment methods do you accept?',
      content: 'We accept all major credit cards, including Visa, Mastercard, and American Express.',
      icon: 'solar:card-linear',
    },
  ];
}

Multiple Expanded

import { Component } from '@angular/core';
import { AvAccordionImports } from '@avesra/angular';

@Component({
  selector: 'app-accordion-multiple-demo',
  imports: [
    AvAccordionImports,
  ],
  host: { class: 'flex w-full justify-center' },
  template: `<av-accordion class="w-full max-w-md" allows-multiple>
      <av-accordion-item id="getting-started">
        <h3 av-accordion-heading>
          <button av-accordion-trigger>
            Getting Started
            <svg av-accordion-indicator></svg>
          </button>
        </h3>
        <div av-accordion-panel>
          <div av-accordion-body>
            Learn the basics of Avesra and how to integrate it into your Angular project. This
            section covers installation, setup, and your first component.
          </div>
        </div>
      </av-accordion-item>

      <av-accordion-item id="core-concepts">
        <h3 av-accordion-heading>
          <button av-accordion-trigger>
            Core Concepts
            <svg av-accordion-indicator></svg>
          </button>
        </h3>
        <div av-accordion-panel>
          <div av-accordion-body>
            Understand the fundamental concepts behind Avesra, including the compound component
            pattern, styling with Tailwind CSS, and accessibility features.
          </div>
        </div>
      </av-accordion-item>

      <av-accordion-item id="advanced-usage">
        <h3 av-accordion-heading>
          <button av-accordion-trigger>
            Advanced Usage
            <svg av-accordion-indicator></svg>
          </button>
        </h3>
        <div av-accordion-panel>
          <div av-accordion-body>
            Explore advanced features like custom variants, theme customization, and integration
            with other libraries in your Angular ecosystem.
          </div>
        </div>
      </av-accordion-item>

      <av-accordion-item id="best-practices">
        <h3 av-accordion-heading>
          <button av-accordion-trigger>
            Best Practices
            <svg av-accordion-indicator></svg>
          </button>
        </h3>
        <div av-accordion-panel>
          <div av-accordion-body>
            Follow our recommended best practices for building performant, accessible, and
            maintainable applications with Avesra components.
          </div>
        </div>
      </av-accordion-item>
    </av-accordion>`,
})
export class AccordionMultipleDemo {}

Disabled State

Entire accordion disabled

Individual items disabled

import { Component } from '@angular/core';
import { AvAccordionImports } from '@avesra/angular';

@Component({
  selector: 'app-accordion-disabled-demo',
  imports: [
    AvAccordionImports,
  ],
  template: `<div class="flex w-full flex-col items-center gap-8">
      <div class="w-full max-w-md space-y-2">
        <h3 class="text-sm font-medium text-muted">Entire accordion disabled</h3>
        <av-accordion class="w-full max-w-md" disabled>
          <av-accordion-item id="disabled-all-1">
            <h3 av-accordion-heading>
              <button av-accordion-trigger>
                Disabled Item 1
                <svg av-accordion-indicator></svg>
              </button>
            </h3>
            <div av-accordion-panel>
              <div av-accordion-body>
                This content cannot be accessed when the accordion is disabled.
              </div>
            </div>
          </av-accordion-item>
          <av-accordion-item id="disabled-all-2">
            <h3 av-accordion-heading>
              <button av-accordion-trigger>
                Disabled Item 2
                <svg av-accordion-indicator></svg>
              </button>
            </h3>
            <div av-accordion-panel>
              <div av-accordion-body>
                This content cannot be accessed when the accordion is disabled.
              </div>
            </div>
          </av-accordion-item>
        </av-accordion>
      </div>

      <div class="w-full max-w-md space-y-2">
        <h3 class="text-sm font-medium text-muted">Individual items disabled</h3>
        <av-accordion class="w-full max-w-md">
          <av-accordion-item id="disabled-item-1">
            <h3 av-accordion-heading>
              <button av-accordion-trigger>
                Active Item
                <svg av-accordion-indicator></svg>
              </button>
            </h3>
            <div av-accordion-panel>
              <div av-accordion-body>This item is active and can be toggled normally.</div>
            </div>
          </av-accordion-item>
          <av-accordion-item id="disabled-item-2" disabled>
            <h3 av-accordion-heading>
              <button av-accordion-trigger>
                Disabled Item
                <svg av-accordion-indicator></svg>
              </button>
            </h3>
            <div av-accordion-panel>
              <div av-accordion-body>
                This content cannot be accessed when the item is disabled.
              </div>
            </div>
          </av-accordion-item>
          <av-accordion-item id="disabled-item-3">
            <h3 av-accordion-heading>
              <button av-accordion-trigger>
                Another Active Item
                <svg av-accordion-indicator></svg>
              </button>
            </h3>
            <div av-accordion-panel>
              <div av-accordion-body>This item is also active and can be toggled.</div>
            </div>
          </av-accordion-item>
        </av-accordion>
      </div>
    </div>`,
})
export class AccordionDisabledDemo {}

Controlled

Expanded: getting-started

Learn the basics of Avesra and how to integrate it into your Angular project. This section covers installation, setup, and your first component.

import { Component, computed, signal } from '@angular/core';
import { AvAccordionImports, AvButtonComponent } from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-accordion-controlled-demo',
  imports: [
    AvAccordionImports,
    AvButtonComponent,
    AppIconComponent,
  ],
  template: `<div class="w-full max-w-md">
      <div class="mb-4 flex items-center justify-between">
        <p class="text-sm text-muted">
          Expanded: <strong>{{ expandedLabel() }}</strong>
        </p>
        <div class="flex gap-2">
          <button
            av-button
            type="button"
            variant="secondary"
            size="sm"
            aria-label="Previous item"
            [disabled]="isPrevDisabled()"
            (click)="onPrevious()"
          >
            <app-icon icon="solar:alt-arrow-up-linear" size="16" />
          </button>
          <button
            av-button
            type="button"
            variant="secondary"
            size="sm"
            aria-label="Next item"
            [disabled]="isNextDisabled()"
            (click)="onNext()"
          >
            <app-icon icon="solar:alt-arrow-down-linear" size="16" />
          </button>
        </div>
      </div>
      <av-accordion [(expandedKeys)]="expandedKeys">
        @for (item of items; track item.id) {
          <av-accordion-item [id]="item.id">
            <h3 av-accordion-heading>
              <button av-accordion-trigger>
                {{ item.title }}
                <svg av-accordion-indicator></svg>
              </button>
            </h3>
            <div av-accordion-panel>
              <div av-accordion-body>{{ item.content }}</div>
            </div>
          </av-accordion-item>
        }
      </av-accordion>
    </div>`,
})
export class AccordionControlledDemo {
  readonly items = [
    {
      id: 'getting-started',
      title: 'Getting Started',
      content:
        'Learn the basics of Avesra and how to integrate it into your Angular project. This section covers installation, setup, and your first component.',
    },
    {
      id: 'core-concepts',
      title: 'Core Concepts',
      content:
        'Understand the fundamental concepts behind Avesra, including the compound component pattern, styling with Tailwind CSS, and accessibility features.',
    },
    {
      id: 'advanced-usage',
      title: 'Advanced Usage',
      content:
        'Explore advanced features like custom variants, theme customization, and integration with other libraries in your Angular ecosystem.',
    },
  ];

  readonly expandedKeys = signal<string[]>(['getting-started']);
  readonly itemIds = this.items.map((item) => item.id);

  readonly expandedLabel = computed(() => this.expandedKeys().join(', ') || 'none');

  readonly isPrevDisabled = computed(() => {
    const current = this.expandedKeys()[0];
    return !current || this.itemIds.indexOf(current) <= 0;
  });

  readonly isNextDisabled = computed(() => {
    const current = this.expandedKeys()[0];
    const index = this.itemIds.indexOf(current);
    return index < 0 || index >= this.itemIds.length - 1;
  });

  onPrevious(): void {
    const current = this.expandedKeys()[0];
    const index = this.itemIds.indexOf(current);
    if (index > 0) {
      this.expandedKeys.set([this.itemIds[index - 1]]);
    }
  }

  onNext(): void {
    const current = this.expandedKeys()[0];
    const index = this.itemIds.indexOf(current);
    if (index >= 0 && index < this.itemIds.length - 1) {
      this.expandedKeys.set([this.itemIds[index + 1]]);
    }
  }
}

Custom Indicator

import { Component, signal } from '@angular/core';
import { AvAccordionImports } from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-accordion-custom-indicator-demo',
  imports: [
    AvAccordionImports,
    AppIconComponent,
  ],
  host: { class: 'flex w-full justify-center' },
  template: `<av-accordion
      class="w-full max-w-md"
      variant="surface"
      [(expandedKeys)]="expandedKeys"
    >
      <av-accordion-item id="1">
        <h3 av-accordion-heading>
          <button av-accordion-trigger>
            Using Plus/Minus Icon
            <span
              av-accordion-indicator
              class="inline-flex size-4 items-center justify-center data-[expanded=true]:!rotate-0"
            >
              @if (expandedKeys().includes('1')) {
                <app-icon icon="solar:minus-circle-linear" size="16" />
              } @else {
                <app-icon icon="solar:add-circle-linear" size="16" />
              }
            </span>
          </button>
        </h3>
        <div av-accordion-panel>
          <div av-accordion-body>
            This accordion uses a plus icon that transforms when expanded. The icon automatically
            rotates 45 degrees to form an X.
          </div>
        </div>
      </av-accordion-item>

      <av-accordion-item id="2">
        <h3 av-accordion-heading>
          <button av-accordion-trigger>
            Using Caret Icon
            <span av-accordion-indicator class="inline-flex size-4 items-center justify-center">
              <app-icon icon="solar:alt-arrow-down-linear" size="16" />
            </span>
          </button>
        </h3>
        <div av-accordion-panel>
          <div av-accordion-body>
            This item uses a caret icon for the indicator. The rotation animation is applied
            automatically.
          </div>
        </div>
      </av-accordion-item>

      <av-accordion-item id="3">
        <h3 av-accordion-heading>
          <button av-accordion-trigger>
            Using Arrow Icon
            <span av-accordion-indicator class="inline-flex size-4 items-center justify-center">
              <app-icon icon="solar:double-alt-arrow-down-linear" size="16" />
            </span>
          </button>
        </h3>
        <div av-accordion-panel>
          <div av-accordion-body>
            This item uses an arrow icon. Any icon you pass will receive the rotation animation when
            the item expands.
          </div>
        </div>
      </av-accordion-item>
    </av-accordion>`,
})
export class AccordionCustomIndicatorDemo {
  readonly expandedKeys = signal<string[]>([]);
}

FAQ Layout

Frequently Asked Questions

Everything you need to know about licensing and usage.

General

Licensing

Support

import { Component } from '@angular/core';
import { AvAccordionImports } from '@avesra/angular';

@Component({
  selector: 'app-accordion-custom-faq-demo',
  imports: [
    AvAccordionImports,
  ],
  template: `<div class="flex w-full flex-col gap-6">
      <div class="flex flex-col gap-1">
        <h2 class="text-2xl font-bold">Frequently Asked Questions</h2>
        <p class="mb-4 text-lg font-medium text-muted">
          Everything you need to know about licensing and usage.
        </p>
      </div>
      @for (category of categories; track category.title) {
        <div>
          <p class="text-md mb-2 font-medium text-muted">{{ category.title }}</p>
          <av-accordion class="w-full" variant="surface">
            @for (item of category.items; track item.title; let index = $index) {
              <av-accordion-item [id]="category.title + '-' + index">
                <h3 av-accordion-heading>
                  <button av-accordion-trigger>
                    {{ item.title }}
                    <svg av-accordion-indicator></svg>
                  </button>
                </h3>
                <div av-accordion-panel>
                  <div av-accordion-body>{{ item.content }}</div>
                </div>
              </av-accordion-item>
            }
          </av-accordion>
        </div>
      }
    </div>`,
})
export class AccordionCustomFaqDemo {
  readonly categories = [
  {
    "title": "General",
    "items": [
      {
        "title": "How do I place an order?",
        "content": "Browse our products, add items to your cart, and proceed to checkout. You'll need to provide shipping and payment information to complete your purchase."
      },
      {
        "title": "Can I modify or cancel my order?",
        "content": "Yes, you can modify or cancel your order before it's shipped. Once your order is processed, you can't make changes."
      }
    ]
  },
  {
    "title": "Licensing",
    "items": [
      {
        "title": "How do I purchase a license?",
        "content": "You can purchase a license directly from our website. Select the license type that fits your needs and proceed to checkout."
      },
      {
        "title": "What is the difference between a standard and a pro license?",
        "content": "A standard license is for personal use or small projects, while a pro license includes commercial use rights and priority support."
      }
    ]
  },
  {
    "title": "Support",
    "items": [
      {
        "title": "How do I get support?",
        "content": "You can reach our support team through the contact form on our website, or email us directly at [email protected]."
      }
    ]
  }
];
}

Customization

Tailwind CSS

import { Component } from '@angular/core';
import { AvAccordionImports } from '@avesra/angular';

@Component({
  selector: 'app-accordion-custom-styles-demo',
  imports: [
    AvAccordionImports,
  ],
  host: { class: 'flex w-full justify-center' },
  template: `<av-accordion class="w-full max-w-md rounded-2xl bg-surface/10" variant="surface">
      @for (item of items; track item.title; let index = $index) {
        <av-accordion-item [id]="'custom-styles-' + index" class="group/item">
          <h3 av-accordion-heading>
            <button
              av-accordion-trigger
              class="group flex items-center gap-2 transition-none hover:bg-surface"
            >
              <img
                [alt]="item.title"
                class="h-11 w-11 transition-[scale,rotate] duration-300 ease-out group-hover/item:scale-120 group-hover/item:-rotate-10 group-hover/item:drop-shadow-lg"
                [src]="item.iconUrl"
              />
              <div class="flex flex-col gap-0">
                <span class="font-medium leading-5">{{ item.title }}</span>
                <span class="font-normal leading-6 text-muted/80">{{ item.subtitle }}</span>
              </div>
              <svg av-accordion-indicator class="text-muted/50"></svg>
            </button>
          </h3>
          <div av-accordion-panel>
            <div av-accordion-body class="text-muted/80">{{ item.content }}</div>
          </div>
        </av-accordion-item>
      }
    </av-accordion>`,
})
export class AccordionCustomStylesDemo {
  readonly items = [
  {
    "title": "Set Up Notifications",
    "subtitle": "Receive account activity updates",
    "content": "Stay informed about your account activity with real-time notifications.",
    "iconUrl": "/images/icons/icon-3d-alarm-clock-red.png"
  },
  {
    "title": "Secure Your Account",
    "subtitle": "Enable two-factor authentication",
    "content": "Protect your workspace by turning on two-factor authentication for sign-in.",
    "iconUrl": "/images/icons/icon-3d-shield-check.png"
  },
  {
    "title": "Launch Your Project",
    "subtitle": "Create your first workspace",
    "content": "Spin up a workspace, invite your team, and start shipping from one place.",
    "iconUrl": "/images/icons/icon-3d-rocket-launch.png"
  }
];
}

Global CSS

To customize the Accordion component classes, you can use the @layer components directive. Learn more .

@layer components {
  .av-accordion {
    @apply rounded-xl bg-muted/40;
  }

  .av-accordion__trigger {
    @apply text-lg font-semibold;
  }

  .av-accordion--surface {
    @apply border-2 shadow-lg;
  }
}

Styling Reference

Avesra follows the BEM methodology to ensure component variants and states are reusable and easy to customize.

CSS Classes

The Accordion component uses these CSS classes:

Base Classes

  • .av-accordion — Base accordion container
  • .av-accordion__body — Content body container
  • .av-accordion__heading — Heading wrapper
  • .av-accordion__indicator — Expand/collapse indicator icon
  • .av-accordion__item — Individual accordion item
  • .av-accordion__panel — Collapsible panel container
  • .av-accordion__trigger — Clickable trigger button

Variant Classes

  • .av-accordion--surface — Surface variant with raised background

State Classes

  • .av-accordion__trigger[aria-expanded="true"] — Expanded state
  • .av-accordion__panel[aria-hidden="false"] — Panel visible state

Interactive States

The component supports both CSS pseudo-classes and data attributes for flexibility:

  • Hover::hover or [data-hovered="true"] on trigger
  • Focus::focus-visible or [data-focus-visible="true"] on trigger
  • Disabled::disabled or [aria-disabled="true"] on trigger
  • Expanded:[aria-expanded="true"] on trigger

API Reference

See also Disclosure for single-section collapsible content.

PropTypeDefaultDescription
variant'default' | 'surface''default'Visual style variant (av-accordion).
hide-separatorbooleanfalseHides separators between accordion items (av-accordion).
allows-multiplebooleanfalseAllows multiple items to be expanded at once (av-accordion).
disabledbooleanfalseDisables all accordion items (av-accordion).
expandedKeysstring[][]Expanded item ids. Supports two-way binding with [(expandedKeys)] (av-accordion).
idstring—Required unique accordion item id (av-accordion-item).
disabledbooleanfalseDisables a single accordion item (av-accordion-item).

Made with ❤ by SyntaxHertz. Open source and available on GitHub.