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.
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.
disclosure-basic.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"onDisclosureContentsays so. is-disabledreaches the trigger, which becomes a realdisabledbutton 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
| Prop | Type | Default | Description |
|---|---|---|---|
defaultExpanded | boolean | — | Expanded in uncontrolled mode. |
id | string | — | Identity of the disclosure within a group, and the value carried in the group's expandedKeys. Generated when omitted. |
isDisabled | boolean | — | Disables the disclosure, and its trigger with it. |
isExpanded | boolean | — | Expanded in controlled mode. Pairs with v-model:is-expanded. |
Slots: default.
| Event | Payload |
|---|---|
expandedChange | boolean |
update:isExpanded | boolean |
DisclosureBody
Slots: default.
DisclosureContent
| Prop | Type | Default | Description |
|---|---|---|---|
role | string | "group" | ARIA role of the panel. Matches React Aria's default. |
Slots: default.
DisclosureHeading
Slots: default.
DisclosureIndicator
Slots: default.
DisclosureTrigger
Slots: default.