Skip to content

DisclosureGroup ​

A disclosure group coordinates a set of disclosures: by default opening one closes the last, and allows-multiple-expanded lets them stand open together. Without a group each Disclosure is independent, which is often what you want — reach for the group when the sections compete for the same space.

ts
import { Disclosure, DisclosureGroup } from "ropav";

Every component ships its own stylesheet, and the theme decides the colours.

disclosure-group-basic.vue
vue
<script setup lang="ts">
import {
  Button,
  Disclosure,
  DisclosureBody,
  DisclosureContent,
  DisclosureGroup,
  DisclosureHeading,
  DisclosureIndicator,
} from "ropav";

const sections = [
  {
    body: "Every component ships its own stylesheet, and the theme decides the colours.",
    id: "styling",
    label: "Styling",
  },
  {
    body: "One tab stop per control, and the keyboard interactions the ARIA pattern asks for.",
    id: "keyboard",
    label: "Keyboard",
  },
];
</script>

<template>
  <DisclosureGroup class="panel" :default-expanded-keys="['styling']">
    <Disclosure v-for="section in sections" :id="section.id" :key="section.id">
      <DisclosureHeading>
        <Button class="trigger" variant="tertiary">
          {{ section.label }}
          <DisclosureIndicator class="indicator" />
        </Button>
      </DisclosureHeading>
      <DisclosureContent>
        <DisclosureBody class="body">{{ section.body }}</DisclosureBody>
      </DisclosureContent>
    </Disclosure>
  </DisclosureGroup>
</template>

<style scoped>
.panel {
  width: 100%;
  max-width: var(--rp-container-md);
}

.trigger {
  width: 100%;
  justify-content: space-between;
}

.indicator {
  color: var(--rp-muted);
}

/* `DisclosureBody` puts the class on the element inside its root, which a scoped rule cannot
   reach on its own - the panel is the nearest thing wearing the scope. */
.panel :deep(.body) {
  color: var(--rp-muted);
  padding: calc(var(--rp-spacing) * 3);
  font-size: var(--rp-text-sm);
  line-height: var(--rp-text-sm--line-height);
}
</style>

Each disclosure needs an id to be keyed by, and those keys are what expanded-keys carries. default-expanded-keys starts the group somewhere; v-model:expanded-keys, or expanded-keys with an expanded-change listener, hands the state to the caller.

is-disabled on the group disables every disclosure in it, and a disclosure can still disable itself. The parts inside — DisclosureHeading, DisclosureContent, DisclosureBody, DisclosureIndicator — belong to Disclosure, which is where they are documented.

The trigger is whatever you put there ​

A disclosure hands its press behaviour down to the first pressable inside it, so an ordinary Button in the heading becomes the trigger — with the aria-expanded and aria-controls wiring already on it — and nothing has to be forwarded. DisclosureContent shadows that, so a button inside the panel stays an ordinary button.

Accessibility ​

  • The heading is a real <h3>, so the sections show up in a document outline the way a reader navigating by heading expects.
  • The trigger carries aria-expanded and aria-controls, and the panel is what those point at.
  • With one panel open at a time, closing happens under the reader — which is a reason to prefer allows-multiple-expanded where the sections are reference material rather than steps.

API ​

PropTypeDefaultDescription
allowsMultipleExpandedboolean—Whether more than one disclosure can be expanded at a time.
defaultExpandedKeysIterable<DisclosureKey>—Expanded disclosure keys in uncontrolled mode.
expandedKeysIterable<DisclosureKey>—Expanded disclosure keys in controlled mode. Pairs with v-model:expanded-keys.
isDisabledboolean—Disables every disclosure in the group.

Slots: default.

EventPayload
expandedChangeSet<DisclosureKey>
update:expandedKeysSet<DisclosureKey>