ComponentsData & statusAccordion
On this page
Data & status

Accordion released

A stack of sections the reader opens and closes — one at a time, or as many as they like.

  • Import@pacit/components/accordion
  • Selectorspct-accordion, pct-accordion-item

Implements the W3C ARIA APG Accordion pattern ↗

What ships in the package?

Standalone components, design tokens and schematics — no zone.js.

Which Angular versions?

One major per release line; the support policy names the numbers.

Can I retheme it?

Every colour is a token — override one variable or a whole scope.

accordion.demo.ts — the same file runs here and is highlighted under Code

Start here

Usage

One import per component, and the shortest possible use — the package itself is installed once, on the start page.

Import this component
import { PctAccordion, PctAccordionItem } from '@pacit/components/accordion';
ExportedWhat it is
PctAccordionApiinterfaceWhat an item needs to know about the group it stands in: the name that makes the group exclusive, and the level its title is drawn at. Declared HERE, beside the group, so that the item can inject it while importing the group — the other direction would close the import cycle. It is the tabs' channel one component over, and for the same reason.
PCT_ACCORDIONtokenThe channel through which a group offers itself to the items below it.
PctAccordioncomponentpct-accordionAn accordion: a stack of sections a user opens and closes, each with a heading of its own.
PctAccordionItemcomponentpct-accordion-itemOne section of a <pct-accordion>: a <details> with a <summary>, a heading inside the summary, and the consumer's content below it.
PctAccordionHeadingLeveltype2 | 3 | 4 | 5 | 6The heading level an item's title is drawn at — a real <h2>…<h6>, because the whole reason the title is a heading is that a screen-reader user jumps between sections with the H key, and that navigation reads the document's outline rather than an attribute.
<pct-accordion>
  <pct-accordion-item label="What ships in the package?"> Standalone components, design tokens and schematics. </pct-accordion-item>
</pct-accordion>
Examples

Every state, running

Each example is the component itself, not a screenshot. Open the code under any of them.

One at a time

exclusive closes the open section when another opens — a FAQ reads better that way. Without it every section keeps its own state, which is what a settings page wants. headingLevel sets the heading the buttons sit in, so the outline stays honest.

Do I need zone.js?

No — the library is zoneless from the first line.

Does it render on the server?

Every component renders on the server and hydrates without a mismatch.

Can a screen reader use it?

Every promise about that is a test in three engines.
API

What it reads, and what it writes back

Read from the source at build time — the types, the defaults and the lines of documentation are the library's own.

An accordion: a stack of sections a user opens and closes, each with a heading of its own.

Inputs

NameTypeWhat it does
exclusivedefault falsebooleanWhether opening one section closes the rest.
headingLeveldefault 3PctAccordionHeadingLevelThe level the items' titles are drawn at — <h3> unless told otherwise, which is the level a section of a page under one <h1> and one <h2> usually wants.

Outputs

None of its own. The native events — (click), (focus), the form's submit — are the platform's, and two-way models above carry the rest.

What it writes on the element

The hooks a stylesheet or a test can hold. Stable by contract — the parts inventory gate fails the build when one disappears.

AttributeBound toNote
classpct-accordion
Styling

Parts to select, tokens to override

Two ways in, both stable. No class names to depend on.

Parts

In a test, PctAccordionHarness, PctAccordionItemHarness from @pacit/components/testing read a part by the name below and a state by its attribute — the harness gate holds the names to the package.

PartSelectorWhat it is
heading[data-pct-part="heading"]the button that opens and closes the section
item[data-pct-part="item"]one section: its heading and its panel
marker[data-pct-part="marker"]the open-or-closed sign beside the heading
panel[data-pct-part="panel"]the content shown while the section is open

Tokens

Every value the component reads, with the light and the dark default. Override a token at any scope and it cascades like any custom property.

TokenStyles
--pct-accordion-heading-bg-hover{pct.surface-100}#f1f5f9 · #1e293bBackground of the heading on hover.
--pct-accordion-heading-fg{pct.text}#0f172a · #ffffffText colour of the heading.
--pct-accordion-heading-fg-disabled{pct.text-disabled}#64748bText colour of the heading while disabled.
--pct-accordion-heading-fg-hover{pct.text}#0f172a · #ffffffText colour of the heading on hover.
--pct-accordion-heading-font-weight{pct.font.weight.medium}500Font weight of the heading.
--pct-accordion-heading-gap{pct.space.3}8pxGap between the heading's text and its marker.
--pct-accordion-heading-padding-x{pct.space.4}12pxHorizontal padding of the heading.
--pct-accordion-heading-padding-y{pct.space.3}8pxVertical padding of the heading.
--pct-accordion-heading-target-min{pct.target.min}24pxTouch-target floor of the heading.
--pct-accordion-item-border{pct.border}#e2e8f0 · #334155Edge colour between items.
--pct-accordion-marker-fg{pct.text-muted}#334155 · #e2e8f0Colour of the open-or-closed marker beside the heading.
--pct-accordion-panel-padding-y{pct.space.4}12pxVertical padding of the panel.

