# Accordion An expand/collapse panel list. Items are collapsed by default; one item can be pre-expanded at server render time. Client-side toggle is handled by `components.js`. --- ## HTML structure ``` div.accordion-root[id] ← outer wrapper div.accordion-item ← one per item, border-b separator h3 button.accordion-trigger ← clickable header; aria-expanded tracks state {title text} svg.accordion-chevron ← rotates 180° when open div.accordion-panel ← collapsible area; height/opacity driven by JS div.pb-4 {content} ``` --- ## CSS mechanics | Class / property | Effect | |---|---| | `overflow-hidden` on panel | Prevents content leaking outside the panel during animation | | `transition-all duration-200` on panel | Smooth height and opacity animation | | `height: 0; opacity: 0` (collapsed) | Starting state set server-side for closed items | | `height: auto; opacity: 1` (open) | Starting state for the pre-expanded item | | `accordion-chevron` + JS `rotate(180deg)` | Chevron rotates down when expanded | --- ## JavaScript (`initAccordion` in `components.js`) Runs on `DOMContentLoaded` and on `htmx:afterSwap` so HTMX-swapped accordions are correctly initialized. **Per-instance initialization:** 1. Guard `root._accInitialised` prevents double-binding after re-renders 2. For each `.accordion-trigger`, attach a `click` listener: - Read current state from `aria-expanded` - If currently open → set `panel.style.height = "0"`, `opacity = "0"`, `aria-expanded = "false"` - If currently closed → set `panel.style.height = scrollHeight + "px"`, `opacity = "1"`, `aria-expanded = "true"` 3. Rotate `.accordion-chevron` via `transform: rotate(180deg)` when open --- ## Constructor signature ```csharp public Accordion( string id, IEnumerable<(string Title, string Content)> items, int openIndex = -1) ``` | Parameter | Description | |---|---| | `id` | Unique element id for the root `div` | | `items` | List of `(Title, Content)` tuples | | `openIndex` | Zero-based index of the pre-expanded item; `-1` = all closed | --- ## Usage examples ### All closed ```csharp new Accordion( id: "faq", items: new[] { ("What is this?", "A fast HTMX app framework."), ("Is it AOT-safe?", "Yes, fully."), ("Do I need Node?", "Only to run the Tailwind build step."), }) ``` ### One pre-expanded ```csharp new Accordion( id: "setup-steps", items: new[] { ("Step 1 — Install", "Run npm install in the project folder."), ("Step 2 — Configure", "Edit appsettings.json with your connection string."), ("Step 3 — Run", "Use dotnet run to start the server."), }, openIndex: 0) ``` ### HTML content in items ```csharp new Accordion( id: "code-examples", items: new[] { ("C# snippet", "
var x = 1 + 1;
"), ("Tip", "

Use AOT-safe serialization patterns.

"), }) ``` --- ## Tips and tricks - `openIndex` only controls the initial server-rendered state. After the page loads, the user can open/close any item freely. - Item `Title` and `Content` strings are inserted as raw HTML — HTML-encode any user-supplied values before passing them in. - Multiple items can be opened simultaneously by the user — there is no "only one open at a time" constraint in the JS. - If you need to identify which accordion is which after a click, listen to the parent element and inspect `event.target.closest('.accordion-item')`. - The `id` must be unique on the page if you place more than one accordion. --- ## Complete page example **`Templates/FaqPage.htmx`** ```html

Frequently Asked Questions

Everything you need to know about BeepBoop.

$$FaqAccordion$$
``` **`Templates/FaqPage.htmx.cs`** ```csharp namespace Htmx.ApiDemo.Templates; public sealed class FaqPage : FaqPageBase { private readonly IHtmxComponent _faq; public FaqPage() { _faq = new Components.Accordion( id: "faq", items: new[] { ("What is BeepBoop?", "A fast, AOT-safe HTMX web framework built on .NET 10."), ("Do I need Node.js?", "Only to run the Tailwind CSS build step during development."), ("Is MongoDB required?", "No — swap in any data store you prefer."), ("How do I deploy?", "Run dotnet publish -c Release for a native AOT binary."), }); } protected override void RenderFaqAccordion(HtmxRenderContext ctx) => _faq.Render(ctx.Next()); } ``` **`Templates/FaqPage.htmx.cs` — GET handler** ```csharp [Handler] [MapGet("/faq")] public static partial class GetFaqHandler { public record Query(); private static Task HandleAsync( Query _, HttpContext ctx, CancellationToken ct) => ctx.WriteHtmxPage(new FaqPage(), title: "FAQ"); } ```