ee8797c142
Co-authored-by: Copilot <copilot@github.com>
5.2 KiB
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:
- Guard
root._accInitialisedprevents double-binding after re-renders - For each
.accordion-trigger, attach aclicklistener:- 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"
- Read current state from
- Rotate
.accordion-chevronviatransform: 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
openIndexonly controls the initial server-rendered state. After the page loads, the user can open/close any item freely.- Item
TitleandContentstrings 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
idmust 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");
}