` by default.
+
+All parts accept a `render` prop for polymorphic rendering and standard HTML attributes for their default element.
+
+## Keyboard Navigation
+
+| Key | Action |
+| ----------------- | ------------------------------ |
+| `ArrowDown` | Move focus to next trigger |
+| `ArrowUp` | Move focus to previous trigger |
+| `Enter` / `Space` | Toggle the focused item |
+
+## Data Attributes
+
+| Attribute | Applies To | Description |
+| ------------------ | -------------------- | ------------------------------------------------- |
+| `data-cl-slot` | All parts | Identifies each part (e.g. `"accordion-trigger"`) |
+| `data-cl-open` | Item, Trigger, Panel | Present when the item is expanded |
+| `data-cl-closed` | Item, Trigger, Panel | Present when the item is collapsed |
+| `data-cl-disabled` | Item, Trigger | Present when the item is disabled |
+
+## CSS Animation
+
+`Accordion.Panel` exposes a `--cl-accordion-panel-height` CSS custom property set to the panel's `scrollHeight` in pixels. Use this for height-based expand/collapse animations:
+
+```css
+[data-cl-slot='accordion-panel'] {
+ overflow: hidden;
+ height: var(--cl-accordion-panel-height);
+ transition: height 200ms ease;
+}
+[data-cl-slot='accordion-panel'][data-cl-closed] {
+ height: 0;
+}
+```
+
+The panel suppresses the enter animation on initial mount — only subsequent opens animate.
+
+## ARIA
+
+- Trigger: `aria-expanded`, `aria-controls` (pointing to its panel), `aria-disabled`
+- Panel: `role="region"`, `aria-labelledby` (pointing to its trigger)
diff --git a/packages/headless/src/primitives/accordion/accordion.test.tsx b/packages/headless/src/primitives/accordion/accordion.test.tsx
new file mode 100644
index 00000000000..1a3ac94482a
--- /dev/null
+++ b/packages/headless/src/primitives/accordion/accordion.test.tsx
@@ -0,0 +1,419 @@
+import { cleanup, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import { axe } from '../../test-utils/axe';
+import { Accordion } from './accordion';
+
+afterEach(() => cleanup());
+
+function renderAccordion(props: Partial
> = {}) {
+ return render(
+
+
+
+ Section 1
+
+ Content 1
+
+
+
+ Section 2
+
+ Content 2
+
+
+
+ Section 3
+
+ Content 3
+
+ ,
+ );
+}
+
+describe('Accordion', () => {
+ describe('slot attributes', () => {
+ it('renders root with data-cl-slot', () => {
+ renderAccordion();
+ expect(document.querySelector('[data-cl-slot="accordion-root"]')).toBeInTheDocument();
+ });
+
+ it('renders items with data-cl-slot', () => {
+ renderAccordion();
+ const items = document.querySelectorAll('[data-cl-slot="accordion-item"]');
+ expect(items).toHaveLength(3);
+ });
+
+ it('renders headers with data-cl-slot', () => {
+ renderAccordion();
+ const headers = document.querySelectorAll('[data-cl-slot="accordion-header"]');
+ expect(headers).toHaveLength(3);
+ });
+
+ it('renders triggers with data-cl-slot', () => {
+ renderAccordion();
+ const triggers = document.querySelectorAll('[data-cl-slot="accordion-trigger"]');
+ expect(triggers).toHaveLength(3);
+ });
+
+ it('renders panels with data-cl-slot when open', () => {
+ renderAccordion({ defaultValue: ['item1'] });
+ expect(document.querySelector('[data-cl-slot="accordion-panel"]')).toBeInTheDocument();
+ });
+ });
+
+ describe('expand/collapse', () => {
+ it('opens an item on trigger click', async () => {
+ const user = userEvent.setup();
+ renderAccordion();
+
+ await user.click(screen.getByRole('button', { name: 'Section 1' }));
+
+ const item = document.querySelectorAll('[data-cl-slot="accordion-item"]')[0];
+ expect(item).toHaveAttribute('data-cl-open', '');
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
+ });
+
+ it('closes an open item on trigger click', async () => {
+ const user = userEvent.setup();
+ renderAccordion({ defaultValue: ['item1'] });
+
+ await user.click(screen.getByRole('button', { name: 'Section 1' }));
+
+ const item = document.querySelectorAll('[data-cl-slot="accordion-item"]')[0];
+ expect(item).toHaveAttribute('data-cl-closed', '');
+ });
+
+ it('calls onValueChange when toggled', async () => {
+ const onValueChange = vi.fn();
+ const user = userEvent.setup();
+ renderAccordion({ onValueChange });
+
+ await user.click(screen.getByRole('button', { name: 'Section 1' }));
+
+ expect(onValueChange).toHaveBeenCalledWith(['item1']);
+ });
+
+ it('allows multiple items open in multiple mode', async () => {
+ const user = userEvent.setup();
+ renderAccordion({ type: 'multiple' });
+
+ await user.click(screen.getByRole('button', { name: 'Section 1' }));
+ await user.click(screen.getByRole('button', { name: 'Section 2' }));
+
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
+ expect(screen.getByText('Content 2')).toBeInTheDocument();
+ });
+
+ it('allows only one item open in single mode', async () => {
+ const user = userEvent.setup();
+ renderAccordion({ type: 'single' });
+
+ await user.click(screen.getByRole('button', { name: 'Section 1' }));
+ await user.click(screen.getByRole('button', { name: 'Section 2' }));
+
+ expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
+ expect(screen.getByText('Content 2')).toBeInTheDocument();
+ });
+ });
+
+ describe('controlled value', () => {
+ it('respects controlled value prop', () => {
+ renderAccordion({ value: ['item2'] });
+
+ expect(screen.getByText('Content 2')).toBeInTheDocument();
+ expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
+ });
+
+ it('does not change when controlled', async () => {
+ const user = userEvent.setup();
+ renderAccordion({ value: [] });
+
+ await user.click(screen.getByRole('button', { name: 'Section 1' }));
+
+ expect(screen.queryByText('Content 1')).not.toBeInTheDocument();
+ });
+ });
+
+ describe('ARIA attributes', () => {
+ it('trigger has aria-expanded=false when closed', () => {
+ renderAccordion();
+ const trigger = screen.getByRole('button', { name: 'Section 1' });
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
+ });
+
+ it('trigger has aria-expanded=true when open', () => {
+ renderAccordion({ defaultValue: ['item1'] });
+ const trigger = screen.getByRole('button', { name: 'Section 1' });
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ it('trigger has aria-controls linked to panel id', () => {
+ renderAccordion({ defaultValue: ['item1'] });
+ const trigger = screen.getByRole('button', { name: 'Section 1' });
+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]');
+
+ expect(trigger).toHaveAttribute('aria-controls', panel?.getAttribute('id'));
+ });
+
+ it('panel has aria-labelledby linked to trigger id', () => {
+ renderAccordion({ defaultValue: ['item1'] });
+ const trigger = screen.getByRole('button', { name: 'Section 1' });
+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]');
+
+ expect(panel).toHaveAttribute('aria-labelledby', trigger.getAttribute('id'));
+ });
+
+ it('panel has role=region', () => {
+ renderAccordion({ defaultValue: ['item1'] });
+ expect(screen.getByRole('region')).toBeInTheDocument();
+ });
+ });
+
+ describe('animation lifecycle', () => {
+ it('panel is not in DOM when closed', () => {
+ renderAccordion();
+ expect(document.querySelector('[data-cl-slot="accordion-panel"]')).not.toBeInTheDocument();
+ });
+
+ it('applies data-cl-open on panel when open', () => {
+ renderAccordion({ defaultValue: ['item1'] });
+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]');
+ expect(panel).toHaveAttribute('data-cl-open', '');
+ });
+
+ it('does not apply starting-style on initially open panels', () => {
+ renderAccordion({ defaultValue: ['item1'] });
+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]');
+ expect(panel).not.toHaveAttribute('data-cl-starting-style');
+ });
+
+ it('sets --cl-accordion-panel-height CSS variable on panel', () => {
+ renderAccordion({ defaultValue: ['item1'] });
+ const panel = document.querySelector('[data-cl-slot="accordion-panel"]') as HTMLElement;
+ // Verify the CSS variable is present in the style attribute
+ expect(panel.getAttribute('style')).toContain('--cl-accordion-panel-height');
+ });
+ });
+
+ describe('disabled', () => {
+ it('disables all items when disabled on root', () => {
+ renderAccordion({ disabled: true });
+ const triggers = screen.getAllByRole('button');
+ triggers.forEach(trigger => {
+ expect(trigger).toHaveAttribute('aria-disabled', 'true');
+ });
+ });
+
+ it('prevents toggle when disabled', async () => {
+ const onValueChange = vi.fn();
+ const user = userEvent.setup();
+ renderAccordion({ disabled: true, onValueChange });
+
+ await user.click(screen.getByRole('button', { name: 'Section 1' }));
+
+ expect(onValueChange).not.toHaveBeenCalled();
+ });
+
+ it('disables individual item', async () => {
+ const onValueChange = vi.fn();
+ const user = userEvent.setup();
+ render(
+
+
+
+ Disabled
+
+ Content
+
+
+
+ Enabled
+
+ Content 2
+
+ ,
+ );
+
+ await user.click(screen.getByRole('button', { name: 'Disabled' }));
+ expect(onValueChange).not.toHaveBeenCalled();
+
+ await user.click(screen.getByRole('button', { name: 'Enabled' }));
+ expect(onValueChange).toHaveBeenCalledWith(['item2']);
+ });
+
+ it('applies data-cl-disabled on item and trigger', () => {
+ render(
+
+
+
+ Disabled
+
+ Content
+
+ ,
+ );
+
+ const item = document.querySelector('[data-cl-slot="accordion-item"]');
+ const trigger = document.querySelector('[data-cl-slot="accordion-trigger"]');
+ expect(item).toHaveAttribute('data-cl-disabled', '');
+ expect(trigger).toHaveAttribute('data-cl-disabled', '');
+ });
+ });
+
+ describe('keyboard navigation', () => {
+ it('moves focus down with ArrowDown', async () => {
+ const user = userEvent.setup();
+ renderAccordion();
+
+ const triggers = screen.getAllByRole('button');
+ triggers[0].focus();
+ await user.keyboard('{ArrowDown}');
+
+ expect(triggers[1]).toHaveFocus();
+ });
+
+ it('moves focus up with ArrowUp', async () => {
+ const user = userEvent.setup();
+ renderAccordion();
+
+ const triggers = screen.getAllByRole('button');
+ triggers[1].focus();
+ await user.keyboard('{ArrowUp}');
+
+ expect(triggers[0]).toHaveFocus();
+ });
+
+ it('toggles item with Enter', async () => {
+ const user = userEvent.setup();
+ renderAccordion();
+
+ const trigger = screen.getByRole('button', { name: 'Section 1' });
+ trigger.focus();
+ await user.keyboard('{Enter}');
+
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
+ });
+
+ it('toggles item with Space', async () => {
+ const user = userEvent.setup();
+ renderAccordion();
+
+ const trigger = screen.getByRole('button', { name: 'Section 1' });
+ trigger.focus();
+ await user.keyboard(' ');
+
+ expect(screen.getByText('Content 1')).toBeInTheDocument();
+ });
+
+ it('moves focus to first trigger with Home', async () => {
+ const user = userEvent.setup();
+ renderAccordion();
+
+ const triggers = screen.getAllByRole('button');
+ triggers[2].focus();
+ await user.keyboard('{Home}');
+
+ expect(triggers[0]).toHaveFocus();
+ });
+
+ it('moves focus to last trigger with End', async () => {
+ const user = userEvent.setup();
+ renderAccordion();
+
+ const triggers = screen.getAllByRole('button');
+ triggers[0].focus();
+ await user.keyboard('{End}');
+
+ expect(triggers[2]).toHaveFocus();
+ });
+
+ it('Home skips disabled triggers', async () => {
+ const user = userEvent.setup();
+ render(
+
+
+
+ Section 1
+
+ Content 1
+
+
+
+ Section 2
+
+ Content 2
+
+
+
+ Section 3
+
+ Content 3
+
+ ,
+ );
+
+ const section3 = screen.getByRole('button', { name: 'Section 3' });
+ section3.focus();
+ await user.keyboard('{Home}');
+
+ expect(screen.getByRole('button', { name: 'Section 2' })).toHaveFocus();
+ });
+
+ it('End skips disabled triggers', async () => {
+ const user = userEvent.setup();
+ render(
+
+
+
+ Section 1
+
+ Content 1
+
+
+
+ Section 2
+
+ Content 2
+
+
+
+ Section 3
+
+ Content 3
+
+ ,
+ );
+
+ const section1 = screen.getByRole('button', { name: 'Section 1' });
+ section1.focus();
+ await user.keyboard('{End}');
+
+ expect(screen.getByRole('button', { name: 'Section 2' })).toHaveFocus();
+ });
+ });
+
+ describe('accessibility (axe)', () => {
+ it('has no violations when collapsed', async () => {
+ const { container } = renderAccordion();
+ expect(await axe(container)).toHaveNoViolations();
+ });
+
+ it('has no violations when expanded', async () => {
+ const { container } = renderAccordion({ defaultValue: ['item1'] });
+ expect(await axe(container)).toHaveNoViolations();
+ });
+ });
+});
diff --git a/packages/headless/src/primitives/accordion/accordion.tsx b/packages/headless/src/primitives/accordion/accordion.tsx
new file mode 100644
index 00000000000..18285ef8adb
--- /dev/null
+++ b/packages/headless/src/primitives/accordion/accordion.tsx
@@ -0,0 +1,352 @@
+'use client';
+
+import { Composite, CompositeItem } from '@floating-ui/react';
+import React, {
+ createContext,
+ type ReactNode,
+ type RefObject,
+ useCallback,
+ useContext,
+ useId,
+ useLayoutEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import { useControllableState } from '../../hooks/use-controllable-state';
+import { useTransition } from '../../hooks/use-transition';
+import { type ComponentProps, mergeProps, renderElement } from '../../utils/render-element';
+
+// ---------------------------------------------------------------------------
+// Context
+// ---------------------------------------------------------------------------
+
+interface AccordionContextValue {
+ value: string[];
+ toggle: (itemValue: string) => void;
+ disabled: boolean;
+ accordionId: string;
+}
+
+const AccordionContext = createContext(null);
+
+function useAccordionContext() {
+ const ctx = useContext(AccordionContext);
+ if (!ctx) {
+ throw new Error('Accordion compound components must be used within ');
+ }
+ return ctx;
+}
+
+interface AccordionItemContextValue {
+ itemValue: string;
+ open: boolean;
+ disabled: boolean;
+ triggerId: string;
+ panelId: string;
+}
+
+const AccordionItemContext = createContext(null);
+
+function useAccordionItemContext() {
+ const ctx = useContext(AccordionItemContext);
+ if (!ctx) {
+ throw new Error('Accordion.Trigger/Header/Panel must be used within ');
+ }
+ return ctx;
+}
+
+// ---------------------------------------------------------------------------
+// Accordion (root)
+// ---------------------------------------------------------------------------
+
+export interface AccordionProps extends ComponentProps<'div'> {
+ /** Controlled open items. */
+ value?: string[];
+ /** Initial open items (uncontrolled). */
+ defaultValue?: string[];
+ /** Called when open items change. */
+ onValueChange?: (value: string[]) => void;
+ /** When true, only one item can be open at a time. @default false */
+ type?: 'single' | 'multiple';
+ /** Disable all items. @default false */
+ disabled?: boolean;
+ children: ReactNode;
+}
+
+function AccordionRoot(props: AccordionProps) {
+ const { render, type = 'multiple', disabled = false, children, ...otherProps } = props;
+
+ const [value, setValue] = useControllableState(props.value, props.defaultValue ?? [], props.onValueChange);
+
+ const accordionId = useId();
+
+ const toggle = useCallback(
+ (itemValue: string) => {
+ const isOpen = value.includes(itemValue);
+ if (isOpen) {
+ setValue(value.filter(v => v !== itemValue));
+ } else if (type === 'single') {
+ setValue([itemValue]);
+ } else {
+ setValue([...value, itemValue]);
+ }
+ },
+ [type, value, setValue],
+ );
+
+ const contextValue = useMemo(
+ () => ({ value, toggle, disabled, accordionId }),
+ [value, toggle, disabled, accordionId],
+ );
+
+ return (
+
+ ) => {
+ // aria-orientation is injected by Composite but is not valid on a
+ // generic (no widget role). Strip it to avoid an axe violation.
+ const { 'aria-orientation': _ao, ...restCompositeProps } = compositeProps;
+
+ const defaultProps: Record
= {
+ 'data-cl-slot': 'accordion-root',
+ onKeyDown: (event: React.KeyboardEvent) => {
+ if (event.key !== 'Home' && event.key !== 'End') return;
+ event.preventDefault();
+ const items = Array.from(
+ event.currentTarget.querySelectorAll('[data-cl-slot="accordion-trigger"]:not([disabled])'),
+ );
+ if (items.length === 0) return;
+ const target = event.key === 'Home' ? items[0] : items[items.length - 1];
+ target.focus();
+ },
+ };
+
+ const merged = mergeProps<'div'>(
+ defaultProps,
+ mergeProps<'div'>(otherProps, restCompositeProps as Record),
+ );
+
+ return renderElement({
+ defaultTagName: 'div',
+ render,
+ props: merged,
+ })!;
+ }}
+ >
+ {children}
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Accordion.Item
+// ---------------------------------------------------------------------------
+
+export interface AccordionItemProps extends ComponentProps<'div'> {
+ /** Unique value identifying this item. */
+ value: string;
+ /** Disable this specific item. */
+ disabled?: boolean;
+}
+
+function AccordionItem(props: AccordionItemProps) {
+ const { render, value: itemValue, disabled: itemDisabled, ...otherProps } = props;
+ const ctx = useAccordionContext();
+
+ const open = ctx.value.includes(itemValue);
+ const disabled = itemDisabled ?? ctx.disabled;
+ const triggerId = `${ctx.accordionId}-trigger-${itemValue}`;
+ const panelId = `${ctx.accordionId}-panel-${itemValue}`;
+
+ const itemContextValue = useMemo(
+ () => ({ itemValue, open, disabled, triggerId, panelId }),
+ [itemValue, open, disabled, triggerId, panelId],
+ );
+
+ const state = { open, disabled };
+
+ const defaultProps = {
+ 'data-cl-slot': 'accordion-item',
+ };
+
+ return (
+
+ {renderElement({
+ defaultTagName: 'div',
+ render,
+ state,
+ stateAttributesMapping: {
+ open: (v: boolean): Record | null => (v ? { 'data-cl-open': '' } : { 'data-cl-closed': '' }),
+ disabled: (v: boolean) => (v ? { 'data-cl-disabled': '' } : null),
+ },
+ props: mergeProps<'div'>(defaultProps, otherProps),
+ })}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Accordion.Header
+// ---------------------------------------------------------------------------
+
+export interface AccordionHeaderProps extends ComponentProps<'h3'> {}
+
+function AccordionHeader(props: AccordionHeaderProps) {
+ const { render, ...otherProps } = props;
+
+ const defaultProps = {
+ 'data-cl-slot': 'accordion-header',
+ };
+
+ return renderElement({
+ defaultTagName: 'h3',
+ render,
+ props: mergeProps<'h3'>(defaultProps, otherProps),
+ });
+}
+
+// ---------------------------------------------------------------------------
+// Accordion.Trigger
+// ---------------------------------------------------------------------------
+
+export interface AccordionTriggerProps extends ComponentProps<'button'> {}
+
+function AccordionTrigger(props: AccordionTriggerProps) {
+ const { render, children, ...otherProps } = props;
+ const ctx = useAccordionContext();
+ const { itemValue, open, disabled, triggerId, panelId } = useAccordionItemContext();
+
+ const state = { open, disabled };
+
+ return (
+ ) => {
+ const defaultProps: Record = {
+ 'data-cl-slot': 'accordion-trigger',
+ id: triggerId,
+ type: 'button' as const,
+ 'aria-expanded': open,
+ 'aria-controls': panelId,
+ 'aria-disabled': disabled || undefined,
+ onClick: () => {
+ if (!disabled) ctx.toggle(itemValue);
+ },
+ };
+
+ const merged = mergeProps<'button'>(
+ mergeProps<'button'>(defaultProps, otherProps),
+ compositeProps as Record,
+ );
+
+ return renderElement({
+ defaultTagName: 'button',
+ render,
+ state,
+ stateAttributesMapping: {
+ open: (v: boolean): Record | null =>
+ v ? { 'data-cl-open': '' } : { 'data-cl-closed': '' },
+ disabled: (v: boolean) => (v ? { 'data-cl-disabled': '' } : null),
+ },
+ props: merged,
+ })!;
+ }}
+ >
+ {children}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Accordion.Panel
+// ---------------------------------------------------------------------------
+
+export interface AccordionPanelProps extends ComponentProps<'div'> {}
+
+function AccordionPanel(props: AccordionPanelProps) {
+ const { render, ...otherProps } = props;
+ const { open, triggerId, panelId } = useAccordionItemContext();
+
+ const panelRef = useRef(null);
+ const [height, setHeight] = useState(undefined);
+
+ // Track whether open has ever transitioned from true→false.
+ // Until that happens, skip enter animations (prevents animate-on-load).
+ const hasBeenClosed = useRef(false);
+ if (!open) hasBeenClosed.current = true;
+
+ const { mounted, transitionProps } = useTransition({
+ open,
+ ref: panelRef as RefObject,
+ });
+
+ // Measure the content height and keep it in sync via ResizeObserver
+ useLayoutEffect(() => {
+ if (!mounted) return;
+
+ const panel = panelRef.current;
+ if (!panel) return;
+
+ // Measure scrollHeight of the panel's content
+ const measure = () => {
+ setHeight(panel.scrollHeight);
+ };
+
+ measure();
+
+ const ro = new ResizeObserver(measure);
+ // Observe children mutations that affect height
+ ro.observe(panel, { box: 'border-box' });
+
+ return () => ro.disconnect();
+ }, [mounted]);
+
+ const state = { open };
+
+ // Skip enter animation for panels that have never been closed
+ const effectiveTransitionProps = !hasBeenClosed.current
+ ? {
+ ...transitionProps,
+ 'data-cl-starting-style': undefined,
+ style: undefined,
+ }
+ : transitionProps;
+
+ const defaultProps: Record = {
+ 'data-cl-slot': 'accordion-panel',
+ id: panelId,
+ role: 'region' as const,
+ 'aria-labelledby': triggerId,
+ ref: panelRef,
+ ...effectiveTransitionProps,
+ style: {
+ '--cl-accordion-panel-height': height != null ? `${height}px` : undefined,
+ ...effectiveTransitionProps.style,
+ },
+ };
+
+ return renderElement({
+ defaultTagName: 'div',
+ render,
+ enabled: mounted,
+ state,
+ stateAttributesMapping: {
+ open: (v: boolean): Record | null => (v ? { 'data-cl-open': '' } : { 'data-cl-closed': '' }),
+ },
+ props: mergeProps<'div'>(defaultProps, otherProps),
+ });
+}
+
+// ---------------------------------------------------------------------------
+// Compound export
+// ---------------------------------------------------------------------------
+
+export const Accordion = Object.assign(AccordionRoot, {
+ Item: AccordionItem,
+ Header: AccordionHeader,
+ Trigger: AccordionTrigger,
+ Panel: AccordionPanel,
+});
diff --git a/packages/headless/src/primitives/accordion/index.ts b/packages/headless/src/primitives/accordion/index.ts
new file mode 100644
index 00000000000..798b42a95b3
--- /dev/null
+++ b/packages/headless/src/primitives/accordion/index.ts
@@ -0,0 +1,8 @@
+export type {
+ AccordionHeaderProps,
+ AccordionItemProps,
+ AccordionPanelProps,
+ AccordionProps,
+ AccordionTriggerProps,
+} from './accordion';
+export { Accordion } from './accordion';
diff --git a/packages/headless/vite.config.ts b/packages/headless/vite.config.ts
index e3385d93f49..f89462fe606 100644
--- a/packages/headless/vite.config.ts
+++ b/packages/headless/vite.config.ts
@@ -11,6 +11,7 @@ export default defineConfig({
build: {
lib: {
entry: {
+ 'primitives/accordion/index': 'src/primitives/accordion/index.ts',
'primitives/dialog/index': 'src/primitives/dialog/index.ts',
'utils/index': 'src/utils/index.ts',
'hooks/use-controllable-state': 'src/hooks/use-controllable-state.ts',