Skip to content

Disclosure ​

A disclosure is one section that opens and closes. It stands on its own — reach for a DisclosureGroup only when several of them compete for the same space and opening one should close the last.

ts
import {
  Disclosure,
  DisclosureBody,
  DisclosureContent,
  DisclosureHeading,
  DisclosureIndicator,
} from "ropav";

DisclosureHeading is the heading, DisclosureContent is the panel it controls, and DisclosureBody is the padded box inside that panel — the split is what lets the panel animate its height while its contents keep their own spacing.

Every component, its stylesheet, and the types that go with them.
disclosure-basic.vue
vue
<script setup lang="ts">
import {
  Button,
  Disclosure,
  DisclosureBody,
  DisclosureContent,
  DisclosureHeading,
  DisclosureIndicator,
} from "ropav";
</script>

<template>
  <Disclosure class="panel" default-expanded>
    <DisclosureHeading>
      <Button class="trigger" variant="tertiary">
        What is in the box?
        <DisclosureIndicator class="indicator" />
      </Button>
    </DisclosureHeading>
    <DisclosureContent>
      <DisclosureBody class="body">
        Every component, its stylesheet, and the types that go with them.
      </DisclosureBody>
    </DisclosureContent>
  </Disclosure>
</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>

default-expanded starts it open; v-model:is-expanded, or is-expanded with an expanded-change listener, hands the state to the caller. An id is only needed inside a group, where it is the key the group tracks.

The trigger is whatever you put there ​

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

DisclosureTrigger exists for the case where the heading holds something that is not pressable on its own and you want to say where the press goes.

Accessibility ​

  • The heading is a real <h3>, so the section appears in a document outline where a reader navigating by heading expects to find it.
  • The panel keeps role="group" by default. Where the section is a region of the page in its own right, role="region" on DisclosureContent says so.
  • is-disabled reaches the trigger, which becomes a real disabled button and leaves the tab order with it. Where the section should still be findable, say in the heading why it is unavailable rather than disabling it.

API ​

Disclosure

PropTypeDefaultDescription
defaultExpandedboolean—Expanded in uncontrolled mode.
idstring—Identity of the disclosure within a group, and the value carried in the group's expandedKeys. Generated when omitted.
isDisabledboolean—Disables the disclosure, and its trigger with it.
isExpandedboolean—Expanded in controlled mode. Pairs with v-model:is-expanded.

Slots: default.

EventPayload
expandedChangeboolean
update:isExpandedboolean

DisclosureBody

Slots: default.

DisclosureContent

PropTypeDefaultDescription
rolestring"group"ARIA role of the panel. Matches React Aria's default.

Slots: default.

DisclosureHeading

Slots: default.

DisclosureIndicator

Slots: default.

DisclosureTrigger

Slots: default.