Accordion
A modal dialog that interrupts the user with important content and expects a response.
Preview
<script setup lang="ts">
import {
Accordion,
AccordionItem,
AccordionTrigger,
AccordionContent,
} from "~/components/ui/accordion";
</script>
<template>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-2">
<AccordionTrigger>Is it styled?</AccordionTrigger>
<AccordionContent>
Yes. It comes with default styles that matches the other
components' aesthetic.
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-3">
<AccordionTrigger>Is it animated?</AccordionTrigger>
<AccordionContent>
Yes. It's animated by default, but you can disable it if you
prefer.
</AccordionContent>
</AccordionItem>
</Accordion>
</template>
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>