Accordion
A stack of headed sections that each show or hide their content.
- v1.0.0
- WAI-ARIA: Accordion
- SSR
- Zoneless
- 2.9 kB gzip
- +0.8 kB Angular CDK, shared
import { Component } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
Accordion,
AccordionContent,
AccordionHeader,
AccordionItem,
AccordionTrigger,
} from '@/ui/accordion/accordion';
/**
* Frequently asked questions, one open at a time. The open one closes
* again on its own button, since the accordion is collapsible.
*/
@Component({
selector: 'example-accordion-demo',
imports: [
Accordion,
AccordionContent,
AccordionHeader,
AccordionItem,
AccordionTrigger,
NdIcon,
],
template: `
<div
ndAccordion
collapsible
defaultValue="shipping"
class="w-md max-w-full"
>
@for (question of questions; track question.value) {
<div ndAccordionItem [value]="question.value">
<h2 ndAccordionHeader>
<button ndAccordionTrigger>
{{ question.title }}
<svg ndIcon="chevron-down"></svg>
</button>
</h2>
<div ndAccordionContent>{{ question.answer }}</div>
</div>
}
</div>
`,
})
export class AccordionDemoExample {
protected readonly questions = [
{
value: 'shipping',
title: 'How long does shipping take?',
answer:
'Orders ship within two business days, and arrive in three to five.',
},
{
value: 'returns',
title: 'What is your return policy?',
answer: 'Return any item within 30 days of delivery for a full refund.',
},
{
value: 'support',
title: 'How do I reach support?',
answer: 'Email us any time; we answer within a day.',
},
];
}
An accordion stacks headed sections that each show or hide their content,
such as a list of frequently asked questions. Each heading's button opens
its section; by default one section is open at a time, multiple lets
several be, and collapsible lets the open one close again. The arrow
keys move between the buttons.
Installation
npx @ndop/cli add accordionpnpm dlx @ndop/cli add accordionyarn dlx @ndop/cli add accordionbunx @ndop/cli add accordionInstall the dependencies
npm install @ndop/primitives @ndop/corepnpm add @ndop/primitives @ndop/coreyarn add @ndop/primitives @ndop/corebun add @ndop/primitives @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/accordion/accordion.tsimport { computed, Directive, input } from '@angular/core'; import { NdAccordion, NdAccordionContent, NdAccordionHeader, NdAccordionItem, NdAccordionTrigger, } from '@ndop/primitives/accordion'; import { cn } from '@/lib/utils'; /** * An accordion (`@ndop/primitives/accordion`): a stack of headed sections * that each show or hide their content. One section opens at a time unless * it's `multiple`; `collapsible` lets the open one close. * * @example * ```html * <div ndAccordion collapsible defaultValue="shipping"> * <div ndAccordionItem value="shipping"> * <h3 ndAccordionHeader> * <button ndAccordionTrigger> * Shipping <svg ndIcon="chevron-down"></svg> * </button> * </h3> * <div ndAccordionContent>Ships in two days.</div> * </div> * </div> * ``` */ @Directive({ selector: '[ndAccordion]', hostDirectives: [ { directive: NdAccordion, inputs: ['value', 'defaultValue', 'multiple', 'collapsible', 'disabled'], outputs: ['valueChange'], }, ], host: { 'data-slot': 'accordion' }, }) export class Accordion {} /** * One section of an accordion, with a line under it. * * @example * ```html * <div ndAccordionItem value="shipping">…</div> * ``` */ @Directive({ selector: '[ndAccordionItem]', hostDirectives: [ { directive: NdAccordionItem, inputs: ['value', 'disabled'] }, ], host: { 'data-slot': 'accordion-item', '[class]': 'classes()' }, }) export class AccordionItem { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('border-b last:border-b-0', this.class()), ); } /** * The heading around a section's trigger: an `h2` to `h6` that fits the * page's outline. * * @example * ```html * <h3 ndAccordionHeader>…</h3> * ``` */ @Directive({ selector: '[ndAccordionHeader]', hostDirectives: [NdAccordionHeader], host: { 'data-slot': 'accordion-header', '[class]': 'classes()' }, }) export class AccordionHeader { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('flex', this.class())); } /** * The button that opens and closes its section. Put the chevron icon in it, * last; it turns over while the section is open. * * @example * ```html * <button ndAccordionTrigger>Shipping <svg ndIcon="chevron-down"></svg></button> * ``` */ @Directive({ selector: 'button[ndAccordionTrigger]', hostDirectives: [NdAccordionTrigger], host: { 'data-slot': 'accordion-trigger', '[class]': 'classes()' }, }) export class AccordionTrigger { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-start text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180', '[&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:translate-y-0.5 [&>svg]:text-muted-foreground [&>svg]:transition-transform [&>svg]:duration-200', this.class(), ), ); } /** * The content its trigger shows and hides, sliding open and closed. * * @example * ```html * <div ndAccordionContent>Ships in two days.</div> * ``` */ @Directive({ selector: '[ndAccordionContent]', hostDirectives: [NdAccordionContent], host: { 'data-slot': 'accordion-content', '[class]': 'classes()' }, }) export class AccordionContent { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down', // tw-animate-css's accordion keyframes read this height. '[--radix-accordion-content-height:var(--nd-accordion-content-height)]', // Space below the text as a pseudo-element, which the height // animation clips; padding would stay visible at height 0. 'after:block after:h-4', this.class(), ), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import {
Accordion,
AccordionContent,
AccordionHeader,
AccordionItem,
AccordionTrigger,
} from '@/ui/accordion/accordion';<div
ndAccordion
collapsible
defaultValue="shipping"
class="w-md max-w-full"
>
@for (question of questions; track question.value) {
<div ndAccordionItem [value]="question.value">
<h2 ndAccordionHeader>
<button ndAccordionTrigger>
{{ question.title }}
<svg ndIcon="chevron-down"></svg>
</button>
</h2>
<div ndAccordionContent>{{ question.answer }}</div>
</div>
}
</div>Examples
Accordion multiple
Sections that open independently, with one disabled.
import { Component, signal } from '@angular/core';
import { NdIcon } from '@ndop/core';
import {
Accordion,
AccordionContent,
AccordionHeader,
AccordionItem,
AccordionTrigger,
} from '@/ui/accordion/accordion';
/**
* Sections that open independently, with `multiple`. One is disabled,
* and the open sections are bound to a signal.
*/
@Component({
selector: 'example-accordion-multiple',
imports: [
Accordion,
AccordionContent,
AccordionHeader,
AccordionItem,
AccordionTrigger,
NdIcon,
],
template: `
<div ndAccordion multiple [(value)]="open" class="w-md max-w-full">
<div ndAccordionItem value="profile">
<h2 ndAccordionHeader>
<button ndAccordionTrigger>
Profile <svg ndIcon="chevron-down"></svg>
</button>
</h2>
<div ndAccordionContent>Your name, photo and bio.</div>
</div>
<div ndAccordionItem value="billing" disabled>
<h2 ndAccordionHeader>
<button ndAccordionTrigger>
Billing <svg ndIcon="chevron-down"></svg>
</button>
</h2>
<div ndAccordionContent>Managed by your organization.</div>
</div>
<div ndAccordionItem value="security">
<h2 ndAccordionHeader>
<button ndAccordionTrigger>
Security <svg ndIcon="chevron-down"></svg>
</button>
</h2>
<div ndAccordionContent>Password and two-factor authentication.</div>
</div>
</div>
`,
})
export class AccordionMultipleExample {
protected readonly open = signal<readonly string[]>(['profile', 'security']);
}
Accessibility
Implements the Accordion pattern of the WAI-ARIA Authoring Practices.
Keyboard
Keys the component handles.
| Key | Action |
|---|---|
| Tab | Moves focus through the triggers and any open content, in document order. |
| EnterorSpace | Opens or closes the focused trigger's item. |
| ArrowDown | Moves focus to the next enabled trigger, wrapping to the first. |
| ArrowUp | Moves focus to the previous enabled trigger, wrapping to the last. |
| Home | Moves focus to the first enabled trigger. |
| End | Moves focus to the last enabled trigger. |
Focus
- Every enabled trigger is in the tab order; opening or closing an item keeps focus on its trigger.
- Disabled triggers are skipped by the arrow keys.
Right-to-left
Nothing mirrors: the arrow keys follow the vertical order of the triggers.
API reference
Accordion
[ndAccordion]An accordion (@ndop/primitives/accordion): a stack of headed sections
that each show or hide their content. One section opens at a time unless
it's multiple; collapsible lets the open one close.
| Accordion binding | Type | Default |
|---|---|---|
[defaultValue]Initially open items when value isn't bound: one value or a list. | string | readonly string[] | null | null |
[multiple]Lets several items be open at once. | boolean | false |
[collapsible]Lets the open item close when only one item may be open. | boolean | false |
[disabled]Disables every item. | boolean | false |
[(value)]The open items' values. Two-way bindable with [(value)]. | readonly string[] | [] |
| Data attribute | Values |
|---|---|
data-slot | accordion |
AccordionItem
[ndAccordionItem]One section of an accordion, with a line under it.
| AccordionItem binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
[value]requiredThe value value lists while it's open. | string | — |
[disabled]Disables its trigger. | boolean | false |
| Data attribute | Values |
|---|---|
data-slot | accordion-item |
data-state | — |
data-disabled | — |
AccordionHeader
[ndAccordionHeader]The heading around a section's trigger: an h2 to h6 that fits the
page's outline.
| AccordionHeader binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | accordion-header |
data-state | — |
data-disabled | — |
AccordionTrigger
button[ndAccordionTrigger]The button that opens and closes its section. Put the chevron icon in it, last; it turns over while the section is open.
Manages aria-expanded, aria-controls, aria-disabled, id.
| AccordionTrigger binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | accordion-trigger |
data-state | — |
data-disabled | — |
AccordionContent
[ndAccordionContent]role regionThe content its trigger shows and hides, sliding open and closed.
Manages aria-labelledby, id, hidden.
| AccordionContent binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | accordion-content |
data-state | — |
data-disabled | — |
data-animating | — |
Agent notes
Do
- Put each trigger in a heading of the level that fits the page.
- Keep the headings short enough to scan.
- Use
collapsibleunless one section must always stay open.
Don't
- Don't hide content everyone needs; show it.
- Don't use it for a single section; use Collapsible.