diff --git a/packages/react/src/components/Disclosure/__tests__/useDisclosure-test.js b/packages/react/src/components/Disclosure/__tests__/useDisclosure-test.js
new file mode 100644
index 000000000000..4625e68227f4
--- /dev/null
+++ b/packages/react/src/components/Disclosure/__tests__/useDisclosure-test.js
@@ -0,0 +1,147 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { cleanup, render, screen } from '@testing-library/react';
+import React from 'react';
+import userEvent, { specialChars } from '@testing-library/user-event';
+import { useDisclosure } from '../index.js';
+import '@testing-library/jest-dom';
+
+describe('useDisclosure', () => {
+ afterEach(cleanup);
+
+ // https://www.w3.org/TR/wai-aria-practices-1.1/#keyboard-interaction-8
+ it('should toggle visibility when the button is clicked', () => {
+ function TestComponent() {
+ const { buttonProps, contentProps, open } = useDisclosure('testid');
+ return (
+ <>
+
+
+ content
+
+ >
+ );
+ }
+
+ render();
+
+ const content = screen.getByText('content');
+ expect(content).not.toBeVisible();
+
+ const trigger = screen.getByText('trigger');
+
+ userEvent.tab();
+ expect(trigger).toHaveFocus();
+
+ userEvent.click(document.activeElement);
+ expect(content).toBeVisible();
+
+ userEvent.click(document.activeElement);
+ expect(content).not.toBeVisible();
+ });
+
+ it('should toggle visibility when the button is focused and Enter or Space is pressed', () => {
+ function TestComponent() {
+ const { buttonProps, contentProps, open } = useDisclosure('testid');
+ return (
+ <>
+
+
+ content
+
+ >
+ );
+ }
+
+ render();
+
+ const trigger = screen.getByText('trigger');
+
+ userEvent.type(trigger, `${specialChars.space}`);
+ expect(trigger).toHaveFocus();
+
+ userEvent.type(trigger, `${specialChars.enter}`);
+ expect(trigger).toHaveFocus();
+ });
+
+ // https://www.w3.org/TR/wai-aria-practices-1.1/#wai-aria-roles-states-and-properties-8
+ it('should set `aria-expanded` to match the visibility of the content', () => {
+ function TestComponent() {
+ const { buttonProps, contentProps, open } = useDisclosure('testid');
+ return (
+ <>
+
+
+ content
+
+ >
+ );
+ }
+
+ render();
+
+ const trigger = screen.getByRole('button');
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
+
+ userEvent.click(trigger);
+
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ it('should set `aria-controls` to match the id of the content', () => {
+ function TestComponent() {
+ const { buttonProps, contentProps, open } = useDisclosure('testid');
+ return (
+ <>
+
+
+ content
+
+ >
+ );
+ }
+
+ render();
+
+ const content = screen.getByText('content');
+ const trigger = screen.getByText('trigger');
+ const contentId = content.id;
+
+ expect(trigger).toHaveAttribute('aria-controls', contentId);
+ });
+
+ it('should set `id` on the content', () => {
+ function TestComponent() {
+ const { buttonProps, contentProps, open } = useDisclosure('testid');
+ return (
+ <>
+
+
+ content
+
+ >
+ );
+ }
+
+ render();
+
+ const content = screen.getByText('content');
+
+ expect(content).toHaveAttribute('id');
+ });
+});
diff --git a/packages/react/src/components/Disclosure/index.js b/packages/react/src/components/Disclosure/index.js
new file mode 100644
index 000000000000..ba163d23b5f5
--- /dev/null
+++ b/packages/react/src/components/Disclosure/index.js
@@ -0,0 +1,31 @@
+/**
+ * Copyright IBM Corp. 2016, 2018
+ *
+ * This source code is licensed under the Apache-2.0 license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import { useState } from 'react';
+
+function useDisclosure(id) {
+ const [open, setOpen] = useState(false);
+
+ const buttonProps = {
+ 'aria-controls': id,
+ 'aria-expanded': open,
+ onClick() {
+ setOpen(!open);
+ },
+ };
+ const contentProps = {
+ id,
+ };
+
+ return {
+ buttonProps,
+ contentProps,
+ open,
+ };
+}
+
+export { useDisclosure };