UIDex

Accordion

الأقسام المطوية

aria-expanded + aria-controls

also calledcollapsible، collapse، expansion panel، disclosure group، expander، أكورديون، قوائم قابلة للطيّ، أقسام قابلة للتوسيع، لوحات مطوية، كولابس

An accordion is a vertical stack of headers, each one a button that expands or collapses a panel of content directly beneath it. The unit is the item, meaning a header plus its own panel, and the group as a whole is governed by one policy: exactly one panel open at a time, or several at once. It is not tabs. Tabs are peers sharing one fixed-height frame that shows a single panel and can never show two together, while an expanding accordion item pushes everything below it down and changes the height of the page itself. A dropdown is not the same thing either: it floats in a layer above the page and hands the page back untouched when it closes, whereas an accordion panel lives inside the content flow and reflows what surrounds it. Nor is it a single disclosure, which is one standalone button revealing one block, with no siblings and no policy coordinating them. The details and summary elements are the browser's own implementation: they give you the behaviour and the accessibility semantics without a line of JavaScript, but animating their height is painful because the browser swaps state in a single frame. As a practical rule, never fold away content the reader has to read anyway. Collapsing is for secondary or repetitive material such as FAQs and order details.

If you called it…

"the FAQ questions that open when you click them""the sections that expand and push the page down""the plus sign that turns into a minus""the list of headers with hidden text under each one""the thing where opening one closes the other"

Live specimen

Interact with the demo. Every part is real and numbered.

التبويبات إطار ثابت الارتفاع؛ القسم يدفع ما تحته.

يعطيك السلوك والوصولية بلا JavaScript، لكن تحريكه متعب.

الأول فقط، أو لا شيء إذا كفت العناوين وحدها.

RTLالمؤشّر يدور رأسياً فلا ينعكس، الذي ينعكس هو موضعه

Anatomy: every part, named

Hover a row to locate it in the demo above.

Build prompt

Build an accordion. Each item is a header inside an h3 holding a <button> with aria-expanded and an aria-controls value equal to a panel id that actually exists, and the panel carries aria-labelledby pointing back at the button; generate the ids with useId so they never collide when the component appears twice on one page. Make the button span the full header width as a flex row with justify-content: space-between, with the indicator last in DOM order and aria-hidden. Animate the height with grid-template-rows from 0fr to 1fr over an inner wrapper that has overflow: hidden, or with a max-height measured from scrollHeight. Never try to transition height: auto. Keep the header button outside the clipped area so the focus ring is not cut off. Make the policy a prop: one panel open or several, with closing everything allowed. When collapsed, take the panel content out of the tab order with inert, hidden, or hidden="until-found". Wire ArrowDown, ArrowUp, Home and End to move between headers, and do not swap them in RTL. Use logical properties throughout: padding-inline-start for nested indentation, padding-inline for panel padding, border-inline-start for the nesting guide line, inset-inline-end for any absolute placement; mirror a sideways chevron with scaleX(-1) under [dir="rtl"], never mirror one that rotates vertically, and drive the rotation with the standalone rotate property rather than transform so the two rules cannot overwrite each other. Disable the animation under prefers-reduced-motion.

How it behaves right-to-left

This section is ours alone.

Where the indicator sits

mirrors

The indicator belongs at the logical end of the header: right in English, left in Arabic. Make the header a flex row with justify-content: space-between and put the indicator last in DOM order, and it moves on its own when the page flips, with no extra rule. A position: absolute; right: 12px leaves it sitting on top of the Arabic text, so use inset-inline-end: 12px instead.

A chevron that rotates

never mirrors

A chevron that points down while collapsed and rotates 180° on open is completely direction-neutral; never mirror it. Any angle other than 180° needs more care. If the collapsed state is rotate(-90deg), scaleX(-1) on top of it reverses the sign of the angle, so the icon spins the other way and settles upside down. The second trap is that transform is a single non-accumulating property: a transform: rotate(180deg) rule for the open state wipes out a transform: scaleX(-1) rule completely, so either combine them in one declaration or use the standalone rotate and scale properties, which do compose.

A sideways indicator

mirrors

If the indicator points sideways while collapsed it is a purely directional icon that must point left in Arabic, but how you flip it depends on how it is drawn. An SVG path is geometry, not text, so the direction property never touches it and you have to flip it explicitly with transform: scaleX(-1) under [dir="rtl"] or :dir(rtl). A literal › character, by contrast, is Bidi_Mirrored in Unicode, so the browser already draws it reversed inside an rtl paragraph and adding scaleX(-1) flips it back to the wrong side. An icon-font glyph lives in the Unicode Private Use Area, which carries no mirroring property at all, so it never flips on its own.

Indentation and padding

mirrors

A nested accordion indents child headers from the parent header by a fixed step. Use padding-inline-start on the header instead of padding-left, or the indent lands on the wrong edge: the children look perfectly flush with their parent and the gap shows up at the end of the line instead. The same rule covers panel padding (padding-inline, not padding-left/right) and the vertical guide line that signals nesting, where border-inline-start replaces border-left so the line stands beside the content instead of behind it.

The navigation keys

never mirrors

The accordion axis is vertical, so its keyboard model does not mirror: ArrowDown and ArrowUp still move between headers and Home/End still jump to the first and last, whatever the page direction. ArrowLeft and ArrowRight do flip, if you support them: in horizontal orientation (orientation="horizontal") or in a tree-style pattern where one arrow opens the item and the other closes it. Because event.key reports the physical key and knows nothing about direction, read the direction yourself with getComputedStyle(el).direction or el.matches(":dir(rtl)") and swap the two handlers, or pass dir="rtl" to the Radix root and let it do the swap.

In code

Each row is one framework's word for the same thing. Take the row your project speaks.

ARIAaria-expanded + aria-controlsThe pair that defines the pattern: state on the button, wiring to the panel by matching id.
HTML<details name="faq"><summary>The native version; sharing one name across several details makes an exclusive accordion with no JavaScript.
HTMLhidden="until-found"Keeps a collapsed panel findable by in-page search, and opens the section when the match is hit.
CSSgrid-template-rows: 0fr → 1frThe practical way to animate an unknown height, over an inner wrapper with overflow: hidden.
CSSpadding-inline-startThe logical property that moves nested indentation to the correct edge when direction flips.
Radix UI<Accordion.Root type="single" collapsible>type sets the policy, and the panel height is exposed as --radix-accordion-content-height.
Ant Design<Collapse accordion items={items} />Ant calls the component Collapse; the accordion boolean is the single-open policy.

See also

Updated · 2026-08-19