Skip to content
edmi-ui

Accordion

Stacked headings that each open a section; one at a time by default, multiple for many.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

A token that tracks a basket of xStocks. One join buys every token in the basket at its weight.

A token that tracks a basket of xStocks. One join buys every token in the basket at its weight.

A token that tracks a basket of xStocks. One join buys every token in the basket at its weight.
Terminal window
npx shadcn@latest add @edmi-ui/accordion
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/accordion
Terminal window
yarn dlx shadcn@latest add @edmi-ui/accordion
Terminal window
bunx shadcn@latest add @edmi-ui/accordion

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/accordion
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/accordion
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/accordion
Terminal window
bunx shadcn-vue@latest add @edmi-ui/accordion

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/accordion.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/accordion.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/accordion.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/accordion.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
<Accordion defaultValue={["index"]} className="max-w-md">
<AccordionItem value="index">
<AccordionTrigger>What is a tokenized index?</AccordionTrigger>
<AccordionContent>
A token that tracks a basket of xStocks. One join buys every token in
the basket at its weight.
</AccordionContent>
</AccordionItem>
<AccordionItem value="rebalance">
<AccordionTrigger>How is it rebalanced?</AccordionTrigger>
<AccordionContent>Weekly, by the index agent.</AccordionContent>
</AccordionItem>
<AccordionItem value="cost">
<AccordionTrigger>What does it cost?</AccordionTrigger>
<AccordionContent>A 0.4% annual management fee.</AccordionContent>
</AccordionItem>
</Accordion>
<script setup lang="ts">
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
</script>
<template>
<Accordion type="single" collapsible default-value="index" class="max-w-md">
<AccordionItem value="index">
<AccordionTrigger>What is a tokenized index?</AccordionTrigger>
<AccordionContent>
A token that tracks a basket of xStocks. One join buys every token in
the basket at its weight.
</AccordionContent>
</AccordionItem>
<AccordionItem value="rebalance">
<AccordionTrigger>How is it rebalanced?</AccordionTrigger>
<AccordionContent>Weekly, by the index agent.</AccordionContent>
</AccordionItem>
<AccordionItem value="cost">
<AccordionTrigger>What does it cost?</AccordionTrigger>
<AccordionContent>A 0.4% annual management fee.</AccordionContent>
</AccordionItem>
</Accordion>
</template>
<script lang="ts">
import * as Accordion from "$lib/components/ui/accordion";
</script>
<Accordion.Root type="single" value="index" class="max-w-md">
<Accordion.Item value="index">
<Accordion.Trigger>What is a tokenized index?</Accordion.Trigger>
<Accordion.Content>
A token that tracks a basket of xStocks. One join buys every token in the basket at its weight.
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="rebalance">
<Accordion.Trigger>How is it rebalanced?</Accordion.Trigger>
<Accordion.Content>Weekly, by the index agent.</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="cost">
<Accordion.Trigger>What does it cost?</Accordion.Trigger>
<Accordion.Content>A 0.4% annual management fee.</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
registry/ui/accordion.tsx
import { Accordion as AccordionPrimitive } from "@base-ui/react/accordion";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "cn";
import { CaretDownIcon } from "@phosphor-icons/react";
// `multiple` and array `defaultValue` come straight from Base UI (DESIGN §5).
// ✦ `variant="card"`: items live inside a raised card with hairline dividers.
const accordionVariants = cva("flex w-full flex-col", {
variants: {
variant: {
default: "",
card: "rounded-xl border border-border bg-card px-4",
},
},
defaultVariants: { variant: "default" },
});
function Accordion({
className,
variant = "default",
...props
}: AccordionPrimitive.Root.Props & VariantProps<typeof accordionVariants>) {
return (
<AccordionPrimitive.Root
data-slot="accordion"
data-variant={variant}
className={cn(accordionVariants({ variant }), className)}
{...props}
/>
);
}
function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
return (
<AccordionPrimitive.Item
data-slot="accordion-item"
className={cn("border-b border-border last:border-b-0", className)}
{...props}
/>
);
}
function AccordionTrigger({
className,
children,
...props
}: AccordionPrimitive.Trigger.Props) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
"group/accordion-trigger relative flex flex-1 items-center justify-between gap-4 py-3.5 text-left text-[14px] font-medium outline-none transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
{children}
<CaretDownIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[panel-open]/accordion-trigger:rotate-180"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}
function AccordionContent({
className,
children,
...props
}: AccordionPrimitive.Panel.Props) {
return (
<AccordionPrimitive.Panel
data-slot="accordion-content"
className="h-(--accordion-panel-height) overflow-hidden text-[13.5px] leading-relaxed text-muted-foreground transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0"
{...props}
>
<div
className={cn(
"pb-4 [&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4",
className,
)}
>
{children}
</div>
</AccordionPrimitive.Panel>
);
}
export {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
accordionVariants,
};
registry/ui/accordion/Accordion.vue
<script setup lang="ts">
import type { AccordionRootEmits, AccordionRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { AccordionVariants } from '.'
import { reactiveOmit } from '@vueuse/core'
import { AccordionRoot, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/registry/edmi/lib/utils'
import { accordionVariants } from '.'
// Reka needs `type="single" | "multiple"` (React: `multiple` prop). ✦ `variant="card"` raises the list into a card.
const props = withDefaults(
defineProps<AccordionRootProps & { class?: HTMLAttributes['class'], variant?: AccordionVariants['variant'] }>(),
{ variant: 'default' },
)
const emits = defineEmits<AccordionRootEmits>()
const delegatedProps = reactiveOmit(props, 'class', 'variant')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<AccordionRoot
v-slot="slotProps"
data-slot="accordion"
:data-variant="variant"
v-bind="forwarded"
:class="cn(accordionVariants({ variant }), props.class)"
>
<slot v-bind="slotProps" />
</AccordionRoot>
</template>
registry/ui/accordion/AccordionContent.vue
<script setup lang="ts">
import type { AccordionContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AccordionContent } from 'reka-ui'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AccordionContent
data-slot="accordion-content"
v-bind="delegatedProps"
class="overflow-hidden text-[13.5px] leading-relaxed text-muted-foreground data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
>
<div
:class="cn('pb-4 [&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4', props.class)"
>
<slot />
</div>
</AccordionContent>
</template>
registry/ui/accordion/AccordionItem.vue
<script setup lang="ts">
import type { AccordionItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { AccordionItem, useForwardProps } from 'reka-ui'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<AccordionItem
v-slot="slotProps"
data-slot="accordion-item"
v-bind="forwardedProps"
:class="cn('border-b border-border last:border-b-0', props.class)"
>
<slot v-bind="slotProps" />
</AccordionItem>
</template>
registry/ui/accordion/AccordionTrigger.vue
<script setup lang="ts">
import type { AccordionTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { ChevronDown } from '@lucide/vue'
import { reactiveOmit } from '@vueuse/core'
import { AccordionHeader, AccordionTrigger } from 'reka-ui'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<AccordionHeader class="flex">
<AccordionTrigger
data-slot="accordion-trigger"
v-bind="delegatedProps"
:class="
cn(
'group/accordion-trigger relative flex flex-1 items-center justify-between gap-4 py-3.5 text-left text-[14px] font-medium outline-none transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
props.class,
)
"
>
<slot />
<slot name="icon">
<ChevronDown
data-slot="accordion-trigger-icon"
class="pointer-events-none size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]/accordion-trigger:rotate-180"
/>
</slot>
</AccordionTrigger>
</AccordionHeader>
</template>
registry/ui/accordion/index.ts
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
export { default as Accordion } from "./Accordion.vue";
export { default as AccordionContent } from "./AccordionContent.vue";
export { default as AccordionItem } from "./AccordionItem.vue";
export { default as AccordionTrigger } from "./AccordionTrigger.vue";
// ✦ `variant="card"`: items live inside a raised card with hairline dividers.
export const accordionVariants = cva("flex w-full flex-col", {
variants: {
variant: {
default: "",
card: "rounded-xl border border-border bg-card px-4",
},
},
defaultVariants: { variant: "default" },
});
export type AccordionVariants = VariantProps<typeof accordionVariants>;
src/lib/registry/ui/accordion/accordion.svelte
<script lang="ts" module>
import { tv, type VariantProps } from "tailwind-variants";
// ✦ `variant="card"`: items live inside a raised card with hairline dividers.
export const accordionVariants = tv({
base: "flex w-full flex-col",
variants: {
variant: {
default: "",
card: "rounded-xl border border-border bg-card px-4",
},
},
defaultVariants: { variant: "default" },
});
export type AccordionVariant = VariantProps<typeof accordionVariants>["variant"];
</script>
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(),
variant = "default",
class: className,
...restProps
}: AccordionPrimitive.RootProps & { variant?: AccordionVariant } = $props();
</script>
<AccordionPrimitive.Root
bind:ref
bind:value={value as never}
data-slot="accordion"
data-variant={variant}
class={cn(accordionVariants({ variant }), className)}
{...restProps}
/>
src/lib/registry/ui/accordion/accordion-content.svelte
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithoutChild<AccordionPrimitive.ContentProps> = $props();
</script>
<AccordionPrimitive.Content
bind:ref
data-slot="accordion-content"
class="overflow-hidden text-[13.5px] leading-relaxed text-muted-foreground data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...restProps}
>
<div
class={cn(
"pb-4 [&_a]:text-foreground [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4",
className
)}
>
{@render children?.()}
</div>
</AccordionPrimitive.Content>
src/lib/registry/ui/accordion/accordion-item.svelte
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: AccordionPrimitive.ItemProps = $props();
</script>
<AccordionPrimitive.Item
bind:ref
data-slot="accordion-item"
class={cn("border-b border-border last:border-b-0", className)}
{...restProps}
/>
src/lib/registry/ui/accordion/accordion-trigger.svelte
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import CaretDownIcon from "phosphor-svelte/lib/CaretDown";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
level = 3,
children,
...restProps
}: WithoutChild<AccordionPrimitive.TriggerProps> & {
level?: AccordionPrimitive.HeaderProps["level"];
} = $props();
</script>
<AccordionPrimitive.Header {level} class="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
bind:ref
class={cn(
"group/accordion-trigger relative flex flex-1 items-center justify-between gap-4 py-3.5 text-left text-[14px] font-medium outline-none transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...restProps}
>
{@render children?.()}
<CaretDownIcon
data-slot="accordion-trigger-icon"
class="pointer-events-none size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-[state=open]/accordion-trigger:rotate-180"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
src/lib/registry/ui/accordion/index.ts
import Root, {
type AccordionVariant,
accordionVariants,
} from "./accordion.svelte";
import Content from "./accordion-content.svelte";
import Item from "./accordion-item.svelte";
import Trigger from "./accordion-trigger.svelte";
export {
type AccordionVariant,
accordionVariants,
Content,
Content as AccordionContent,
Item,
Item as AccordionItem,
Root,
//
Root as Accordion,
Trigger,
Trigger as AccordionTrigger,
};
StackBlitz ↗StackBlitz ↗StackBlitz ↗

How the vault entity is formed.

Answer holder questions with the agent, using the same mandate context.

How the vault entity is formed.

Answer holder questions with the agent, using the same mandate context.
How the vault entity is formed.
Answer holder questions with the agent, using the same mandate context.
Prop Type Default
multiple boolean false
defaultValue / value string[] []
variant ✦ "default" | "card" "default"
AccordionItem value string —

Anatomy: Accordion, AccordionItem, AccordionTrigger, AccordionContent.

  • variant="card": items inside a bordered card with hairline dividers.
  • Vue (Reka UI) and Svelte (Bits UI): type="single" | "multiple" (plus collapsible) replaces React’s multiple boolean.
  • Svelte: the root value is a string when type="single" and a string[] when type="multiple".