• Skip to main content
  • Skip to primary sidebar

Web Development Archive

  • Archive
You are here: Home / Angular / Accordion with animation

Accordion with animation

import { animate, state, style, transition, trigger } from '@angular/animations';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';

import { IconComponent } from '../icon/icon.component';

@Component({
selector: 'app-accordion',
standalone: true,
imports: [CommonModule, IconComponent],
templateUrl: './accordion.component.html',
styleUrl: './accordion.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('slideToggle', [
state('void', style({ height: '0px', overflow: 'hidden' })),
state('active', style({ height: '*', overflow: 'hidden' })),
transition('void <=> active', animate('300ms ease-in-out'))
])
]
})
export class AccordionComponent {
@Input() public title: string;
@Input() public desc: string;
@Input() public icon: string = 'icon-chevron-down';
@Input() public isActive: boolean;

@Output() public toggled = new EventEmitter<void>();

public toggleAccordion(): void {
this.toggled.emit();
}
}
<div class="accordion">
<div class="accordion-item">
<div class="accordion-item-title" (click)="toggleAccordion()">
<div class="accordion-item-title-text" [ngClass]="{ active: isActive }">
{{ title }}
</div>
<div class="accordion-icon" [ngClass]="{ active: isActive }">
<app-icon [icon]="icon" [size]="24" />
</div>
</div>
<div class="accordion-item-panel" [@slideToggle]="isActive ? 'active' : 'void'">
<div class="accordion-item-panel-content" [innerHTML]="desc"></div>
</div>
</div>
</div>
import { Injectable } from '@angular/core';
import { Card } from '@app/shared';

@Injectable({
providedIn: 'root'
})
export class FaqService {
public getData(): readonly Card[] {
return [
{ title: 'FAQ_1.Q', desc: 'FAQ_1.A' },
{ title: 'FAQ_2.Q', desc: 'FAQ_2.A' },
{ title: 'FAQ_3.Q', desc: 'FAQ_3.A' },
];
}
}
import { Link } from './link.model';

export type Card = Readonly<{
readonly title: string;
readonly desc?: string;
readonly cta?: Link;
}>;
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
import { AccordionComponent, Card } from '@app/shared';
import { TranslocoPipe } from '@jsverse/transloco';

import { FaqService } from '../../shared/services/faq.service';

@Component({
selector: 'app-faq',
standalone: true,
imports: [CommonModule, TranslocoPipe, AccordionComponent],
templateUrl: './faq.component.html',
styleUrl: './faq.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FaqComponent implements OnInit {
private readonly _faqService = inject(FaqService);
public activeAccordionIndex: number | null = null;
public data: readonly Card[];

public ngOnInit(): void {
this.data = this._faqService.getData();
}

public onAccordionToggle(index: number): void {
if (this.activeAccordionIndex === index) {
this.activeAccordionIndex = null;
} else {
this.activeAccordionIndex = index;
}
}
}
<section class="faq">
<div class="container">
<div class="heading">
<h2 class="heading-2">{{ 'FAQ.TITLE' | transloco }}</h2>
<p class="subtitle">{{ 'FAQ.SUBTITLE' | transloco }}</p>
</div>
<div class="accordion">
<ng-container *ngFor="let item of data; let i = index">
<app-accordion
[title]="item.title | transloco"
[desc]="item.desc | transloco"
[isActive]="activeAccordionIndex === i"
(toggled)="onAccordionToggle(i)"
/>
</ng-container>
</div>
</div>
</section>
@use 'shared' as *;

:host {
display: block;
}

.accordion {
width: 100%;

.grid-2 {
gap: 80px;
}
.title.desktop {
display: none;
@include media-breakpoint-up(md) {
display: block;
}
}
.title.mobile {
display: block;
margin-bottom: -40px;
@include media-breakpoint-up(md) {
display: none;
}
}
&-caption {
order: 2;
@include media-breakpoint-up(md) {
order: 1;
}
.title {
margin-bottom: 80px;
}
}
}

.accordion {
&-icon {
width: 24px;
height: 24px;
transition: transform 0.3s ease-in-out;
&.active {
transform: rotate(180deg);
}
}
&-item {
background-color: var(--color-white);
border: 1px solid var(--color-grey-300);
border-radius: 8px;
margin-bottom: 16px;

&-title {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 16px;
&-text {
color: var(--color-primary-950);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 120%;
max-width: 94%;
}
}
&-panel {
overflow: hidden;
color: var(--color-primary-950);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 120%;
&-content {
padding: 0 16px 16px;
}

&.active {
border-top: 1px solid var(--color-grey-300);
}
}
}
}

main.ts

import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';

import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, {
...appConfig,
providers: [provideAnimations(), ...(appConfig.providers || [])]
}).catch((err) => console.error(err));

Filed Under: Angular

About Gabor Flamich

I'm a web developer and designer based in Budapest, Hungary. In recent years, I've documented hundreds of solutions I came across during development. This site is an archive for useful code snippets on WordPress, Genesis Framework and WooCommerce. If You have any questions related to WordPress development, get in touch!

Primary Sidebar

  • angular.io
© 2026 WP Flames - All Right Reserved