forty-cdk
llms.txt

Primitives

Disclosure

A single trigger that shows or hides a related region of content.

forty-cdk/disclosure WAI-ARIA APG

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, multiple allows 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.

Default

Behavior, ARIA and focus are handled for you; the styling is entirely yours.

Disabled

The trigger leaves the tab order and ignores clicks and keys.

API

ForDisclosure

PropertyTypeDescription
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 attributeValues
data-stateopen | closed
data-disabledpresent | absent

ForDisclosureTrigger

PropertyTypeDescription
disabled
input
Disables this trigger only, merged OR with the root's disabled.
Default: —
Data attributeValues
data-stateopen | closed
data-disabledpresent | 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 attributeValues
data-stateopen | closed
data-disabledpresent | 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 for animate.enter / animate.leave.
  • Leave it mounted: preserve scroll/input state or run CSS-only transitions off data-state. While closed, the directive sets aria-hidden="true" and inert on the host so the panel is removed from the accessibility tree and focus order. Add display: 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 hidden attribute to the content. Either wrap it with @if (open()) so it unmounts when closed, or leave it mounted and rely on the aria-hidden="true" + inert reflection 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 disabled attribute 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.