Accordion
Vertically stacked collapsible sections that let users show and hide content panels by clicking headings. Built on native `<details>`/`<summary>`.
Is it accessible?
Yes. It uses native details/summary and no JS.
Can I customize the icon?
Yes you can.
---import Accordion from '@bejamas/ui/components/Accordion.astro';---
<Accordion class="w-full max-w-[420px] self-start"> <Accordion part="item" open> <Accordion part="trigger"> Is it accessible? </Accordion> <Accordion part="content"> Yes. It uses native details/summary and no JS. </Accordion> </Accordion> <Accordion part="item"> <Accordion part="trigger"> Can I customize the icon? </Accordion> <Accordion part="content"> Yes you can. </Accordion> </Accordion></Accordion>Installation
Section titled “Installation” bunx bejamas add accordion npx bejamas add accordion pnpm dlx bejamas add accordion yarn dlx bejamas add accordion---/** * @component Accordion * @title Accordion * @description Vertically stacked collapsible sections that let users show and hide content panels by clicking headings. Built on native `<details>`/`<summary>`. * * @preview * * <Accordion class="w-full max-w-[420px] self-start"> * <Accordion part="item" open> * <Accordion part="trigger"> * Is it accessible? * </Accordion> * <Accordion part="content"> * Yes. It uses native details/summary and no JS. * </Accordion> * </Accordion> * <Accordion part="item"> * <Accordion part="trigger"> * Can I customize the icon? * </Accordion> * <Accordion part="content"> * Yes you can. * </Accordion> * </Accordion> * </Accordion> * * @usage * * ```astro * --- * import Accordion from '@/components/ui/Accordion.astro'; * --- * * <Accordion class="w-full max-w-[420px] self-start"> * <Accordion part="item" open> * <Accordion part="trigger"> * Is it accessible? * </Accordion> * <Accordion part="content"> * Yes. It uses native details/summary and no JS. * </Accordion> * </Accordion> * </Accordion> * ``` */
import { cva } from "class-variance-authority";import { cn } from "@bejamas/ui/lib/utils";import type { HTMLAttributes, HTMLTag } from "astro/types";
type AccordionPart = "root" | "item" | "trigger" | "content";
type AccordionRootProps = { part?: "root"; as?: HTMLTag; class?: string;} & HTMLAttributes<"div">;
type AccordionItemProps = { part: "item"; open?: boolean; class?: string;} & HTMLAttributes<"details">;
type AccordionTriggerProps = { part: "trigger"; class?: string;} & HTMLAttributes<"summary">;
type AccordionContentProps = { part: "content"; class?: string;} & HTMLAttributes<"div">;
type Props = | AccordionRootProps | AccordionItemProps | AccordionTriggerProps | AccordionContentProps;
const { part: rawPart, as: Tag = "div" as HTMLTag, class: className = "", ...rest} = Astro.props as Props;
const part = (rawPart ?? "root") as AccordionPart;
// Variants (shadcn-equivalent)const itemVariants = cva("border-b last:border-b-0");const triggerVariants = cva( "flex w-full items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring disabled:pointer-events-none disabled:opacity-50 marker:content-['']",);const contentOuterVariants = cva( // Keeping animation classes optional; native details doesn't provide state attributes "overflow-hidden text-sm",);---
{ part === "root" && ( <Tag {...rest} data-slot="accordion" class={cn(className)}> <slot name="item" /> <slot /> </Tag> )}
{ part === "item" && ( <details {...rest} open={(rest as any).open ?? false} slot="item" data-slot="accordion-item" class={cn(itemVariants(), "group/accordion", className)} > <slot name="trigger" /> <slot name="content" /> <slot /> </details> )}
{ part === "trigger" && ( <summary {...rest} slot="trigger" data-slot="accordion-trigger" class={cn(triggerVariants(), className)} > <span class="flex-1"> <slot /> </span> {Astro.slots.icon ? ( <span class="inline-flex items-center justify-center transition-transform duration-200 group-open/accordion:rotate-180"> <slot name="icon" /> </span> ) : ( <svg class="size-4 shrink-0 text-muted-foreground translate-y-0.5 transition-transform duration-200 group-open/accordion:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" > <path d="m6 9 6 6 6-6" /> </svg> )} </summary> )}
{ part === "content" && ( <div {...rest} slot="content" data-slot="accordion-content" class={cn(contentOuterVariants())} > <div class={cn("pt-0 pb-4", className)}> <slot /> </div> </div> )}---import Accordion from '@/components/ui/Accordion.astro';---
<Accordion class="w-full max-w-[420px] self-start"> <Accordion part="item" open> <Accordion part="trigger"> Is it accessible? </Accordion> <Accordion part="content"> Yes. It uses native details/summary and no JS. </Accordion> </Accordion></Accordion>