diff --git a/commitlint.config.cjs b/commitlint.config.cjs
index 6b2393ba..a3365520 100644
--- a/commitlint.config.cjs
+++ b/commitlint.config.cjs
@@ -11,6 +11,8 @@ module.exports = {
"deps",
"deps-dev",
// Components as scopes listed below
+ "accordion",
+ "accordion-group",
"button",
"icon",
"input",
diff --git a/src/baklava.ts b/src/baklava.ts
index d2fec698..1d9c0f41 100644
--- a/src/baklava.ts
+++ b/src/baklava.ts
@@ -1,3 +1,5 @@
+export { default as BlAccordionGroup } from "./components/accordion-group/bl-accordion-group";
+export { default as BlAccordion } from "./components/accordion-group/accordion/bl-accordion";
export { default as BlAlert } from "./components/alert/bl-alert";
export { default as BlBadge } from "./components/badge/bl-badge";
export { default as BlButton } from "./components/button/bl-button";
diff --git a/src/components/accordion-group/accordion/bl-accordion.css b/src/components/accordion-group/accordion/bl-accordion.css
new file mode 100644
index 00000000..e018f464
--- /dev/null
+++ b/src/components/accordion-group/accordion/bl-accordion.css
@@ -0,0 +1,81 @@
+:host {
+ display: block;
+}
+
+.accordion {
+ --border: 1px solid var(--bl-color-neutral-lighter);
+ --default-radius: var(--bl-size-2xs);
+ --radius-top-left: var(--bl-accordion-radius-top-left, var(--default-radius));
+ --radius-top-right: var(--bl-accordion-radius-top-right, var(--default-radius));
+ --radius-bottom-right: var(--bl-accordion-radius-bottom-right, var(--default-radius));
+ --radius-bottom-left: var(--bl-accordion-radius-bottom-left, var(--default-radius));
+
+ width: 100%;
+}
+
+.summary {
+ list-style: none;
+ user-select: none;
+ cursor: pointer;
+ font: var(--bl-font-title-3-medium);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--bl-size-2xs);
+ padding: var(--bl-size-m);
+ background: var(--bl-color-neutral-full);
+ color: var(--bl-color-neutral-darker);
+ border: var(--border);
+ border-bottom: var(--bl-accordion-border-bottom, var(--border));
+ border-radius: var(--radius-top-left) var(--radius-top-right) var(--radius-bottom-right)
+ var(--radius-bottom-left);
+ transition: background-color 200ms;
+}
+
+.summary::-webkit-details-marker {
+ display: none;
+}
+
+.summary:hover {
+ background: var(--bl-color-neutral-lightest);
+}
+
+.summary:focus-visible {
+ outline: 2px solid var(--bl-color-primary);
+ outline-offset: -1px;
+}
+
+.indicator {
+ transition: transform 200ms;
+}
+
+.accordion[open] .indicator {
+ transform: rotate(180deg);
+}
+
+.accordion[open] .summary {
+ border-bottom: 0;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.caption {
+ flex: 1;
+}
+
+.accordion-content {
+ padding: var(--bl-size-m);
+ background: var(--bl-color-neutral-full);
+ border: var(--border);
+ border-top: 0;
+ border-bottom: var(--bl-accordion-border-bottom, var(--border));
+ border-bottom-left-radius: var(--radius-bottom-left);
+ border-bottom-right-radius: var(--radius-bottom-right);
+ font: var(--bl-font-body-text-2-regular);
+}
+
+.disabled .summary {
+ cursor: not-allowed;
+ background: var(--bl-color-neutral-lightest);
+ color: var(--bl-color-neutral-light);
+}
diff --git a/src/components/accordion-group/accordion/bl-accordion.stories.mdx b/src/components/accordion-group/accordion/bl-accordion.stories.mdx
new file mode 100644
index 00000000..196c0e65
--- /dev/null
+++ b/src/components/accordion-group/accordion/bl-accordion.stories.mdx
@@ -0,0 +1,92 @@
+import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs";
+import { html } from "lit";
+import { ifDefined } from 'lit/directives/if-defined.js';
+import { unsafeHTML } from 'lit/directives/unsafe-html.js';
+
+
+
+export const AccordionTemplate = (args) => html`
+ ${unsafeHTML(args.content)}
+ `;
+
+# Accordion
+
+[ADR](https://github.com/Trendyol/baklava/issues/739)
+[Figma](https://www.figma.com/file/RrcLH0mWpIUy4vwuTlDeKN/Baklava-Design-Guide?type=design&node-id=15548%3A20910)
+
+Accordion is a component that allows the user to show and hide sections of related content on a page.
+
+### Usage
+
+
+* The accordion component includes a free content area.
+* The caption can be set either via **attribute** or **slot**.
+* An icon can be added to the beginning of the caption.
+* Accordion can be disabled.
+* The accordion group component combines accordions and only allows one accordion to be open by default
+
+
+
+
+## With Icon
+
+Add icon to beginning of caption with `icon` attribute.
+
+
+
+## Disabled
+
+Use the `disable` attribute to prevent the details from expanding.
+
+
+
+## Reference
+
+
+
+## Public Functions
+
+* `expand()`: Can be used to expand accordion.
+* `collapse()`: Can be used to collapse accordion.
+
+Example usage;
+
+```js
+document.querySelector('bl-accordion').expand();
+document.querySelector('bl-accordion').collapse();
+```
diff --git a/src/components/accordion-group/accordion/bl-accordion.test.ts b/src/components/accordion-group/accordion/bl-accordion.test.ts
new file mode 100644
index 00000000..da2f5cba
--- /dev/null
+++ b/src/components/accordion-group/accordion/bl-accordion.test.ts
@@ -0,0 +1,184 @@
+import { assert, fixture, html, expect, waitUntil, aTimeout } from "@open-wc/testing";
+import { spy } from "sinon";
+import BlAccordion from "./bl-accordion";
+
+describe("bl-accordion", () => {
+ it("is defined", () => {
+ const el = document.createElement("bl-accordion");
+
+ assert.instanceOf(el, BlAccordion);
+ });
+
+ it("renders with default values", async () => {
+ const el = await fixture(html`
+ `);
+
+ assert.shadowDom.equal(
+ el,
+ `
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+ );
+ });
+
+ it("should set icon", async () => {
+ const el = await fixture(html``);
+
+ await el.updateComplete;
+ const iconEl = el.shadowRoot!.querySelector(".icon")!;
+
+ expect(iconEl.getAttribute("name")).to.eq("eye_on");
+ });
+
+ it("should set info icon when icon is boolean", async () => {
+ const el = await fixture(html``);
+
+ const iconEl = el.shadowRoot!.querySelector(".icon")!;
+
+ expect(iconEl.getAttribute("name")).to.eq("info");
+ });
+
+ it("should set caption", async () => {
+ const captionText = "Best caption";
+ const el = await fixture(html``);
+ const captionEl = el.shadowRoot?.querySelector(".caption");
+
+ expect(captionEl).to.exist;
+ expect(captionEl!.innerText).to.eq(captionText);
+ });
+
+ it("should set caption via slot", async () => {
+ const captionText = "Best caption";
+ const el = await fixture(html`${captionText}
`);
+ const captionSlot = el.shadowRoot!.querySelector('slot[name="caption"]');
+ const captionSlotContent = captionSlot!.assignedNodes()[0] as HTMLDivElement;
+
+ expect(captionSlotContent.innerText).to.eq(captionText);
+ });
+
+ it("should be visible with the open attribute", async () => {
+ const el = await fixture(html`
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
+ dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
+ donec.
+ `);
+
+ const body = el.shadowRoot!.querySelector(".accordion-content")!;
+
+ expect(body.clientHeight).greaterThan(0);
+ });
+
+ it("should not be visible without the open attribute", async () => {
+ const el = await fixture(html`
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
+ dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
+ donec.
+ `);
+
+ const body = el.shadowRoot!.querySelector(".accordion-content")!;
+
+ expect(body.clientHeight).to.eq(0);
+ });
+
+ it("should emit bl-toggle when click on collapsed accordion", async () => {
+ const el = await fixture(html`
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
+ dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
+ donec.
+ `);
+
+ const toggleHandler = spy();
+
+ el.addEventListener("bl-toggle", (e) => toggleHandler((e as CustomEvent).detail));
+
+ const summary = el.shadowRoot!.querySelector("summary")!;
+
+ summary.click();
+
+ await waitUntil(() => toggleHandler.calledOnce);
+
+ expect(toggleHandler).to.have.been.calledWith(true);
+ });
+
+ it("should emit bl-toggle when click on expanded accordion", async () => {
+ const el = await fixture(html`
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
+ dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
+ donec.
+ `);
+
+ const toggleHandler = spy();
+
+ el.addEventListener("bl-toggle", (e) => toggleHandler((e as CustomEvent).detail));
+
+ const summary = el.shadowRoot!.querySelector("summary")!;
+
+ summary.click();
+
+ await waitUntil(() => toggleHandler.calledOnce);
+
+ expect(toggleHandler).to.have.been.calledWith(false);
+ });
+
+ it("should not open when disabled", async () => {
+ const el = await fixture(html`
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
+ dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
+ donec.
+ `);
+
+ const summary = el.shadowRoot!.querySelector("summary")!;
+
+ summary.click();
+
+ expect(el.open).to.eq(false);
+ });
+
+ it("should not change open attribute when disabled", async () => {
+ const el = await fixture(html`
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
+ dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
+ donec.
+ `);
+
+ expect(el.open).to.eq(false);
+ });
+
+ it("should not affect animation", async () => {
+ const el = await fixture(html`
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
+ dolore magna aliqua. Molestie at elementum eu facilisis. Morbi quis commodo odio aenean sed adipiscing diam
+ donec.
+ `);
+
+ const summary = el.shadowRoot!.querySelector("summary")!;
+
+ summary.click();
+ summary.click();
+ summary.click();
+
+ await aTimeout(200);
+ expect(el.open).to.eq(true);
+ });
+});
diff --git a/src/components/accordion-group/accordion/bl-accordion.ts b/src/components/accordion-group/accordion/bl-accordion.ts
new file mode 100644
index 00000000..0e700e37
--- /dev/null
+++ b/src/components/accordion-group/accordion/bl-accordion.ts
@@ -0,0 +1,165 @@
+import { html, LitElement, PropertyValues, TemplateResult } from "lit";
+import { customElement, property, query } from "lit/decorators.js";
+import { CSSResultGroup } from "lit/development";
+import { classMap } from "lit/directives/class-map.js";
+import { event, EventDispatcher } from "../../../utilities/event";
+import { stringBooleanConverter } from "../../../utilities/string-boolean.converter";
+import "../../icon/bl-icon";
+import { BaklavaIcon } from "../../icon/icon-list";
+import style from "./bl-accordion.css";
+
+enum AnimationStatus {
+ EXPANDING,
+ COLLAPSING,
+}
+
+@customElement("bl-accordion")
+export default class BlAccordion extends LitElement {
+ /**
+ * Whether the accordion is expanded
+ */
+ @property({ reflect: true, type: Boolean })
+ open = false;
+
+ /**
+ * Sets accordion caption.
+ */
+ @property({ reflect: true })
+ caption?: string;
+
+ /**
+ * Add icon to beginning of the title
+ */
+ @property({ converter: stringBooleanConverter() })
+ icon?: boolean | BaklavaIcon;
+
+ /**
+ * Whether the accordion is disabled
+ */
+ @property({ reflect: true, type: Boolean })
+ disabled = false;
+
+ /**
+ * Fires when accordion open state change.
+ */
+ @event("bl-toggle") private _onToggle: EventDispatcher;
+
+ @property({ type: Number })
+ animationDuration = 250;
+
+ private _animation: Animation | null;
+ private _animationStatus: AnimationStatus | null = null;
+
+ @query("details")
+ detailsEl: HTMLDetailsElement;
+
+ @query("summary")
+ summaryEl: HTMLElement;
+
+ @query(".accordion-content")
+ contentEl: HTMLElement;
+
+ static get styles(): CSSResultGroup {
+ return [style];
+ }
+
+ _animate(isExpanding: boolean) {
+ this._animationStatus = isExpanding ? AnimationStatus.EXPANDING : AnimationStatus.COLLAPSING;
+
+ const startHeight = `${this.detailsEl.offsetHeight}px`;
+ const endHeight = isExpanding
+ ? `${this.summaryEl.offsetHeight + this.contentEl.offsetHeight}px`
+ : `${this.summaryEl.offsetHeight}px`;
+
+ if (this._animation) {
+ this._animation.cancel();
+ }
+
+ this._animation = this.detailsEl.animate(
+ {
+ height: [startHeight, endHeight],
+ },
+ {
+ duration: this.animationDuration,
+ easing: "ease-out",
+ }
+ );
+
+ this._animation.onfinish = () => this._onAnimationFinish(isExpanding);
+ this._animation.oncancel = () => (this._animationStatus = null);
+ }
+
+ private _onAnimationFinish(open: boolean) {
+ this.open = open;
+ this._animation = null;
+ this._animationStatus = null;
+ this.detailsEl.style.height = this.detailsEl.style.overflow = "";
+ }
+
+ expand() {
+ this.detailsEl.style.overflow = "hidden";
+ this.detailsEl.style.height = `${this.detailsEl.offsetHeight}px`;
+ this.open = true;
+ this._animate(true);
+ }
+
+ collapse() {
+ this._animate(false);
+ }
+
+ private _clickHandler(e: Event) {
+ e.preventDefault();
+
+ if (this.disabled) return;
+
+ if (this._animationStatus === AnimationStatus.COLLAPSING || !this.open) {
+ this.expand();
+ } else if (this._animationStatus === AnimationStatus.EXPANDING || this.open) {
+ this.collapse();
+ }
+ }
+
+ protected updated(_changedProperties: PropertyValues) {
+ if (_changedProperties.has("open")) {
+ if (this.disabled && this.open) {
+ this._onAnimationFinish(false);
+ return;
+ }
+
+ this._onToggle(this.open);
+ }
+ }
+
+ render(): TemplateResult {
+ const icon = this.icon
+ ? html``
+ : null;
+
+ return html`
+
+ ${icon}
+
+ ${this.caption}
+
+
+
+
+
+
+
+ `;
+ }
+}
diff --git a/src/components/accordion-group/bl-accordion-group.css b/src/components/accordion-group/bl-accordion-group.css
new file mode 100644
index 00000000..5dc5926b
--- /dev/null
+++ b/src/components/accordion-group/bl-accordion-group.css
@@ -0,0 +1,24 @@
+.accordion-group {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+}
+
+.accordion-group ::slotted(bl-accordion:first-child) {
+ --bl-accordion-radius-bottom-right: 0;
+ --bl-accordion-radius-bottom-left: 0;
+ --bl-accordion-border-bottom: 0;
+}
+
+.accordion-group ::slotted(bl-accordion:not(:last-child, :first-child)) {
+ --bl-accordion-radius-bottom-right: 0;
+ --bl-accordion-radius-bottom-left: 0;
+ --bl-accordion-radius-top-left: 0;
+ --bl-accordion-radius-top-right: 0;
+ --bl-accordion-border-bottom: 0;
+}
+
+.accordion-group ::slotted(bl-accordion:last-child) {
+ --bl-accordion-radius-top-right: 0;
+ --bl-accordion-radius-top-left: 0;
+}
diff --git a/src/components/accordion-group/bl-accordion-group.stories.mdx b/src/components/accordion-group/bl-accordion-group.stories.mdx
new file mode 100644
index 00000000..a14f1d78
--- /dev/null
+++ b/src/components/accordion-group/bl-accordion-group.stories.mdx
@@ -0,0 +1,44 @@
+import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs";
+import { ifDefined } from "lit/directives/if-defined.js";
+import { html } from "lit";
+
+
+
+export const AccordionTemplate = (args) => html`
+ ${args.accordions.map((a) => html`
+ ${a.content}`)}
+ `;
+
+# Accordion Group
+
+[ADR](https://github.com/Trendyol/baklava/issues/739)
+[Figma](https://www.figma.com/file/RrcLH0mWpIUy4vwuTlDeKN/Baklava-Design-Guide?type=design&node-id=15548%3A20910)
+
+The Accordion Group component combines accordions and only allows one accordion to be open by default
+
+
+
+## Allow Multiple
+
+Allow multiple accordions to be open at once with `multiple` attribute.
+
+
+
+## Reference
+
+
diff --git a/src/components/accordion-group/bl-accordion-group.test.ts b/src/components/accordion-group/bl-accordion-group.test.ts
new file mode 100644
index 00000000..3bd970fe
--- /dev/null
+++ b/src/components/accordion-group/bl-accordion-group.test.ts
@@ -0,0 +1,43 @@
+import { assert, aTimeout, expect, fixture, html } from "@open-wc/testing";
+import BlAccordionGroup from "./bl-accordion-group";
+import "./accordion/bl-accordion";
+import BlAccordion from "./accordion/bl-accordion";
+
+describe("bl-accordion-group", () => {
+ it("is defined", () => {
+ const el = document.createElement("bl-accordion-group");
+
+ assert.instanceOf(el, BlAccordionGroup);
+ });
+
+ it("renders with default values", async () => {
+ const el = await fixture(html`
+ `);
+
+ assert.shadowDom.equal(el, "
");
+ });
+
+ it("should open only one accordion", async () => {
+ const el = await fixture(html`
+
+
+
+
+ `);
+
+ await el.updateComplete;
+
+ const accordions = el.querySelectorAll("bl-accordion")!;
+
+ for (const accordion of accordions) {
+ accordion.shadowRoot!.querySelector("summary")!.click();
+ await aTimeout(250);
+ }
+
+ await aTimeout(250);
+
+ const openAccordions = el.querySelectorAll("bl-accordion[open]");
+
+ expect(openAccordions.length).to.eq(1);
+ });
+});
diff --git a/src/components/accordion-group/bl-accordion-group.ts b/src/components/accordion-group/bl-accordion-group.ts
new file mode 100644
index 00000000..05739bb9
--- /dev/null
+++ b/src/components/accordion-group/bl-accordion-group.ts
@@ -0,0 +1,39 @@
+import { html, LitElement, TemplateResult } from "lit";
+import { customElement, property, queryAssignedElements } from "lit/decorators.js";
+import { CSSResultGroup } from "lit/development";
+import { BlAccordion } from "../../baklava";
+import style from "./bl-accordion-group.css";
+
+@customElement("bl-accordion-group")
+export default class BlAccordionGroup extends LitElement {
+ /**
+ * Allow multiple accordions to be open at once
+ */
+ @property({ reflect: true, type: Boolean })
+ multiple = false;
+
+ @queryAssignedElements({ selector: "bl-accordion" })
+ accordions: BlAccordion[];
+
+ static get styles(): CSSResultGroup {
+ return [style];
+ }
+
+ handleToggleAccordions(e: CustomEvent) {
+ const target = e.target as BlAccordion;
+
+ if (!this.multiple && e.detail) {
+ this.accordions.forEach(a => {
+ if (target !== a) {
+ a.collapse();
+ }
+ });
+ }
+ }
+
+ render(): TemplateResult {
+ return html`
+
+
`;
+ }
+}