Accordion

A modal dialog that interrupts the user with important content and expects a response.

Preview

Installation

Copy and paste this into your project

// ~/components/ui/accordion.tsx

import {
  Accordion,
  AccordionTrigger as AccordionTriggerPrimitive,
  AccordionItem as AccordionItemPrimitive,
  AccordionContent as AccordionContentPrimitive,
  AccordionItemProps,
} from "@ark-ui/vue";
import { ExtendProps, cn } from "~/lib/utils";
import { defineComponent, ref, onMounted, onBeforeUnmount } from "vue";

const AccordionItem = defineComponent({
  props: {} as ExtendProps<AccordionItemProps>,
  setup(props, { slots, attrs }) {
    return () => (
      <AccordionItemPrimitive
        class={cn("border-b", attrs.class ?? "")}
        {...attrs}
        {...props}
      >
        {slots.default?.()}
      </AccordionItemPrimitive>
    );
  },
});

const AccordionTrigger = defineComponent({
  setup(_, { slots, attrs }) {
    return () => (
      <AccordionTriggerPrimitive>
        <button
          class={cn(
            "w-full flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
            attrs.class ?? ""
          )}
          {...attrs}
        >
          {slots.default?.()}
        </button>
      </AccordionTriggerPrimitive>
    );
  },
});

const AccordionContent = defineComponent({
  setup(_, { slots, attrs }) {
    const content = ref<HTMLElement | null>(null);
    const container = ref<HTMLElement | null>(null);

    const updateContentHeight = () => {
      if (!content.value || !container.value) return;

      container.value.style.setProperty("height", "auto");
      const contentHeight = getComputedStyle(content.value).height;

      container.value.style.setProperty("--content-height", contentHeight);

      content.value.style.setProperty("height", null);
    };

    onMounted(() => {
      updateContentHeight();
      window.addEventListener("resize", updateContentHeight);
    });

    onBeforeUnmount(() => {
      window.removeEventListener("resize", updateContentHeight);
    });

    return () => (
      <div ref={container}>
        <AccordionContentPrimitive
          class={cn(
            "overflow-hidden block text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down h-0 data-[expanded]:h-[var(--content-height)] duration-300 ease-in-out",
            attrs.class ?? ""
          )}
          {...attrs}
        >
          <div class="pb-4 pt-0" ref={content}>
            {slots.default?.()}
          </div>
        </AccordionContentPrimitive>
      </div>
    );
  },
});

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

Usage

import {
  Accordion,
  AccordionItem,
  AccordionTrigger,
  AccordionContent,
} from "~/components/ui/accordion";
<Accordion type="single" collapsible>
  <AccordionItem value="item-1">
    <AccordionTrigger>Is it accessible?</AccordionTrigger>
    <AccordionContent>
      Yes. It adheres to the WAI-ARIA design pattern.
    </AccordionContent>
  </AccordionItem>
</Accordion>