Components
Accordion
A collapsible item list, single or multiple open at once, wrapping Morphos's Accordion primitive.
Installation
npx kosmesis add accordionpnpm dlx kosmesis add accordionyarn dlx kosmesis add accordionbunx kosmesis add accordionInstall the following dependencies:
npm install @morphos/layoutpnpm add @morphos/layoutyarn add @morphos/layoutbun add @morphos/layoutCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Accordion as MorphosAccordion, AccordionContent as MorphosAccordionContent, AccordionItem as MorphosAccordionItem, AccordionTrigger as MorphosAccordionTrigger, type AccordionContentProps as MorphosAccordionContentProps, type AccordionItemProps as MorphosAccordionItemProps, type AccordionProps as MorphosAccordionProps, type AccordionTriggerProps as MorphosAccordionTriggerProps} from "@morphos/layout";import { cn } from "@/lib/utils";export type AccordionProps = MorphosAccordionProps;/** * Extends (not wraps) Morphos's `Accordion` so `new Accordion({ type: "single" })` still yields a * real instance with `.isOpen()`/`.toggle()` — the pattern every `AccordionItem`/`Trigger`/ * `Content` needs via their `accordion` prop. Only `render()` is overridden, to add default * classes; if you also mount `<Accordion>` via JSX (optional — Morphos primitives are usually * instantiated directly and never mounted), this is what produces the container element. */@Component()export class Accordion extends MorphosAccordion { render() { return ( <div id={this.id} class={this.class} data-type={this.type}> {this.children} </div> ); }}export type AccordionItemProps = MorphosAccordionItemProps;@Component()export class AccordionItem extends StatelessComponent<AccordionItemProps> { render() { const { class: cls, ...rest } = this.props; return <MorphosAccordionItem class={cn("border-b last:border-b-0", cls)} {...rest} />; }}export type AccordionTriggerProps = MorphosAccordionTriggerProps;@Component()export class AccordionTrigger extends StatelessComponent<AccordionTriggerProps> { render() { const { class: cls, children, ...rest } = this.props; return ( <MorphosAccordionTrigger class={cn( "flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium outline-none transition-all hover:underline focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-expanded]>svg]:rotate-180 [&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:translate-y-0.5 [&>svg]:text-muted-foreground", cls, )} {...rest} > {children} </MorphosAccordionTrigger> ); }}export type AccordionContentProps = MorphosAccordionContentProps;@Component()export class AccordionContent extends StatelessComponent<AccordionContentProps> { render() { const { class: cls, children, ...rest } = this.props; return ( <MorphosAccordionContent class="overflow-hidden text-sm" {...rest}> <div class={cn("pt-0 pb-4", cls)}>{children}</div> </MorphosAccordionContent> ); }}Install the following dependencies:
npm install @morphos/layout @praxisjs/csspnpm add @morphos/layout @praxisjs/cssyarn add @morphos/layout @praxisjs/cssbun add @morphos/layout @praxisjs/cssCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { Accordion as MorphosAccordion, AccordionContent as MorphosAccordionContent, AccordionItem as MorphosAccordionItem, AccordionTrigger as MorphosAccordionTrigger, type AccordionContentProps as MorphosAccordionContentProps, type AccordionItemProps as MorphosAccordionItemProps, type AccordionProps as MorphosAccordionProps, type AccordionTriggerProps as MorphosAccordionTriggerProps} from "@morphos/layout";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class AccordionStyles extends Stylesheet { $item = this.css({ borderBottom: `1px solid ${t.border}` }).last({ borderBottom: "none" }); $trigger = this.css({ display: "flex", flex: "1 1 0%", alignItems: "flex-start", justifyContent: "space-between", gap: "1rem", borderRadius: "0.375rem", padding: "1rem 0", textAlign: "left", fontSize: "0.875rem", fontWeight: 500, outline: "none", transition: "all 120ms ease", }) .hover({ textDecoration: "underline" }) .focusVisible({ boxShadow: `0 0 0 3px color-mix(in oklab, ${t.ring} 50%, transparent)` }) .disabled({ pointerEvents: "none", opacity: 0.5 }) .on("& > svg", { pointerEvents: "none", flexShrink: 0, width: "1rem", height: "1rem", transform: "translateY(0.125rem)", color: t.mutedForeground, transition: "transform 120ms ease" }) .on("&[data-expanded] > svg", { transform: "rotate(180deg)" }); $content = this.css({ overflow: "hidden", fontSize: "0.875rem" }); $contentInner = this.css({ paddingTop: "0", paddingBottom: "1rem" });}export type AccordionProps = MorphosAccordionProps;/** * Extends (not wraps) Morphos's `Accordion` so `new Accordion({ type: "single" })` still yields a * real instance with `.isOpen()`/`.toggle()` — the pattern every `AccordionItem`/`Trigger`/ * `Content` needs via their `accordion` prop. */@Component()export class Accordion extends MorphosAccordion { render() { return ( <div id={this.id} class={this.class} data-type={this.type}> {this.children} </div> ); }}export type AccordionItemProps = MorphosAccordionItemProps;@Component()export class AccordionItem extends StatelessComponent<AccordionItemProps> { @Styled(AccordionStyles) $s!: AccordionStyles; render() { const { class: cls, ...rest } = this.props; return <MorphosAccordionItem class={cx(this.$s.$item, cls)} {...rest} />; }}export type AccordionTriggerProps = MorphosAccordionTriggerProps;@Component()export class AccordionTrigger extends StatelessComponent<AccordionTriggerProps> { @Styled(AccordionStyles) $s!: AccordionStyles; render() { const { class: cls, children, ...rest } = this.props; return ( <MorphosAccordionTrigger class={cx(this.$s.$trigger, cls)} {...rest}> {children} </MorphosAccordionTrigger> ); }}export type AccordionContentProps = MorphosAccordionContentProps;@Component()export class AccordionContent extends StatelessComponent<AccordionContentProps> { @Styled(AccordionStyles) $s!: AccordionStyles; render() { const { class: cls, children, ...rest } = this.props; return ( <MorphosAccordionContent class={this.$s.$content} {...rest}> <div class={cx(this.$s.$contentInner, cls)}>{children}</div> </MorphosAccordionContent> ); }}Examples
Usage
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
@Component()
class FaqAccordion extends StatefulComponent {
@State() accordion = new Accordion({ type: "single", collapsible: true });
render() {
return (
<Accordion type="single" collapsible class="w-full max-w-md">
<AccordionItem accordion={this.accordion} value="item-1">
<AccordionTrigger accordion={this.accordion} item="item-1">
Is it accessible?
</AccordionTrigger>
<AccordionContent accordion={this.accordion} item="item-1">
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
}<Accordion> mounted in the tree and this.accordion are two separate objects — see the note
on Radio Group. Keep type/collapsible in sync between them.
Props
| Prop | Type | Default |
|---|---|---|
type | "single" | "multiple" | "single" |
collapsible | boolean | true — only relevant when type="single" |
defaultValue | string | string[] | — |
onValueChange | (value: string | string[]) => void | — |