useDisclosure
Provides the behavior and accessibility implementation for a disclosure component.
| install | yarn add react-aria |
|---|---|
| version | 3.44.0 |
| usage | import {useDisclosure} from 'react-aria' |
API#
useDisclosure(
props: AriaDisclosureProps,
state: DisclosureState,
ref: RefObject<HTMLElement
| | null>
): DisclosureAria
Features#
A disclosure is a collapsible section of content. It is composed of a trigger button and a panel that contains the content. useDisclosure can be used to implement these in an accessible way.
- Support for mouse, touch, and keyboard interactions to open and close the disclosure
- Support for disabled disclosures
- Follows the disclosure ARIA pattern, semantically linking the trigger button and panel
- Uses hidden="until-found" in supported browsers, enabling find-in-page search support and improved search engine visibility for collapsed content
Anatomy#
A disclosure consists of a trigger button and a panel. Clicking on or pressing Enter or Space while the trigger button is focused toggles the visibility of the panel.
useDisclosure returns props to spread onto the trigger button and panel.
| Name | Type | Description |
buttonProps | AriaButtonProps | Props for the disclosure button. |
panelProps | HTMLAttributes<HTMLElement> | Props for the disclosure panel. |
State is managed by the useDisclosureState
hook in @react-stately/disclosure. The state object should be passed as an option to useDisclosure.
Example#
This example displays a basic disclosure with a button that toggles the visibility of the panel.
import {useButton, useDisclosure} from 'react-aria';
import {useDisclosureState} from 'react-stately';
import {mergeProps, useFocusRing} from 'react-aria';
function Disclosure(props) {
let state = useDisclosureState(props);
let panelRef = React.useRef<HTMLDivElement | null>(null);
let triggerRef = React.useRef<HTMLButtonElement | null>(null);
let { buttonProps: triggerProps, panelProps } = useDisclosure(
props,
state,
panelRef
);
let { buttonProps } = useButton(triggerProps, triggerRef);
let { isFocusVisible, focusProps } = useFocusRing();
return (
<div className="disclosure">
<h3>
<button
className="trigger"
ref={triggerRef}
{...mergeProps(buttonProps, focusProps)}
style={{ outline: isFocusVisible ? '2px solid dodgerblue' : 'none' }}
>
<svg viewBox="0 0 24 24">
<path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
{props.title}
</button>
</h3>
<div className="panel" ref={panelRef} {...panelProps}>
<p>
{props.children}
</p>
</div>
</div>
);
}
<Disclosure title="System Requirements">
Details about system requirements here.
</Disclosure>
import {useButton, useDisclosure} from 'react-aria';
import {useDisclosureState} from 'react-stately';
import {mergeProps, useFocusRing} from 'react-aria';
function Disclosure(props) {
let state = useDisclosureState(props);
let panelRef = React.useRef<HTMLDivElement | null>(null);
let triggerRef = React.useRef<HTMLButtonElement | null>(
null
);
let { buttonProps: triggerProps, panelProps } =
useDisclosure(props, state, panelRef);
let { buttonProps } = useButton(triggerProps, triggerRef);
let { isFocusVisible, focusProps } = useFocusRing();
return (
<div className="disclosure">
<h3>
<button
className="trigger"
ref={triggerRef}
{...mergeProps(buttonProps, focusProps)}
style={{
outline: isFocusVisible
? '2px solid dodgerblue'
: 'none'
}}
>
<svg viewBox="0 0 24 24">
<path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
{props.title}
</button>
</h3>
<div className="panel" ref={panelRef} {...panelProps}>
<p>
{props.children}
</p>
</div>
</div>
);
}
<Disclosure title="System Requirements">
Details about system requirements here.
</Disclosure>
import {
useButton,
useDisclosure
} from 'react-aria';
import {useDisclosureState} from 'react-stately';
import {
mergeProps,
useFocusRing
} from 'react-aria';
function Disclosure(
props
) {
let state =
useDisclosureState(
props
);
let panelRef = React
.useRef<
| HTMLDivElement
| null
>(null);
let triggerRef = React
.useRef<
| HTMLButtonElement
| null
>(null);
let {
buttonProps:
triggerProps,
panelProps
} = useDisclosure(
props,
state,
panelRef
);
let { buttonProps } =
useButton(
triggerProps,
triggerRef
);
let {
isFocusVisible,
focusProps
} = useFocusRing();
return (
<div className="disclosure">
<h3>
<button
className="trigger"
ref={triggerRef}
{...mergeProps(
buttonProps,
focusProps
)}
style={{
outline:
isFocusVisible
? '2px solid dodgerblue'
: 'none'
}}
>
<svg viewBox="0 0 24 24">
<path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
{props.title}
</button>
</h3>
<div
className="panel"
ref={panelRef}
{...panelProps}
>
<p>
{props
.children}
</p>
</div>
</div>
);
}
<Disclosure title="System Requirements">
Details about system
requirements here.
</Disclosure>
Show CSS
@import "@react-aria/example-theme";
.disclosure {
.trigger {
background: none;
border: none;
box-shadow: none;
font-weight: bold;
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-color);
svg {
rotate: 0deg;
transition: rotate 200ms;
width: 12px;
height: 12px;
fill: none;
stroke: currentColor;
stroke-width: 3px;
}
&[aria-expanded="true"] svg {
rotate: 90deg;
}
&:disabled {
color: var(--gray-300);
}
}
}
.panel {
margin-left: 32px;
}@import "@react-aria/example-theme";
.disclosure {
.trigger {
background: none;
border: none;
box-shadow: none;
font-weight: bold;
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-color);
svg {
rotate: 0deg;
transition: rotate 200ms;
width: 12px;
height: 12px;
fill: none;
stroke: currentColor;
stroke-width: 3px;
}
&[aria-expanded="true"] svg {
rotate: 90deg;
}
&:disabled {
color: var(--gray-300);
}
}
}
.panel {
margin-left: 32px;
}@import "@react-aria/example-theme";
.disclosure {
.trigger {
background: none;
border: none;
box-shadow: none;
font-weight: bold;
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-color);
svg {
rotate: 0deg;
transition: rotate 200ms;
width: 12px;
height: 12px;
fill: none;
stroke: currentColor;
stroke-width: 3px;
}
&[aria-expanded="true"] svg {
rotate: 90deg;
}
&:disabled {
color: var(--gray-300);
}
}
}
.panel {
margin-left: 32px;
}Usage#
The following examples show how to use the Disclosure component created in the above example.
Default expansion#
Whether or not the disclosure is expanded or not by default can be set with the defaultExpanded prop.
<Disclosure title="System Requirements" defaultExpanded>
Details about system requirements here.
</Disclosure>
<Disclosure title="System Requirements" defaultExpanded>
Details about system requirements here.
</Disclosure>
<Disclosure
title="System Requirements"
defaultExpanded
>
Details about system
requirements here.
</Disclosure>
Controlled expansion#
Expansion can be controlled using the isExpanded prop, paired with the onExpandedChange event. The onExpandedChange event is fired when the user presses the trigger button.
function ControlledDisclosure(props) {
let [isExpanded, setExpanded] = React.useState(false);
return (
<Disclosure
title="System Requirements"
isExpanded={isExpanded}
onExpandedChange={setExpanded}
>
Details about system requirements here.
</Disclosure>
);
}
function ControlledDisclosure(props) {
let [isExpanded, setExpanded] = React.useState(false);
return (
<Disclosure
title="System Requirements"
isExpanded={isExpanded}
onExpandedChange={setExpanded}
>
Details about system requirements here.
</Disclosure>
);
}
function ControlledDisclosure(
props
) {
let [
isExpanded,
setExpanded
] = React.useState(
false
);
return (
<Disclosure
title="System Requirements"
isExpanded={isExpanded}
onExpandedChange={setExpanded}
>
Details about
system requirements
here.
</Disclosure>
);
}
Disabled#
A disclosure can be disabled with the isDisabled prop. This will disable the trigger button and prevent the panel from being opened or closed.
<Disclosure title="System Requirements" isDisabled>
Details about system requirements here.
</Disclosure>
<Disclosure title="System Requirements" isDisabled>
Details about system requirements here.
</Disclosure>
<Disclosure
title="System Requirements"
isDisabled
>
Details about system
requirements here.
</Disclosure>
Disclosure Group#
A disclosure group (i.e. accordion) is a set of disclosures where only one disclosure can be expanded at a time. The following example shows how to create a DisclosureGroup component with the useDisclosureGroupState hook. We'll also create a DisclosureItem component that uses the DisclosureGroupState context for managing its state.
import {useId} from 'react-aria';
import {useDisclosureGroupState} from 'react-stately';
const DisclosureGroupStateContext = React.createContext(null);
function DisclosureGroup(props) {
let state = useDisclosureGroupState(props);
return (
<div className="group">
<DisclosureGroupStateContext.Provider value={state}>
{props.children}
</DisclosureGroupStateContext.Provider>
</div>
);
}
function DisclosureItem(props) {
let defaultId = useId();
let id = props.id || defaultId;
let groupState = React.useContext(DisclosureGroupStateContext);
let isExpanded = groupState
? groupState.expandedKeys.has(id)
: props.isExpanded;
let state = useDisclosureState({
...props,
isExpanded,
onExpandedChange(isExpanded) {
if (groupState) {
groupState.toggleKey(id);
}
props.onExpandedChange?.(isExpanded);
}
});
let panelRef = React.useRef<HTMLDivElement | null>(null);
let triggerRef = React.useRef<HTMLButtonElement | null>(null);
let isDisabled = props.isDisabled || groupState?.isDisabled || false;
let { buttonProps: triggerProps, panelProps } = useDisclosure(
{
...props,
isExpanded,
isDisabled
},
state,
panelRef
);
let { buttonProps } = useButton(triggerProps, triggerRef);
let { isFocusVisible, focusProps } = useFocusRing();
return (
<div className="disclosure">
<h3>
<button
className="trigger"
ref={triggerRef}
{...mergeProps(buttonProps, focusProps)}
style={{ outline: isFocusVisible ? '2px solid dodgerblue' : 'none' }}
>
<svg viewBox="0 0 24 24">
<path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
{props.title}
</button>
</h3>
<div className="panel" ref={panelRef} {...panelProps}>
<p>
{props.children}
</p>
</div>
</div>
);
}
import {useId} from 'react-aria';
import {useDisclosureGroupState} from 'react-stately';
const DisclosureGroupStateContext = React.createContext(
null
);
function DisclosureGroup(props) {
let state = useDisclosureGroupState(props);
return (
<div className="group">
<DisclosureGroupStateContext.Provider value={state}>
{props.children}
</DisclosureGroupStateContext.Provider>
</div>
);
}
function DisclosureItem(props) {
let defaultId = useId();
let id = props.id || defaultId;
let groupState = React.useContext(
DisclosureGroupStateContext
);
let isExpanded = groupState
? groupState.expandedKeys.has(id)
: props.isExpanded;
let state = useDisclosureState({
...props,
isExpanded,
onExpandedChange(isExpanded) {
if (groupState) {
groupState.toggleKey(id);
}
props.onExpandedChange?.(isExpanded);
}
});
let panelRef = React.useRef<HTMLDivElement | null>(null);
let triggerRef = React.useRef<HTMLButtonElement | null>(
null
);
let isDisabled = props.isDisabled ||
groupState?.isDisabled || false;
let { buttonProps: triggerProps, panelProps } =
useDisclosure(
{
...props,
isExpanded,
isDisabled
},
state,
panelRef
);
let { buttonProps } = useButton(triggerProps, triggerRef);
let { isFocusVisible, focusProps } = useFocusRing();
return (
<div className="disclosure">
<h3>
<button
className="trigger"
ref={triggerRef}
{...mergeProps(buttonProps, focusProps)}
style={{
outline: isFocusVisible
? '2px solid dodgerblue'
: 'none'
}}
>
<svg viewBox="0 0 24 24">
<path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
{props.title}
</button>
</h3>
<div className="panel" ref={panelRef} {...panelProps}>
<p>
{props.children}
</p>
</div>
</div>
);
}
import {useId} from 'react-aria';
import {useDisclosureGroupState} from 'react-stately';
const DisclosureGroupStateContext =
React.createContext(
null
);
function DisclosureGroup(
props
) {
let state =
useDisclosureGroupState(
props
);
return (
<div className="group">
<DisclosureGroupStateContext.Provider
value={state}
>
{props.children}
</DisclosureGroupStateContext.Provider>
</div>
);
}
function DisclosureItem(
props
) {
let defaultId =
useId();
let id = props.id ||
defaultId;
let groupState = React
.useContext(
DisclosureGroupStateContext
);
let isExpanded =
groupState
? groupState
.expandedKeys
.has(id)
: props.isExpanded;
let state =
useDisclosureState({
...props,
isExpanded,
onExpandedChange(
isExpanded
) {
if (groupState) {
groupState
.toggleKey(
id
);
}
props
.onExpandedChange?.(
isExpanded
);
}
});
let panelRef = React
.useRef<
| HTMLDivElement
| null
>(null);
let triggerRef = React
.useRef<
| HTMLButtonElement
| null
>(null);
let isDisabled =
props.isDisabled ||
groupState
?.isDisabled ||
false;
let {
buttonProps:
triggerProps,
panelProps
} = useDisclosure(
{
...props,
isExpanded,
isDisabled
},
state,
panelRef
);
let { buttonProps } =
useButton(
triggerProps,
triggerRef
);
let {
isFocusVisible,
focusProps
} = useFocusRing();
return (
<div className="disclosure">
<h3>
<button
className="trigger"
ref={triggerRef}
{...mergeProps(
buttonProps,
focusProps
)}
style={{
outline:
isFocusVisible
? '2px solid dodgerblue'
: 'none'
}}
>
<svg viewBox="0 0 24 24">
<path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
{props.title}
</button>
</h3>
<div
className="panel"
ref={panelRef}
{...panelProps}
>
<p>
{props
.children}
</p>
</div>
</div>
);
}
Usage#
The following examples show how to use the DisclosureGroup component created in the above example.
<DisclosureGroup>
<DisclosureItem title="Personal Information">
Personal information form here.
</DisclosureItem>
<DisclosureItem title="Billing Address">
Billing address form here.
</DisclosureItem>
</DisclosureGroup>
<DisclosureGroup>
<DisclosureItem title="Personal Information">
Personal information form here.
</DisclosureItem>
<DisclosureItem title="Billing Address">
Billing address form here.
</DisclosureItem>
</DisclosureGroup>
<DisclosureGroup>
<DisclosureItem title="Personal Information">
Personal
information form
here.
</DisclosureItem>
<DisclosureItem title="Billing Address">
Billing address
form here.
</DisclosureItem>
</DisclosureGroup>
Default expansion#
Which disclosure is expanded by default can be set with the defaultExpandedKeys prop.
<DisclosureGroup defaultExpandedKeys={['billing']}>
<DisclosureItem id="personal" title="Personal Information">
Personal information form here.
</DisclosureItem>
<DisclosureItem id="billing" title="Billing Address">
Billing address form here.
</DisclosureItem>
</DisclosureGroup>
<DisclosureGroup defaultExpandedKeys={['billing']}>
<DisclosureItem
id="personal"
title="Personal Information"
>
Personal information form here.
</DisclosureItem>
<DisclosureItem id="billing" title="Billing Address">
Billing address form here.
</DisclosureItem>
</DisclosureGroup>
<DisclosureGroup
defaultExpandedKeys={[
'billing'
]}
>
<DisclosureItem
id="personal"
title="Personal Information"
>
Personal
information form
here.
</DisclosureItem>
<DisclosureItem
id="billing"
title="Billing Address"
>
Billing address
form here.
</DisclosureItem>
</DisclosureGroup>
Controlled expansion#
Expansion can be controlled using the expandedKeys prop, paired with the onExpandedChange event. The onExpandedChange event is fired when one of the disclosures is expanded or collapsed.
function ControlledDisclosureGroup(props) {
let [expandedKeys, setExpandedKeys] = React.useState(['personal']);
return (
<DisclosureGroup
expandedKeys={expandedKeys}
onExpandedChange={setExpandedKeys}
>
<DisclosureItem id="personal" title="Personal Information">
Personal information form here.
</DisclosureItem>
<DisclosureItem id="billing" title="Billing Address">
Billing address form here.
</DisclosureItem>
</DisclosureGroup>
);
}
function ControlledDisclosureGroup(props) {
let [expandedKeys, setExpandedKeys] = React.useState([
'personal'
]);
return (
<DisclosureGroup
expandedKeys={expandedKeys}
onExpandedChange={setExpandedKeys}
>
<DisclosureItem
id="personal"
title="Personal Information"
>
Personal information form here.
</DisclosureItem>
<DisclosureItem id="billing" title="Billing Address">
Billing address form here.
</DisclosureItem>
</DisclosureGroup>
);
}
function ControlledDisclosureGroup(
props
) {
let [
expandedKeys,
setExpandedKeys
] = React.useState([
'personal'
]);
return (
<DisclosureGroup
expandedKeys={expandedKeys}
onExpandedChange={setExpandedKeys}
>
<DisclosureItem
id="personal"
title="Personal Information"
>
Personal
information form
here.
</DisclosureItem>
<DisclosureItem
id="billing"
title="Billing Address"
>
Billing address
form here.
</DisclosureItem>
</DisclosureGroup>
);
}
Multiple expanded#
Multiple disclosures can be expanded at the same time by setting the allowsMultipleExpanded prop to true.
<DisclosureGroup allowsMultipleExpanded>
<DisclosureItem title="Personal Information">
Personal information form here.
</DisclosureItem>
<DisclosureItem title="Billing Address">
Billing address form here.
</DisclosureItem>
</DisclosureGroup>
<DisclosureGroup allowsMultipleExpanded>
<DisclosureItem title="Personal Information">
Personal information form here.
</DisclosureItem>
<DisclosureItem title="Billing Address">
Billing address form here.
</DisclosureItem>
</DisclosureGroup>
<DisclosureGroup
allowsMultipleExpanded
>
<DisclosureItem title="Personal Information">
Personal
information form
here.
</DisclosureItem>
<DisclosureItem title="Billing Address">
Billing address
form here.
</DisclosureItem>
</DisclosureGroup>
Disabled#
An entire disclosure group can be disabled with the isDisabled prop. This will disable all trigger buttons and prevent the panels from being opened or closed.
<DisclosureGroup isDisabled>
<DisclosureItem title="Personal Information">
Personal information form here.
</DisclosureItem>
<DisclosureItem title="Billing Address">
Billing address form here.
</DisclosureItem>
</DisclosureGroup>
<DisclosureGroup isDisabled>
<DisclosureItem title="Personal Information">
Personal information form here.
</DisclosureItem>
<DisclosureItem title="Billing Address">
Billing address form here.
</DisclosureItem>
</DisclosureGroup>
<DisclosureGroup
isDisabled
>
<DisclosureItem title="Personal Information">
Personal
information form
here.
</DisclosureItem>
<DisclosureItem title="Billing Address">
Billing address
form here.
</DisclosureItem>
</DisclosureGroup>