Files
Htmx/docs/Components/Accordion.md
T
2026-05-04 19:57:48 +05:00

5.2 KiB

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

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

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

new Accordion(
    id: "setup-steps",
    items: new[]
    {
        ("Step 1 — Install",  "Run <code>npm install</code> in the project folder."),
        ("Step 2 — Configure", "Edit <code>appsettings.json</code> with your connection string."),
        ("Step 3 — Run",      "Use <code>dotnet run</code> to start the server."),
    },
    openIndex: 0)

HTML content in items

new Accordion(
    id: "code-examples",
    items: new[]
    {
        ("C# snippet", "<pre><code>var x = 1 + 1;</code></pre>"),
        ("Tip",        "<p>Use <strong>AOT-safe</strong> serialization patterns.</p>"),
    })

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

<div class="max-w-2xl mx-auto py-10">
  <h1 class="text-2xl font-bold mb-2">Frequently Asked Questions</h1>
  <p class="text-muted-foreground mb-8">Everything you need to know about BeepBoop.</p>
  $$FaqAccordion$$
</div>

Templates/FaqPage.htmx.cs

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 <code>dotnet publish -c Release</code> for a native AOT binary."),
            });
    }

    protected override void RenderFaqAccordion(HtmxRenderContext ctx)
        => _faq.Render(ctx.Next());
}

Templates/FaqPage.htmx.cs — GET handler

[Handler]
[MapGet("/faq")]
public static partial class GetFaqHandler
{
    public record Query();

    private static Task<IResult> HandleAsync(
        Query _,
        HttpContext ctx,
        CancellationToken ct)
        => ctx.WriteHtmxPage(new FaqPage(), title: "FAQ");
}