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
templateRefsorDependency 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
#templateRefsand 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-expandedon theDisclosureButtonaria-controlson theDisclosureButton(if linked to DisclosurePanel)idandaria-labelledbylinkage 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
Disclosureis open - Can be styled or animated freely
hiddenattribute 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 |