diff --git a/libs/components/src/disclosure/disclosure-trigger-for.directive.ts b/libs/components/src/disclosure/disclosure-trigger-for.directive.ts
new file mode 100644
index 0000000000..0547028172
--- /dev/null
+++ b/libs/components/src/disclosure/disclosure-trigger-for.directive.ts
@@ -0,0 +1,27 @@
+import { Directive, HostBinding, HostListener, Input } from "@angular/core";
+
+import { DisclosureComponent } from "./disclosure.component";
+
+@Directive({
+ selector: "[bitDisclosureTriggerFor]",
+ exportAs: "disclosureTriggerFor",
+ standalone: true,
+})
+export class DisclosureTriggerForDirective {
+ /**
+ * Accepts template reference for a bit-disclosure component instance
+ */
+ @Input("bitDisclosureTriggerFor") disclosure: DisclosureComponent;
+
+ @HostBinding("attr.aria-expanded") get ariaExpanded() {
+ return this.disclosure.open;
+ }
+
+ @HostBinding("attr.aria-controls") get ariaControls() {
+ return this.disclosure.id;
+ }
+
+ @HostListener("click") click() {
+ this.disclosure.open = !this.disclosure.open;
+ }
+}
diff --git a/libs/components/src/disclosure/disclosure.component.ts b/libs/components/src/disclosure/disclosure.component.ts
new file mode 100644
index 0000000000..58c67ad0f0
--- /dev/null
+++ b/libs/components/src/disclosure/disclosure.component.ts
@@ -0,0 +1,21 @@
+import { Component, HostBinding, Input, booleanAttribute } from "@angular/core";
+
+let nextId = 0;
+
+@Component({
+ selector: "bit-disclosure",
+ standalone: true,
+ template: ``,
+})
+export class DisclosureComponent {
+ /**
+ * Optionally init the disclosure in its opened state
+ */
+ @Input({ transform: booleanAttribute }) open?: boolean = false;
+
+ @HostBinding("class") get classList() {
+ return this.open ? "" : "tw-hidden";
+ }
+
+ @HostBinding("id") id = `bit-disclosure-${nextId++}`;
+}
diff --git a/libs/components/src/disclosure/disclosure.mdx b/libs/components/src/disclosure/disclosure.mdx
new file mode 100644
index 0000000000..8df8e7025b
--- /dev/null
+++ b/libs/components/src/disclosure/disclosure.mdx
@@ -0,0 +1,55 @@
+import { Meta, Story, Primary, Controls } from "@storybook/addon-docs";
+
+import * as stories from "./disclosure.stories";
+
+
+
+```ts
+import { DisclosureComponent, DisclosureTriggerForDirective } from "@bitwarden/components";
+```
+
+# Disclosure
+
+The `bit-disclosure` component is used in tandem with the `bitDisclosureTriggerFor` directive to
+create an accessible content area whose visibility is controlled by a trigger button.
+
+To compose a disclosure and trigger:
+
+1. Create a trigger component (see "Supported Trigger Components" section below)
+2. Create a `bit-disclosure`
+3. Set a template reference on the `bit-disclosure`
+4. Use the `bitDisclosureTriggerFor` directive on the trigger component, and pass it the
+ `bit-disclosure` template reference
+5. Set the `open` property on the `bit-disclosure` to init the disclosure as either currently
+ expanded or currently collapsed. The disclosure will default to `false`, meaning it defaults to
+ being hidden.
+
+```
+
+click button to hide this content
+```
+
+
+
+
+
+
+## Supported Trigger Components
+
+This is the list of currently supported trigger components:
+
+- Icon button `muted` variant
+
+## Accessibility
+
+The disclosure and trigger directive functionality follow the
+[Disclosure (Show/Hide)](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) pattern for
+accessibility, automatically handling the `aria-controls` and `aria-expanded` properties. A `button`
+element must be used as the trigger for the disclosure. The `button` element must also have an
+accessible label/title -- please follow the accessibility guidelines for whatever trigger component
+you choose.
diff --git a/libs/components/src/disclosure/disclosure.stories.ts b/libs/components/src/disclosure/disclosure.stories.ts
new file mode 100644
index 0000000000..974589a667
--- /dev/null
+++ b/libs/components/src/disclosure/disclosure.stories.ts
@@ -0,0 +1,29 @@
+import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
+
+import { IconButtonModule } from "../icon-button";
+
+import { DisclosureTriggerForDirective } from "./disclosure-trigger-for.directive";
+import { DisclosureComponent } from "./disclosure.component";
+
+export default {
+ title: "Component Library/Disclosure",
+ component: DisclosureComponent,
+ decorators: [
+ moduleMetadata({
+ imports: [DisclosureTriggerForDirective, DisclosureComponent, IconButtonModule],
+ }),
+ ],
+} as Meta;
+
+type Story = StoryObj;
+
+export const DisclosureWithIconButton: Story = {
+ render: (args) => ({
+ props: args,
+ template: /*html*/ `
+
+ click button to hide this content
+ `,
+ }),
+};
diff --git a/libs/components/src/disclosure/index.ts b/libs/components/src/disclosure/index.ts
new file mode 100644
index 0000000000..b5bdf68725
--- /dev/null
+++ b/libs/components/src/disclosure/index.ts
@@ -0,0 +1,2 @@
+export * from "./disclosure-trigger-for.directive";
+export * from "./disclosure.component";
diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts
index 54f6dfda96..d036e1c77c 100644
--- a/libs/components/src/icon-button/icon-button.component.ts
+++ b/libs/components/src/icon-button/icon-button.component.ts
@@ -52,10 +52,14 @@ const styles: Record = {
"tw-bg-transparent",
"!tw-text-muted",
"tw-border-transparent",
+ "aria-expanded:tw-bg-text-muted",
+ "aria-expanded:!tw-text-contrast",
"hover:tw-bg-transparent-hover",
"hover:tw-border-primary-700",
"focus-visible:before:tw-ring-primary-700",
"disabled:tw-opacity-60",
+ "aria-expanded:hover:tw-bg-secondary-700",
+ "aria-expanded:hover:tw-border-secondary-700",
"disabled:hover:tw-border-transparent",
"disabled:hover:tw-bg-transparent",
...focusRing,
diff --git a/libs/components/src/icon-button/icon-button.mdx b/libs/components/src/icon-button/icon-button.mdx
index 8361d4c399..a45160d788 100644
--- a/libs/components/src/icon-button/icon-button.mdx
+++ b/libs/components/src/icon-button/icon-button.mdx
@@ -29,8 +29,6 @@ Icon buttons can be found in other components such as: the
[dialog](?path=/docs/component-library-dialogs--docs), and
[table](?path=/docs/component-library-table--docs).
-
-
## Styles
There are 4 common styles for button main, muted, contrast, and danger. The other styles follow the
@@ -40,48 +38,48 @@ button component styles.
Used for general icon buttons appearing on the theme’s main `background`
-
+
### Muted
Used for low emphasis icon buttons appearing on the theme’s main `background`
-
+
### Contrast
Used on a theme’s colored or contrasting backgrounds such as in the navigation or on toasts and
banners.
-
+
### Danger
Danger is used for “trash” actions throughout the experience, most commonly in the bottom right of
the dialog component.
-
+
### Primary
Used in place of the main button component if no text is used. This allows the button to display
square.
-
+
### Secondary
Used in place of the main button component if no text is used. This allows the button to display
square.
-
+
### Light
Used on a background that is dark in both light theme and dark theme. Example: end user navigation
styles.
-
+
**Note:** Main and contrast styles appear on backgrounds where using `primary-700` as a focus
indicator does not meet WCAG graphic contrast guidelines.
@@ -95,11 +93,11 @@ with less padding around the icon, such as in the navigation component.
### Small
-
+
### Default
-
+
## Accessibility
diff --git a/libs/components/src/icon-button/icon-button.stories.ts b/libs/components/src/icon-button/icon-button.stories.ts
index 0f25d2de58..b5542f7860 100644
--- a/libs/components/src/icon-button/icon-button.stories.ts
+++ b/libs/components/src/icon-button/icon-button.stories.ts
@@ -23,7 +23,7 @@ type Story = StoryObj;
export const Default: Story = {
render: (args) => ({
props: args,
- template: `
+ template: /*html*/ `