alpha

useDisclosureGroupState

Manages state for a group of disclosures, e.g. an accordion. It supports both single and multiple expanded items.

installyarn add @react-stately/disclosure
version3.0.0-alpha.0
usageimport {useDisclosureGroupState} from '@react-stately/disclosure'

API#


useDisclosureGroupState( (props: DisclosureGroupProps )): DisclosureGroupState

Interface#


Properties

NameTypeDescription
allowsMultipleExpandedbooleanWhether multiple items can be expanded at the same time.
isDisabledbooleanWhether all items are disabled.
expandedKeysSet<Key>A set of keys for items that are expanded.

Methods

MethodDescription
toggleKey( (key: Key )): voidToggles the expanded state for an item by its key.
setExpandedKeys( (keys: Set<Key> )): voidReplaces the set of expanded keys.

Example#


The following example shows how to create a DisclosureGroup component with the useDisclosureGroupState hook. We'll also create a Disclosure component that uses the DisclosureGroupState context for managing its state.

import {useButton} from '@react-aria/button';
import {useDisclosure} from '@react-aria/disclosure';
import {useDisclosureGroupState, useDisclosureState} 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 Disclosure(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);

  return (
    <div className="disclosure">
      <h3>
        <button className="trigger" ref={triggerRef} {...buttonProps}>
          <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 {useButton} from '@react-aria/button';
import {useDisclosure} from '@react-aria/disclosure';
import {
  useDisclosureGroupState,
  useDisclosureState
} 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 Disclosure(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);

  return (
    <div className="disclosure">
      <h3>
        <button
          className="trigger"
          ref={triggerRef}
          {...buttonProps}
        >
          <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 {useButton} from '@react-aria/button';
import {useDisclosure} from '@react-aria/disclosure';
import {
  useDisclosureGroupState,
  useDisclosureState
} 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 Disclosure(
  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
    );

  return (
    <div className="disclosure">
      <h3>
        <button
          className="trigger"
          ref={triggerRef}
          {...buttonProps}
        >
          <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>
  );
}
Show CSS
.disclosure {
  .trigger {
    background: none;
    border: none;
    box-shadow: none;
    font-weight: bold;
    font-size: 16px;
    display: flex;
    align-items: center;
    gap: 8px;

    svg {
      rotate: 0deg;
      transition: rotate 200ms;
      width: 12px;
      height: 12px;
      fill: none;
      stroke: currentColor;
      stroke-width: 3px;
    }

    &[aria-expanded="true"] svg {
      rotate: 90deg;
    }
  }
}

.panel {
  margin-left: 32px;
}
.disclosure {
  .trigger {
    background: none;
    border: none;
    box-shadow: none;
    font-weight: bold;
    font-size: 16px;
    display: flex;
    align-items: center;
    gap: 8px;

    svg {
      rotate: 0deg;
      transition: rotate 200ms;
      width: 12px;
      height: 12px;
      fill: none;
      stroke: currentColor;
      stroke-width: 3px;
    }

    &[aria-expanded="true"] svg {
      rotate: 90deg;
    }
  }
}

.panel {
  margin-left: 32px;
}
.disclosure {
  .trigger {
    background: none;
    border: none;
    box-shadow: none;
    font-weight: bold;
    font-size: 16px;
    display: flex;
    align-items: center;
    gap: 8px;

    svg {
      rotate: 0deg;
      transition: rotate 200ms;
      width: 12px;
      height: 12px;
      fill: none;
      stroke: currentColor;
      stroke-width: 3px;
    }

    &[aria-expanded="true"] svg {
      rotate: 90deg;
    }
  }
}

.panel {
  margin-left: 32px;
}

Usage#

The following examples show how to use the DisclosureGroup component created in the above example.

<DisclosureGroup>
  <Disclosure title="Personal Information">
    Personal information form here.
  </Disclosure>
  <Disclosure title="Billing Address">
    Billing address form here.
  </Disclosure>
</DisclosureGroup>
<DisclosureGroup>
  <Disclosure title="Personal Information">
    Personal information form here.
  </Disclosure>
  <Disclosure title="Billing Address">
    Billing address form here.
  </Disclosure>
</DisclosureGroup>
<DisclosureGroup>
  <Disclosure title="Personal Information">
    Personal
    information form
    here.
  </Disclosure>
  <Disclosure title="Billing Address">
    Billing address
    form here.
  </Disclosure>
</DisclosureGroup>

Default expansion#

Which disclosure is expanded by default can be set with the defaultExpandedKeys prop.

<DisclosureGroup defaultExpandedKeys={['billing']}>
  <Disclosure id="personal" title="Personal Information">
    Personal information form here.
  </Disclosure>
  <Disclosure id="billing" title="Billing Address">
    Billing address form here.
  </Disclosure>
</DisclosureGroup>
<DisclosureGroup defaultExpandedKeys={['billing']}>
  <Disclosure id="personal" title="Personal Information">
    Personal information form here.
  </Disclosure>
  <Disclosure id="billing" title="Billing Address">
    Billing address form here.
  </Disclosure>
</DisclosureGroup>
<DisclosureGroup
  defaultExpandedKeys={[
    'billing'
  ]}
>
  <Disclosure
    id="personal"
    title="Personal Information"
  >
    Personal
    information form
    here.
  </Disclosure>
  <Disclosure
    id="billing"
    title="Billing Address"
  >
    Billing address
    form here.
  </Disclosure>
</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}
    >
      <Disclosure id="personal" title="Personal Information">
        Personal information form here.
      </Disclosure>
      <Disclosure id="billing" title="Billing Address">
        Billing address form here.
      </Disclosure>
    </DisclosureGroup>
  );
}
function ControlledDisclosureGroup(props) {
  let [expandedKeys, setExpandedKeys] = React.useState([
    'personal'
  ]);

  return (
    <DisclosureGroup
      expandedKeys={expandedKeys}
      onExpandedChange={setExpandedKeys}
    >
      <Disclosure
        id="personal"
        title="Personal Information"
      >
        Personal information form here.
      </Disclosure>
      <Disclosure id="billing" title="Billing Address">
        Billing address form here.
      </Disclosure>
    </DisclosureGroup>
  );
}
function ControlledDisclosureGroup(
  props
) {
  let [
    expandedKeys,
    setExpandedKeys
  ] = React.useState([
    'personal'
  ]);

  return (
    <DisclosureGroup
      expandedKeys={expandedKeys}
      onExpandedChange={setExpandedKeys}
    >
      <Disclosure
        id="personal"
        title="Personal Information"
      >
        Personal
        information form
        here.
      </Disclosure>
      <Disclosure
        id="billing"
        title="Billing Address"
      >
        Billing address
        form here.
      </Disclosure>
    </DisclosureGroup>
  );
}

