Accordion

A vertically stacked list of headers that reveal or hide associated sections of content.

This component is still in Beta status. This means that it is ready for production, but the API might change.

"Qwik UI's Accordion implementation follows the WAI-Aria design pattern, along with some additional API's that enhance the flexibility, types, and performance."

Features

Provided by Qwik UI.

  • ✨ Full keyboard navigation
  • ✨ Single or Multi Accordion
  • ✨ Controlled or uncontrolled
  • ✨ Animatable, dynamic, and resumable

Building blocks

accordion.tsx

import { component$ } from "@builder.io/qwik";
import { Accordion } from 'senzui'

const Component = component$(() => {
  return (
    <Accordion>
      <Accordion.Item>
        <Accordion.Header>
          <Accordion.Trigger></Accordion.Trigger>
        </Accordion.Header>
        <Accordion.Content></Accordion.Content>
      </Accordion.Item>
    </Accordion>
  )
})

Anatomy

ComponentDescription
AccordionThe root container for the accordion.
Accordion.Item

An item for accordion, inside the root. It contains the item's heading, trigger, and content.

Accordion.Header

An HTML heading element. The default is set to h3, can be changed with the as prop.

Accordion.Trigger

Toggles the corresponding content when clicked or activated. It should be placed within the Accordion.Header component.

Accordion.Content

Contains the content associated with an item when clicking on its respective trigger.

API

You can read more about this component's API on Qwik UI's documentation.

Manual install

If you only want this styled component to be installed, follow these steps:

Install dependencies:
pnpm add clsx tailwind-merge @qwik-ui/headless
Add the following code to your components/accordion.tsx:
components/accordion.tsx
import {
  Slot,
  type QwikIntrinsicElements,
  component$,
  useStylesScoped$,
} from "@builder.io/qwik";
import {
  AccordionItem,
  AccordionRoot,
  AccordionTrigger,
  AccordionContent,
  AccordionHeader,
} from "@qwik-ui/headless";
import type {
  AccordionHeaderProps,
  AccordionTriggerProps,
  AccordionRootProps,
  AccordionItemProps,
} from "@qwik-ui/headless";
import twMerge from "tailwind-merge";
import clsx from "clsx";

function sz(...classes) {
  return twMerge(clsx(...classes));
}

type StyledAccordionRootProps = {
  defaultAnimation?: boolean;
} & AccordionRootProps;

const StyledAccordionRoot = ({ ...props }: StyledAccordionRootProps) => {
  const {
    defaultAnimation = false,
    class: className,
    animated,
    ...rest
  } = props;

  return (
    <AccordionRoot
      animated={animated}
      class={sz(
        animated &&
          defaultAnimation &&
          "[&>div>div[role=region][data-state=closed]]:[animation:.5s_cubic-bezier(.87,0,.13,1)_0s_1_normal_forwards_accordion-close] [&>div>div[role=region][data-state=open]]:[animation:.5s_cubic-bezier(.87,0,.13,1)_0s_1_normal_forwards_accordion-open]",
        className
      )}
      {...rest}
    />
  );
};

const StyledAccordionItem = ({ ...props }: AccordionItemProps) => {
  const { class: classNames, ...rest } = props;

  return (
    <AccordionItem
      class={sz(
        "border-b border-senzui-150 pb-1 pt-3 last:border-b-0 dark:border-senzui-750 dark:text-senzui-200",
        classNames
      )}
      {...rest}
    />
  );
};

const StyledAccordionTrigger = ({ ...props }: AccordionTriggerProps) => {
  const { class: classNames, ...rest } = props;

  return (
    <AccordionTrigger
      class={sz(
        "group mb-2 flex w-full items-center justify-between overflow-hidden text-senzui-650 outline-none outline hover:text-senzui-900 focus:font-medium focus:text-senzui-900 focus-visible:outline-theme dark:text-senzui-200 dark:hover:text-senzui-50 dark:focus:text-senzui-50 dark:focus-visible:outline-theme",
        classNames
      )}
      {...rest}
    />
  );
};

const StyledAccordionContent = component$(
  ({ ...props }: QwikIntrinsicElements["div"]) => {
    const { class: classNames, ...rest } = props;

    useStylesScoped$(`
      [hidden] {
        display: none
      }
    `);

    return (
      <AccordionContent
        class={sz(
          "rounded-md-md overflow-hidden text-sm text-senzui-500 dark:text-senzui-450",
          classNames
        )}
        {...rest}
      >
        <div class="pb-4 pt-1">
          <Slot />
        </div>
      </AccordionContent>
    );
  }
);

const StyledAccordionHeader = ({ ...props }: AccordionHeaderProps) => {
  return <AccordionHeader {...props} />;
};

const Accordion = StyledAccordionRoot as typeof StyledAccordionRoot & {
  Item: typeof StyledAccordionItem;
  Trigger: typeof StyledAccordionTrigger;
  Content: typeof StyledAccordionContent;
  Header: typeof StyledAccordionHeader;
};

Accordion.Item = StyledAccordionItem;
Accordion.Trigger = StyledAccordionTrigger;
Accordion.Content = StyledAccordionContent;
Accordion.Header = StyledAccordionHeader;

export default Accordion;