Accordion
A stack of disclosures, one open at a time — and still no JavaScript.
src/components/rahti_ui/accordion.rs
Basic
One open at a time — Radix's `type="single"`, spelled as an attribute.
Product Information
Our flagship product combines cutting-edge technology with sleek design. Built with premium materials, it delivers exceptional performance and reliability.
Shipping Details
We offer worldwide shipping through trusted courier partners. Standard delivery takes 3–5 business days, while express shipping ensures delivery within 1–2 business days.
Return Policy
We stand behind our products with a comprehensive 30-day return policy. If you are not completely satisfied, simply return the item in its original condition.
shadcn's own demo. `<Accordion type="single" collapsible defaultValue="item-1">` became a shared `name` on every item and an `open` on the first — the module docs explain why the mode could not stay on the root, and it is the same reason the Resizable's handle reads its group through CSS: a component's children arrive already rendered, so a root cannot tell them anything.
Opening one closes the others, and nothing is coordinating them. That is `<details name="product">` — a radio group of disclosures, with no state, no script and no shared context.
Radix's `collapsible={false}` is not ported: it stops the open item being closed by clicking it, and a native exclusive group always allows that. shadcn's demo writes `collapsible` to opt in to the behaviour this has anyway.
Multiple
The same rows with the exclusivity taken off.
Product Information
Our flagship product combines cutting-edge technology with sleek design. Built with premium materials, it delivers exceptional performance and reliability.
Shipping Details
We offer worldwide shipping through trusted courier partners. Standard delivery takes 3–5 business days, while express shipping ensures delivery within 1–2 business days.
Return Policy
We stand behind our products with a comprehensive 30-day return policy. If you are not completely satisfied, simply return the item in its original condition.
No `name`, so every row opens independently — Radix's `type="multiple"`. It is the *absence* of the attribute rather than a different value, which is why there is no `type` prop to get wrong.
From data
Rows a page assembles rather than writes.
Product Information
Our flagship product combines cutting-edge technology with sleek design. Built with premium materials, it delivers exceptional performance and reliability.
Shipping Details
We offer worldwide shipping through trusted courier partners. Standard delivery takes 3–5 business days, while express shipping ensures delivery within 1–2 business days.
Return Policy
We stand behind our products with a comprehensive 30-day return policy. If you are not completely satisfied, simply return the item in its original condition.
The same accordion, built with `accordion_item().render(…)` and `Html::concat` over a slice. The shared `name` is written once here rather than once per row, which is the answer to the only real cost of moving `type="single"` onto the items.
Every part renders the same outside a tag as inside one, because none of them has a script to leave out — which is not true of most of this library. The chevron still comes with the trigger: it is the component's own, appended after the children, exactly as shadcn appends it.
Instant
The height transition, and the item that opts out of it.
No transition
`instant` drops the height transition. Compare it with the rows above, which glide.
With one
The transition runs on `::details-content`, the browser's own box around everything after the summary.
Animated is the default here, because shadcn's accordion animates — the opposite of the Collapsible, where shadcn ships none. The prop is spelled `instant` rather than `animated=false` because an unwritten Rahti `bool` prop is `false`, so naming it the other way round would have quietly turned every `<AccordionItem />` unanimated. The Separator's `announced` is the same trick.
shadcn's `data-[state=open]:animate-accordion-down` could not cross: `tw-animate-css` does ship those keyframes, but they end at `var(--radix-accordion-content-height, auto)` and there is no Radix here to publish it. What replaced them is `interpolate-size: allow-keywords` plus a transition on `::details-content` — progressive enhancement, so a browser without either ignores the layer and both rows behave like the first.
Disabled
A row that is drawn and does not open.
Open me
Nothing unusual here.
Not today
Never reached.
shadcn writes `disabled:pointer-events-none disabled:opacity-50` on the trigger, and neither utility can ever match on a `<summary>` — it has no `:disabled`. So they are dropped and the same two declarations are applied unconditionally when `disabled` is set, chosen in Rust where it is a `bool`.
`pointer-events-none` is doing real work rather than only styling: it is how the click is actually blocked. `tabindex="-1"` is the keyboard half — without it, `Enter` on a focused summary still opens the row — and `aria-disabled` says so out loud.
Nested
An accordion inside a row, and the chevron that knows which item is its own.
Shipping Details
We offer worldwide shipping through trusted courier partners. Standard delivery takes 3–5 business days, while express shipping ensures delivery within 1–2 business days.
Courier partners
DHL, UPS and local post.
Tracking
A number arrives by email within a day.
Return Policy
We stand behind our products with a comprehensive 30-day return policy. If you are not completely satisfied, simply return the item in its original condition.
Look at the three chevrons. The outer row is open and both inner ones are closed, and each arrow is turned by its own item — because the rotation is written `[[open]>&>svg]:rotate-180` and not `[[open]_&>svg]`. A descendant combinator would turn every chevron inside an open row.
The two groups also have two names. `name` is scoped to the document rather than to the enclosing `<Accordion>`, so an inner group sharing the outer's name would close the row it is sitting inside — which is a real trap and the reason the module docs say it twice.
Headings
What the missing `<AccordionPrimitive.Header>` cost, and how to pay it back.
Product Information
Our flagship product combines cutting-edge technology with sleek design. Built with premium materials, it delivers exceptional performance and reliability.
Shipping Details
We offer worldwide shipping through trusted courier partners. Standard delivery takes 3–5 business days, while express shipping ensures delivery within 1–2 business days.
shadcn wraps every trigger in an `<AccordionPrimitive.Header>` — an `<h3>` with the button inside it — so a screen reader can list the triggers as headings. A `<summary>` cannot be wrapped: it has to be the first child of its `<details>` or it stops being a trigger.
So the header collapses into the summary, and the `flex` it carried comes with it while `flex-1` does not — there is no header left to fill. What is lost is the heading outline, and this is how to put it back: a `<summary>`'s content model allows exactly one heading, so the `<h3>` goes inside the trigger.
Reporting to the page
Radix's `onValueChange`, as the native `toggle` event.
Product Information
This row reports itself as `product`.
Shipping Details
This row reports itself as `shipping`.
Return Policy
This row reports itself as `returns`.
The rows are exclusive through `name` and the first is `open` on the server, so the accordion is already correct in the first byte. What the page adds is one `on("toggle", …)` per row, which is the whole of Radix's `onValueChange` — the event fires after the state has changed and carries it on `target.open`.
**`open` is deliberately not bound here, and that is the interesting part.** A PulsePoint binding always *serves as its literal text*, and `open` is an HTML boolean attribute — so `open={…}` ships a page where every bound row is open, and the runtime collapses them at hydration. One Collapsible pays a flash for that; an accordion of five rows opens all five and then fires five `toggle` events that fight over the value.
So the division of labour is the other way round from React: the browser owns which row is open, and the page listens. Where a page genuinely has to *drive* it, set `details.open` from a handler rather than binding the attribute — the served markup stays right, and nothing has to be corrected on arrival.
The close branch is written `prev => prev === "…" ? "" : prev` rather than a bare empty string. Opening a row in an exclusive group closes its sibling, so two `toggle` events fire and the order is the browser's business; reading the previous value makes the result the same either way.
The port
Four class strings, three elements, and where Radix's props went.
What is gained is the Collapsible's list over again: the whole thing works with the runtime absent, find-in-page opens a closed row to show a match inside it, a print stylesheet can expand every row, and the accessibility is the browser's rather than this library's.
The boundary
The component, written out as the HTML it already was.
Written as plain HTML
No component in sight, and no difference in what it does.
The variants helpers on a page's own elements. Write the chevron yourself if you take this route — the component appends it, and the rotation in the trigger's class list is reaching for a direct `<svg>` child of the summary.
This is the clearest statement of what the Accordion is: four `data-slot`s and four class strings over markup the browser already understands. Every other component in this library loses something when it is written out by hand — the Scroll Area loses its scrollbars, the Resizable stops resizing. This one loses the chevron.