Multiple expanded#

Multiple disclosures can be expanded at the same time by setting the allowsMultipleExpanded prop to true.

<DisclosureGroup allowsMultipleExpanded>
  <Disclosure title="Personal Information">
    Personal information form here.
  </Disclosure>
  <Disclosure title="Billing Address">
    Billing address form here.
  </Disclosure>
</DisclosureGroup>
<DisclosureGroup allowsMultipleExpanded>
  <Disclosure title="Personal Information">
    Personal information form here.
  </Disclosure>
  <Disclosure title="Billing Address">
    Billing address form here.
  </Disclosure>
</DisclosureGroup>
<DisclosureGroup
  allowsMultipleExpanded
>
  <Disclosure title="Personal Information">
    Personal
    information form
    here.
  </Disclosure>
  <Disclosure title="Billing Address">
    Billing address
    form here.
  </Disclosure>
</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>
  <Disclosure title="Personal Information">
    Personal information form here.
  </Disclosure>
  <Disclosure title="Billing Address">
    Billing address form here.
  </Disclosure>
</DisclosureGroup>
<DisclosureGroup isDisabled>
  <Disclosure title="Personal Information">
    Personal information form here.
  </Disclosure>
  <Disclosure title="Billing Address">
    Billing address form here.
  </Disclosure>
</DisclosureGroup>
<DisclosureGroup
  isDisabled
>
  <Disclosure title="Personal Information">
    Personal
    information form
    here.
  </Disclosure>
  <Disclosure title="Billing Address">
    Billing address
    form here.
  </Disclosure>
</DisclosureGroup>