diff --git a/packages/oui-angular/src/index.js b/packages/oui-angular/src/index.js
index 135bbb79..8759a051 100644
--- a/packages/oui-angular/src/index.js
+++ b/packages/oui-angular/src/index.js
@@ -1,6 +1,7 @@
import "@oui-angular/oui-button/src";
import "@oui-angular/oui-calendar/src";
import "@oui-angular/oui-checkbox/src";
+import "@oui-angular/oui-collapsible/src";
import "@oui-angular/oui-radio/src";
import "@oui-angular/oui-message/src";
import "@oui-angular/oui-spinner/src";
@@ -31,6 +32,7 @@ angular.module("oui", [
"oui.button",
"oui.calendar",
"oui.checkbox",
+ "oui.collapsible",
"oui.radio",
"oui.message",
"oui.spinner",
diff --git a/packages/oui-angular/src/index.spec.js b/packages/oui-angular/src/index.spec.js
index b85f44e0..2f41b274 100644
--- a/packages/oui-angular/src/index.spec.js
+++ b/packages/oui-angular/src/index.spec.js
@@ -3,6 +3,7 @@ import "@oui-angular/common/test-utils";
loadTests(require.context("../../oui-button/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-calendar/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-checkbox/src/", true, /.*((\.spec)|(index))$/));
+loadTests(require.context("../../oui-collapsible/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-message/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-radio/src/", true, /.*((\.spec)|(index))$/));
loadTests(require.context("../../oui-spinner/src/", true, /.*((\.spec)|(index))$/));
diff --git a/packages/oui-collapsible/README.md b/packages/oui-collapsible/README.md
new file mode 100644
index 00000000..32c2725b
--- /dev/null
+++ b/packages/oui-collapsible/README.md
@@ -0,0 +1,31 @@
+# Collapsible
+
+
+
+## Usage
+
+### Normal
+
+```html:preview
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis semper ligula nec fringilla tempor. In rhoncus ullamcorper feugiat. Phasellus vel ipsum vitae neque varius luctus. Proin id iaculis arcu. Fusce justo arcu, egestas vel nulla nec, dictum cursus lacus. Aenean elementum vel odio quis rutrum. In quis tellus in neque vulputate rhoncus vitae ut justo. Ut dignissim varius est in consequat. Donec nisi mauris, pellentesque condimentum congue in, blandit ut arcu. In et elit ipsum.
+
+```
+
+### Expanded
+
+```html:preview
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis semper ligula nec fringilla tempor. In rhoncus ullamcorper feugiat. Phasellus vel ipsum vitae neque varius luctus. Proin id iaculis arcu. Fusce justo arcu, egestas vel nulla nec, dictum cursus lacus. Aenean elementum vel odio quis rutrum. In quis tellus in neque vulputate rhoncus vitae ut justo. Ut dignissim varius est in consequat. Donec nisi mauris, pellentesque condimentum congue in, blandit ut arcu. In et elit ipsum.
+
+```
+
+## API
+
+### oui-collapsible
+
+| Attribute | Type | Binding | One-time binding | Values | Default | Description
+| ---- | ---- | ---- | ---- | ---- | ---- | ----
+| `title` | string | @ | | | | collapsible title
+| `aria-label` | string | @? | yes | | | accessibility label
+| `expanded` | boolean | | yes | | `false` | initial expanded state
diff --git a/packages/oui-collapsible/src/collapsible.component.js b/packages/oui-collapsible/src/collapsible.component.js
new file mode 100644
index 00000000..7daf76ac
--- /dev/null
+++ b/packages/oui-collapsible/src/collapsible.component.js
@@ -0,0 +1,14 @@
+import controller from "./collapsible.controller.js";
+import template from "./collapsible.html";
+
+export default {
+ template,
+ controller,
+ bindings: {
+ id: "@",
+ title: "@",
+ ariaLabel: "@?",
+ expanded: ""
+ },
+ transclude: true
+};
diff --git a/packages/oui-collapsible/src/collapsible.controller.js b/packages/oui-collapsible/src/collapsible.controller.js
new file mode 100644
index 00000000..8a4c973a
--- /dev/null
+++ b/packages/oui-collapsible/src/collapsible.controller.js
@@ -0,0 +1,40 @@
+import { addDefaultParameter } from "@oui-angular/common/component-utils";
+
+export default class {
+ constructor ($attrs, $element, $scope, $timeout, $window) {
+ "ngInject";
+ this.$attrs = $attrs;
+ this.$element = $element;
+ this.$scope = $scope;
+ this.$timeout = $timeout;
+ this.$window = $window;
+ }
+
+ $onInit () {
+ addDefaultParameter(this, "expanded", false);
+
+ // Check body height for transition animation
+ const body = this.$element[0].querySelector(".oui-collapsible__body");
+ this.$scope.$watch(() => body.offsetHeight, (newHeight, oldHeight) => {
+ if (newHeight !== oldHeight) {
+ this.wrapperHeight = `${newHeight}px`;
+ }
+ });
+ }
+
+ $postLink () {
+ this.$timeout(() =>
+ this.$element
+ .addClass("oui-collapsible")
+ .removeAttr("aria-label")
+ );
+
+ // Apply on resize for new body height
+ angular.element(this.$window)
+ .on("resize", () => this.$scope.$apply());
+ }
+
+ toggle () {
+ this.expanded = !this.expanded;
+ }
+}
diff --git a/packages/oui-collapsible/src/collapsible.html b/packages/oui-collapsible/src/collapsible.html
new file mode 100644
index 00000000..9e911b9f
--- /dev/null
+++ b/packages/oui-collapsible/src/collapsible.html
@@ -0,0 +1,11 @@
+
+
diff --git a/packages/oui-collapsible/src/index.js b/packages/oui-collapsible/src/index.js
new file mode 100644
index 00000000..447637a1
--- /dev/null
+++ b/packages/oui-collapsible/src/index.js
@@ -0,0 +1,4 @@
+import Collapsible from "./collapsible.component.js";
+
+angular.module("oui.collapsible", [])
+ .component("ouiCollapsible", Collapsible);
diff --git a/packages/oui-collapsible/src/index.spec.js b/packages/oui-collapsible/src/index.spec.js
new file mode 100644
index 00000000..fa48f5b9
--- /dev/null
+++ b/packages/oui-collapsible/src/index.spec.js
@@ -0,0 +1,79 @@
+describe("ouiCollapsible", () => {
+ let TestUtils;
+
+ beforeEach(angular.mock.module("oui.collapsible"));
+ beforeEach(angular.mock.module("oui.test-utils"));
+
+ beforeEach(inject((_TestUtils_) => {
+ TestUtils = _TestUtils_;
+ }));
+
+ function getHeaderElement (element) {
+ return element[0].querySelector(".oui-collapsible__header");
+ }
+
+ function getBodyElement (element) {
+ return element[0].querySelector(".oui-collapsible__body");
+ }
+
+ describe("Component", () => {
+ it("should have the correct title", () => {
+ const titleText = "Collapsible title";
+ const element = TestUtils.compileTemplate(`
+ `
+ );
+
+ const headerEl = getHeaderElement(element);
+ expect(headerEl.innerText).toContain(titleText);
+ });
+
+ it("should have the correct aria-label", () => {
+ const ariaLabel = "Action";
+ const element = TestUtils.compileTemplate(`
+ `
+ );
+
+ const headerEl = getHeaderElement(element);
+ expect(headerEl.getAttribute("aria-label")).toBe(ariaLabel);
+ });
+
+
+ it("should expand and collapse on header click", () => {
+ const element = TestUtils.compileTemplate(`
+ `
+ );
+
+ const headerEl = angular.element(getHeaderElement(element));
+
+ // Expand
+ headerEl.triggerHandler("click");
+ expect(headerEl.attr("aria-expanded")).toBe("true");
+
+ // Collapse
+ headerEl.triggerHandler("click");
+ expect(headerEl.attr("aria-expanded")).toBe("false");
+ });
+
+ it("should be expanded", () => {
+ const element = TestUtils.compileTemplate(`
+ `
+ );
+ const headerEl = angular.element(getHeaderElement(element));
+ expect(headerEl.attr("aria-expanded")).toBe("true");
+ });
+
+ it("should transclude the contents into the collapsible body", () => {
+ const element = TestUtils.compileTemplate(`
+
+ Collapsible body
+ `
+ );
+
+ const bodyEl = getBodyElement(element);
+ expect(bodyEl).toBeTruthy();
+
+ const contentEl = bodyEl.querySelector(".custom-content");
+ expect(contentEl).toBeTruthy();
+ });
+ });
+});