Theming

A brand override in a few declarations. Scope it to a theme attribute, a page or one container — the component never notices.

[data-theme='brand'] {
  --pct-accordion-heading-fg: #134e4a;
  --pct-accordion-heading-bg-hover: #f0fdfa;
  --pct-accordion-item-border: #99f6e4;
}

What ships in the package?

Standalone components, design tokens and schematics — no zone.js.

Which Angular versions?

One major per release line; the support policy names the numbers.

Can I retheme it?

Every colour is a token — override one variable or a whole scope.
Accessibility

Measured, not assumed

Pattern: Accordion — named in the class JSDoc

Keyboard

keyeffecttest
Tabone stop per heading — every section is in the page's sequenceapps/sandbox-e2e/src/accordion.spec.ts
Enter / Spaceopens or closes the focused section — the platform'sapps/sandbox-e2e/src/accordion.spec.ts
Enter on a disabled onenothing, because the click it arrives as is cancelledapps/sandbox-e2e/src/accordion.spec.ts

There is no arrow-key walk, and that is the pattern's own answer. The APG lists Up/Down between headers as optional, and lists "all focusable elements in the accordion are in the page Tab sequence" as required — which the platform already does. A document-level key map would be this library taking ArrowDown away from the page (req-api-platform).

What is measured, and what is not yet

Every line is a test in three engines or a gate on every commit. The gaps are listed the same way, each with the requirement that will close it.

MeasuredARIA APG pattern in the JSDoclibs/components/accordion/src/accordion.ts, libs/components/accordion/src/accordion-item.ts
MeasuredKeyboard map testedapps/sandbox-e2e/src/accordion.spec.ts (every row above, in three engines). There is no unit case for the keys, because there is no code to test — the browser is the implementation
MeasuredThe exclusive groupapps/sandbox-e2e/src/accordion.spec.ts — three sections, opened one after another, each closing the last, in three engines, plus one shared name nobody wrote. The control is recorded rather than prepared: [attr.name] taken off the <details> leaves 3 of 36 red, one per engine, all of them this case, and every other case green — the reading the negative-control table below repeats for every other mechanism this component writes
MeasuredThe disclosure state is not oursapps/sandbox-e2e/src/accordion.spec.ts and libs/components/accordion/src/accordion.spec.ts — the asserted absence of aria-expanded, role and aria-controls on the heading
Measuredaxe auditapps/sandbox-e2e/src/a11y.spec.ts — the /accordion view, LTR and RTL, with all five demos of the sandbox on the page at once (open, exclusive, disabled, an h4 level and a lone section); and a stage of its own in the derived list of panels — a <details> is a panel part the sandbox holds open, so the stage holds the claim that one stands open in the walk rather than an audit of its own (lesson-196)
MeasuredVisual screenshotapps/sandbox-e2e/src/visual.spec.ts — accordion-stack, with one section open and two closed, so the marker's quarter turn is a difference inside one image
Measuredforced-colors: activeapps/sandbox-e2e/src/forced-colors.spec.ts — CanvasText on an ordinary heading, GrayText on a refused one, and the marker the same colour as the row it stands in, which it is only because the sheet hands its color back in this mode. The one state the mode cannot flatten is whether a section is open: the platform announces it
Measuredprefers-reduced-motionlibs/components/accordion/src/accordion-item.scss — the only transitions are color and the marker's rotate over --pct-motion-transition-duration, and the preference is answered by the token build (req-a11y-motion)
MeasuredTouch target ≥ 24×24 pxapps/sandbox-e2e/src/accordion.spec.ts — every heading row's box measured outright; the floor is --pct-accordion-heading-target-min on the row itself, so what a finger gets is what an eye sees. The disarming below narrowed what that case holds — with the floor taken out the row still clears 24 px on its padding alone, so it measures the PROMISE and not this line — and apps/sandbox-e2e/src/target-min.spec.ts is what measures the line: with the padding token at zero and the row emptied, the box reads 24 px, and with the floor deleted it reads 0
By designSize axisnone — deliberately: a section heading is not a control with a height. req-api-size is about --pct-control-height-*, and a heading row takes its height from the type and the padding, which a skin moves through the tokens
GapDensity axisnone — gap. The same one every component here has (req-token-density)
MeasuredRTLapps/sandbox-e2e/src/accordion.spec.ts — the marker measured on the far side of the row in both directions, with the direction inherited from the document rather than written on the group
MeasuredSSR + hydrationapps/sandbox-e2e/src/hydration.spec.ts — the /accordion view renders and hydrates with no NG05xx. Every section is in the server's HTML, hidden by a pseudo-element rather than by a branch
By designFormsnone — deliberately: an accordion is not a form control. It holds no value a form owns and implements no FormValueControl
MeasuredA closed section is findableapps/sandbox-e2e/src/accordion.spec.ts — ::details-content computes content-visibility: hidden closed and visible open, and the text inside answers checkVisibility() === false, in three engines
MeasuredThe heading is a real headingapps/sandbox-e2e/src/accordion.spec.ts — the accessible tree read off the DOM (heading … [level=3], [level=4]) plus the tag name; libs/components/accordion/src/accordion.spec.ts for all five levels and the fallback outside a group (lesson-127)
MeasuredThe press can be refusedlibs/components/accordion/src/accordion.spec.ts — defaultPrevented both ways; apps/sandbox-e2e/src/accordion.spec.ts for the pointer and Enter in three engines (lesson-128)
MeasuredParts in the inventorylibs/components/parts.snapshot.md, tools/check-parts.mjs (target check-parts)
MeasuredTokens + contrast.policy.jsonlibs/tokens/src/contrast.policy.json — five entries. The line between sections is warn and says why: a separator is not what identifies a control or its state, and here the state is not drawn at all
By designStrings through PCT_TEXTSnone — deliberately: the component prints no string of its own, so there is no key to route. tools/check-texts.mjs reads its templates all the same
MeasuredSize budgetlibs/components/size.snapshot.md — the ./accordion row
MeasuredScreen-reader logRead 2026-10-12 from docs/acr/at/ — three readers, whole sandbox, and the act that opens a section. On open all three say one word, expanded, and nothing of the revealed content — the disclosure pattern's promise. The question on arrival was what a reader says about a DisclosureTriangle with a heading inside it, and NVDA alone has said the heading, and not in every pass: Payment, button, collapsed on 2026-10-12, as on 2026-09-28, where the record of 2026-09-16 (543eacde) and the pass of 2026-10-08 (dd0f02fa) read Payment, heading, level 3, button, collapsed — Orca Payment · collapsed button., VoiceOver Shipping expanded summary (the element name, not a mapped role). State is announced by all three, and the disabled item is collapsed button grayed under Orca
Measureddocs page/components/accordion on the published site — prerendered, the demo's own source is the code tab
Evidence

