Disclosure
The Disclosure
component is a fully headless and accessible UI primitive that enables toggling content visibility. It's inspired by HeadlessUI Disclosure, but built natively for Angular.
It is useful for building collapsible panels, accordions, and FAQs — while giving you complete control over markup and styling.
When to use it
Use Disclosure
when you need a toggleable UI component that is:
- Fully customizable in layout and appearance
- Accessible with screen readers and keyboard navigation
- Controlled via Angular
templateRefs
orDependency Injection (DI)
- Compatible with reactive or template-driven forms
Common use cases:
- Collapsible panels
- Accordions
- FAQs
Anatomy
A complete Disclosure
is composed of three components:
Disclosure
- The provider and wrapper. Manages internal open/close state.DisclosureButton
- The trigger that toggles the panel open or closed.DisclosurePanel
- The container for content that is shown or hidden.
All three components are standalone and composable.
Features
- ✅ Accessible by default (ARIA attributes)
- ✅ Tailwind-friendly (no styles imposed)
- ✅ Multiple selectors supported:
<Disclosure>
<div ngxDisclosure>
<ngx-headlessui-disclosure>
- ✅ Works with both
#templateRefs
and Angular DI (DisclosureContextService
) - ✅ Supports multiple instances per page
Installation
Disclosure
ships as part of the @ngx-headless/ui
by default. Install if you haven't already.
npm install @ngx-headless/ui
Import the components directly:
import {
DisclosureComponent,
DisclosureButtonComponent,
DisclosurePanelComponent,
} from "@ngx-headless/ui";
Usage Examples
Template Reference — Single Disclosure
<Disclosure #d="ngxDisclosure">
<DisclosureButton [class.active]="d.isOpen()">Toggle</DisclosureButton>
<DisclosurePanel *ngIf="d.isOpen()">Panel content</DisclosurePanel>
</Disclosure>
Template Reference — Multiple Disclosures (ngFor)
<Disclosure *ngFor="let faq of faqs" #d="ngxDisclosure">
<DisclosureButton>{{ faq.question }}</DisclosureButton>
<DisclosurePanel *ngIf="d.isOpen()">{{ faq.answer }}</DisclosurePanel>
</Disclosure>
Access all instances in the component in your class Root:
...
export class MyComponent {
...
@ViewChildren('d') disclosures!: QueryList<DisclosureComponent>;
// ☝️ access to All Disclosures via #templateRefs
// 🧠 NOTE: You can also use @ViewChild with DisclosureComponent without QueryList<> to capture a single instance
...
}
Dependency Injection — Single Disclosure (Advanced Usage)
In cases where you want to encapsulate toggle logic or access open state inside a child component, you can inject DisclosureContextService.
This is especially useful for buttons, status indicators, or nested logic inside a single Disclosure.
Here's an example:
Add a custom component anywhere inside the <Disclosure>
wrapper:
<Disclosure>
...
<MyChildComponent></MyChildComponent>
...
</Disclosure>
Inject DisclosureContextService into the constructor of your component. You can then access and control the disclosure state programmatically.
import { DisclosureContextService } from '@ngx-headless/ui';
...
export class MyChildComponent {
constructor(public disclosure: DisclosureContextService) {}
...
disclosure.isOpen();
disclosure.toggle();
disclosure.close();
disclosure.open();
}
🧠 Note: This works only inside a single
<Disclosure>
context. Injection is not valid for tracking multiple disclosures.
Accessibility
This component handles
aria-expanded
on theDisclosureButton
aria-controls
on theDisclosureButton
(if linked to DisclosurePanel)id
andaria-labelledby
linkage between button and panel (optional but supported)- Keyboard interaction (customizable): toggle on Enter / Space
- tabindex and focus ring behaviors handled by browser natively
- Screen reader compatibility for collapsed/expanded state
Note: The component is fully headless — all accessibility behaviors are opt-in or behavior-driven without imposing any DOM structure. You are responsible for adding aria-controls, id, and keyboard bindings if needed.
Animations
DisclosurePanel
can be animated freely using Angular’s built-in animation system.
Angular supports entry/exit transitions using @trigger
bindings and the :enter
/:leave
lifecycle hooks.
👉 See Angular Animations Guide
Example
Add an animation trigger to your component:
import { trigger, transition, style, animate } from "@angular/animations";
@Component({
animations: [
trigger("slideToggle", [
transition(":enter", [
style({ height: 0, opacity: 0 }),
animate("200ms ease-out", style({ height: "*", opacity: 1 })),
]),
transition(":leave", [
animate("150ms ease-in", style({ height: 0, opacity: 0 })),
]),
]),
],
})
export class ExampleComponent {}
Use it in the template with *ngIf
:
<DisclosurePanel *ngIf="disclosure.isOpen()" @slideToggle>
Panel content goes here.
</DisclosurePanel>
✅ You now have full control over the entrance and exit behavior of the panel.
You can combine this with Tailwind
transitions or any styling framework as needed.
Component API
DisclosureComponent
Input | Type | Description |
---|---|---|
defaultOpen | boolean | Whether it starts open |
class | string | Class for styling wrapper |
Method | Description |
---|---|
isOpen() | Returns true if panel is open |
toggle() | Toggles panel open/close |
open() | Opens panel |
close() | Closes panel |
DisclosureButtonComponent
- Automatically binds
aria-expanded
- Toggles panel on click
DisclosurePanelComponent
- Visible only when
Disclosure
is open - Can be styled or animated freely
hidden
attribute used for visibility
Access Patterns
Pattern | Supports Multiple? | Central Control? | Use Case |
---|---|---|---|
#templateRef | ✅ Yes | ✅ via ViewChildren | Recommended for most use cases |
DisclosureContextService (DI) | ✅ Per-instance only | ❌ No | Use inside isolated nested components |