Primitives
Disclosure
A single trigger that shows or hides a related region of content.
Toggle the panel with the pointer or Enter and watch data-state flip on the trigger and the content together, so one rule animates both.
One headless primitive, a styleless trigger, and the panel you are reading. Behavior, ARIA and focus are handled for you; the styling is entirely yours.
A button toggles the visibility of a content region, wired with aria-expanded and aria-controls.
When to choose
- Disclosure: one trigger and one region, independent of everything around it. Nothing coordinates it with a neighbour, so several on a page open and close freely.
- Accordion: a group of items under one root sharing a
[(value)]. Single mode closes the open panel when another opens,multipleallows several, and arrow keys move focus between the triggers. Choose it when the sections belong together and their open state is one decision.
Anatomy
<div forDisclosure [(open)]="isOpen">
<button type="button" forDisclosureTrigger>{{ isOpen() ? 'Hide' : 'Show' }} details</button>
<div forDisclosureContent>Hidden content goes here.</div>
</div>
Examples
States
One class and one directive, two states. disabled drops the trigger from the tab order and blocks toggling, so the panel stays where it is; the root and the trigger both reflect data-disabled, which is all the example's stylesheet keys on.
Behavior, ARIA and focus are handled for you; the styling is entirely yours.
The trigger leaves the tab order and ignores clicks and keys.
API
ForDisclosure
| Property | Type | Description |
|---|---|---|
open | model | Two-way bindable open state. Default: false |
disabled | input | When true, click on the trigger is ignored. Reflects data-disabled on the host.Default: — |
| Data attribute | Values | |
|---|---|---|
data-state | open | closed | |
data-disabled | present | absent |
ForDisclosureTrigger
| Property | Type | Description |
|---|---|---|
disabled | input | Disables this trigger only, merged OR with the root's disabled.Default: — |
| Data attribute | Values | |
|---|---|---|
data-state | open | closed | |
data-disabled | present | absent |
Reflects on its host: id, aria-expanded, aria-controls, disabled, data-state. Toggles the state on click. The disabled reflection and the click guard follow the effective state, which is the trigger's own disabled OR the root's. That reflection is the native disabled attribute plus data-disabled, with no aria-disabled: one channel only.
aria-controls is emitted only while open (mirroring the overlay triggers' open-only gating), so the reference never dangles at an unmounted panel under the recommended @if (open()) mount pattern.
Use a native <button type="button"> so Enter / Space activation come for free. Other elements lose keyboard accessibility, and that is on you.
ForDisclosureContent
| Data attribute | Values | |
|---|---|---|
data-state | open | closed | |
data-disabled | present | absent |
Reflects on its host: id, data-state, data-disabled, aria-hidden (when closed), inert (when closed).
The directive does not apply [hidden] or otherwise control DOM presence. Two patterns work:
- Mount/unmount with
@if (open()): the panel is absent from the DOM while closed; idiomatic foranimate.enter/animate.leave. - Leave it mounted: preserve scroll/input state or run CSS-only transitions off
data-state. While closed, the directive setsaria-hidden="true"andinerton the host so the panel is removed from the accessibility tree and focus order. Adddisplay: none(or your own collapse animation) keyed on[data-state="closed"]to also hide it visually.
If the panel is a semantic region, add role="region" and aria-labelledby="..." pointing to the trigger.
Accessibility
Implements the WAI-ARIA Disclosure pattern.
- The library does not auto-add
role="button"or keyboard handlers when the trigger is not a<button>. Always use a real button. - The directive does not apply the native
hiddenattribute to the content. Either wrap it with@if (open())so it unmounts when closed, or leave it mounted and rely on thearia-hidden="true"+inertreflection that keeps the closed panel out of the accessibility tree and focus order. Visual hiding (and enter/leave transitions) are still on you. Drive them off[data-state]. - Disabled state sets the native
disabledattribute on the trigger (effective on<button>elements). Click is also ignored at the directive level as a defensive measure. The trigger can be disabled from the root ([forDisclosure] [disabled]) or per trigger ([forDisclosureTrigger] [disabled]); either source disables it.
Styling
forty-cdk ships no styles: put your own class on each piece and key your CSS off the data-* attributes listed under API, not off the for* selectors (Styling forty-cdk explains why).
.dis-trigger .chevron {
transition: transform 150ms ease;
}
.dis-trigger[data-state='open'] .chevron {
transform: rotate(180deg);
}
Wrapping in a design system
Subclass the root and re-provide FOR_DISCLOSURE_CONTEXT with useExisting pointing at the subclass, since Angular does not inherit a directive's providers; Wrapping non-form roots walks the pattern.