useDisclosure
Provides the behavior and accessibility implementation for a disclosure component.
install | yarn add react-aria |
---|---|
version | 3.35.2 |
usage | import {useDisclosure} from 'react-aria' |
API#
useDisclosure(
props: AriaDisclosureProps,
state: DisclosureState,
ref: RefObject<Element
| | 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 {useDisclosureState} from '@react-stately/disclosure';
import {useDisclosure} from '@react-aria/disclosure';
import {useButton} from '@react-aria/button';
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 {useDisclosureState} from '@react-stately/disclosure';
import {useDisclosure} from '@react-aria/disclosure';
import {useButton} from '@react-aria/button';
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 {useDisclosureState} from '@react-stately/disclosure';
import {useDisclosure} from '@react-aria/disclosure';
import {useButton} from '@react-aria/button';
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 {useDisclosureGroupState} from '@react-stately/disclosure';
import {useId} from '@react-aria/utils';
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 {useDisclosureGroupState} from '@react-stately/disclosure';
import {useId} from '@react-aria/utils';
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 {useDisclosureGroupState} from '@react-stately/disclosure';
import {useId} from '@react-aria/utils';
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>