Skip to main content

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 or Dependency 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 the DisclosureButton
  • aria-controls on the DisclosureButton (if linked to DisclosurePanel)
  • id and aria-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

InputTypeDescription
defaultOpenbooleanWhether it starts open
classstringClass for styling wrapper
MethodDescription
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

PatternSupports Multiple?Central Control?Use Case
#templateRef✅ Yes✅ via ViewChildrenRecommended for most use cases
DisclosureContextService (DI)✅ Per-instance only❌ NoUse inside isolated nested components

See Also