Kosmesis
Components

Accordion

A collapsible item list, single or multiple open at once, wrapping Morphos's Accordion primitive.

Installation

npx kosmesis add accordion
pnpm dlx kosmesis add accordion
yarn dlx kosmesis add accordion
bunx kosmesis add accordion

Install the following dependencies:

npm install @morphos/layout
pnpm add @morphos/layout
yarn add @morphos/layout
bun add @morphos/layout

Copy and paste the following code into your project.

accordion.tsx
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/css
pnpm add @morphos/layout @praxisjs/css
yarn add @morphos/layout @praxisjs/css
bun add @morphos/layout @praxisjs/css

Copy and paste the following code into your project.

accordion.tsx
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

Accordion

PropTypeDefault
type"single" | "multiple""single"
collapsiblebooleantrue — only relevant when type="single"
valuestring | string[]— controlled open value(s)
defaultValuestring | string[]
onValueChange(value: string | string[]) => void

AccordionItem

PropTypeDefault
accordionAccordion— required, the state instance from @State()
valuestring— required
disabledboolean

AccordionTrigger

PropTypeDefault
accordionAccordion— required
itemstring— required, matching the parent AccordionItem's value

AccordionContent

PropTypeDefault
accordionAccordion— required
itemstring— required, matching the parent AccordionItem's value

On this page