What the repository holds for this component

Read at build time from the tracked snapshots, not typed here.

75%mutation score3 of 4 mutants killed
12 × 3e2e cases × enginesplus the shared audits: axe, forced colors, preferences, size, hydration
5colour pairs measuredlight and dark, at every token build
1visual baselinesChromium, the sandbox's own pictures
25elements in the preview8 listeners · 1 render to settle · 7996 µs in jsdom, held exactly but for the clock

Why it is built this way

It implements the pattern by not implementing it. A section is a <details> with a <summary>, so the press, the expanded state a screen reader announces, Enter and Space, the place in the page's tab order and the browser's own find-in-page all belong to the platform. exclusive is one shared name attribute and no code at all — the browser closes the others. What is left for this library is the three things the element has no answer to: the heading the pattern asks for, a way to refuse the press, and a signal to bind (0046).

Decisions behind it

  • 0046A disclosure is the platform's, and so is the group it belongs to
  • 0039A state the platform publishes is not ours to write
  • 0045A panel nobody chose is still text in the document
  • 0011Icons through a template and PCT_ICONS
  • 0013No headless core / skin split
  • lesson-196A list that decides what a gate looks at is a denominator nobody measures
  • lesson-127A heading inside a <summary> survives, because a <summary> is not a button
  • lesson-128preventDefault() is the only way to refuse a disclosure, and it takes the keyboard with it
  • lesson-174Twelve mechanisms taken out one at a time: what a suite holds is not what its case names say
  • lesson-249A centred stage that shrink-wraps its demo moves the control the reader just pressed

Known limitations

  • Nothing animates but the marker. Growing a panel from nothing to auto needs interpolate-size: allow-keywords, and that is chromium's alone — measured false in firefox and webkit. A height animated in one engine of three is a different component in different browsers, so the open and close are instant everywhere, which is what the element does on its own.
  • A heading is a string, not a slot. No icon, no count badge, no second line. A slot would be a second public directive and a second thing to type, and the first component to want one cannot tell a shared property from an accident of the only case — the tabs' reasoning, one component over.
  • Every section's content is rendered, always. That is the price of a closed section being text in the document rather than a branch. A consumer whose section is expensive wraps their own content in an @defer or an @if — which this component cannot do for them without taking the text back out.
  • A disabled section that is open stays open. Refusing the press is all disabled does, and there is nothing else it could do without closing a section the consumer chose to show.
  • exclusive cannot be undone by the consumer's own name. The group generates one and binds it; a name written on the tag would be overwritten. Two sections in different groups are two <pct-accordion> elements, which is what they are anyway.
  • No role="region" on the panels. The APG offers it and warns about landmark proliferation in the same paragraph. The platform already publishes each section as a group, so the offer is declined — a consumer who wants a landmark for one long section can put one inside their own content.
  • A closed section's content contributes no width. It is skipped for layout, not only for paint — that is how the platform closes the section — so a stack in a box that shrinks to fit its content (a flex item, an inline-block) is as wide as its headings while closed and as wide as the widest open section after a press. In normal flow the host is a block and takes its container's width; a shrink-wrapped one needs an inline-size of its own, which is what the site's demos give it (lesson-249).