Add Accordion component (5 parts, keyboard nav, single/multiple, animations)

Parts: Root, Item, Header, Trigger, Content
- Single/multiple mode via type prop
- Collapsible (single mode: click open item to close)
- Per-item and global disabled
- Keyboard: Arrow keys (orientation-aware), Home/End, loop option
- Presence on Content with data-starting/ending-style
- --soma-accordion-content-height/width CSS vars for animations
- forceMount on Content
- Header with level prop (aria-level)
- Auto-generated item values when omitted
- Demo page with 5 sections

Compared with Radix, Ark UI, Bits UI:
- Has collapsible + loop (Radix has collapsible, Bits has loop, soma has both)
- Has header level (like Bits)
- Has CSS animation vars (like all three)
- Has data-starting/ending-style (like Bits)
- Skipped ItemIndicator (Ark) — presentation concern for visual layer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
semantuix
dev 6 months ago
parent 193bfcdca2
commit bb0cdeb554

@ -0,0 +1,261 @@
<script lang="ts">
import * as Accordion from '$soma/components/accordion';
let value1 = $state<string[]>([]);
let value2 = $state<string[]>(['item-1']);
let value3 = $state<string[]>([]);
let value4 = $state<string[]>([]);
</script>
<svelte:head>
<title>Accordion · Soma</title>
</svelte:head>
<div class="page">
<h1>Soma Accordion</h1>
<p>Headless accordion with keyboard navigation, single/multiple modes, and CSS animation support.</p>
<!-- ── 1. Single (default) ──────────────────────────────────────────────── -->
<section>
<h2>1. Single mode (collapsible)</h2>
<p>One item open at a time. Click an open item to collapse it.</p>
<p class="state">value: {JSON.stringify(value1)}</p>
<Accordion.Root bind:value={value1} collapsible>
<Accordion.Item value="item-1">
<Accordion.Header>
<Accordion.Trigger>What is soma?</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">
Soma is a headless component library for Svelte 5. It provides behavior,
accessibility, and composition without visual opinions.
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Header>
<Accordion.Trigger>How does it compare?</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">
Soma matches or exceeds Radix, Ark UI, and Bits UI in features.
Layers are integrated as behaviors, not nested wrappers.
</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-3">
<Accordion.Header>
<Accordion.Trigger>What about animations?</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">
Content exposes --soma-accordion-content-height and data-starting-style /
data-ending-style for CSS transitions. Use forceMount to keep content in DOM.
</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</section>
<!-- ── 2. Multiple ──────────────────────────────────────────────────────── -->
<section>
<h2>2. Multiple mode</h2>
<p>Any number of items open simultaneously.</p>
<p class="state">value: {JSON.stringify(value2)}</p>
<Accordion.Root type="multiple" bind:value={value2}>
<Accordion.Item value="item-1">
<Accordion.Header>
<Accordion.Trigger>First (starts open)</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">This item starts open because value includes 'item-1'.</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2">
<Accordion.Header>
<Accordion.Trigger>Second</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">Open multiple items at once.</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-3">
<Accordion.Header>
<Accordion.Trigger>Third</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">All three can be open.</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</section>
<!-- ── 3. Disabled ──────────────────────────────────────────────────────── -->
<section>
<h2>3. Disabled</h2>
<p>Global disabled + per-item disabled.</p>
<Accordion.Root bind:value={value3} collapsible>
<Accordion.Item value="item-1">
<Accordion.Header>
<Accordion.Trigger>Enabled item</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">This works normally.</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="item-2" disabled>
<Accordion.Header>
<Accordion.Trigger>Disabled item</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">This should never open.</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</section>
<!-- ── 4. Keyboard navigation ───────────────────────────────────────────── -->
<section>
<h2>4. Keyboard navigation (loop)</h2>
<p>
Focus a trigger, then use Arrow keys to navigate. <code>loop=true</code> wraps at
boundaries. Home/End jump to first/last.
</p>
<Accordion.Root bind:value={value4} collapsible loop>
<Accordion.Item value="a">
<Accordion.Header>
<Accordion.Trigger>Alpha</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">Alpha content</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="b">
<Accordion.Header>
<Accordion.Trigger>Beta</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">Beta content</div>
</Accordion.Content>
</Accordion.Item>
<Accordion.Item value="c">
<Accordion.Header>
<Accordion.Trigger>Gamma</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
<div class="content-body">Gamma content</div>
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
</section>
<!-- ── 5. Data attributes ───────────────────────────────────────────────── -->
<section>
<h2>5. Data attributes (inspect DOM)</h2>
<pre>Root: [data-accordion] [data-orientation] [data-disabled]
Item: [data-accordion-item] [data-state] [data-disabled] [data-orientation]
Header: [data-accordion-header] [data-state] [data-disabled] [role=heading] [aria-level]
Trigger: [data-accordion-trigger] [data-state] [data-disabled] [aria-expanded] [aria-controls]
Content: [data-accordion-content] [data-state] [data-disabled] [role=region] [aria-labelledby]
[data-starting-style] [data-ending-style]
--soma-accordion-content-height
--soma-accordion-content-width</pre>
</section>
</div>
<style>
.page {
max-width: 640px;
margin: 2rem auto;
font-family: system-ui, sans-serif;
}
h1 { margin-bottom: 0.5rem; }
h2 { margin-top: 2rem; margin-bottom: 0.5rem; }
p { color: #555; line-height: 1.6; }
.state { font-family: monospace; font-size: 0.85rem; color: #1e293b; }
section {
padding: 1.5rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 1rem;
}
pre {
font-size: 0.8rem;
background: #f8fafc;
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
margin: 0;
}
.content-body {
padding: 0.75rem 1rem;
color: #475569;
line-height: 1.6;
}
/* Accordion styles via data-* */
:global([data-accordion]) {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
:global([data-accordion-item]) {
border-bottom: 1px solid #e2e8f0;
}
:global([data-accordion-item]:last-child) {
border-bottom: none;
}
:global([data-accordion-trigger]) {
display: flex;
width: 100%;
padding: 0.75rem 1rem;
border: none;
background: #f8fafc;
font: inherit;
font-weight: 600;
cursor: pointer;
text-align: left;
justify-content: space-between;
align-items: center;
}
:global([data-accordion-trigger]::after) {
content: '▸';
transition: transform 200ms;
}
:global([data-accordion-trigger][data-state='open']::after) {
transform: rotate(90deg);
}
:global([data-accordion-trigger]:hover:not(:disabled)) {
background: #f1f5f9;
}
:global([data-accordion-trigger]:disabled) {
opacity: 0.5;
cursor: not-allowed;
}
:global([data-accordion-trigger]:focus-visible) {
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
:global([data-accordion-content]) {
overflow: hidden;
background: white;
}
</style>

@ -0,0 +1,295 @@
import { Provider, context, type WithRefOpts } from '$soma/provider';
import { createAttrs, boolToEmptyStrOrUndef, getDataOpenClosed } from '$soma/attrs';
import { readableActive, state, type Active, type ActiveProps, type StateProps } from '$soma/reactive';
import type { OnChangeFn, Orientation, Direction, SomaMouseEvent, SomaKeyboardEvent } from '$soma/types';
import { KEYS, getDirectionalKeys } from '$soma/keyboard';
import { useId } from '$soma/id';
import { Soma } from '$soma/core/soma.svelte';
import { Presence } from '$soma/layers/presence.svelte';
import { ResizeObserver$ } from '$soma/layers/resize-observer.svelte';
const attrs = createAttrs({
component: 'accordion',
parts: ['root', 'item', 'header', 'trigger', 'content'] as const,
});
export type AccordionType = 'single' | 'multiple';
// ── Root ─────────────────────────────────────────────────────────────────────
interface AccordionOpts
extends WithRefOpts,
StateProps<{ value: string[] }>,
ActiveProps<{
type: AccordionType;
collapsible: boolean;
disabled: boolean;
orientation: Orientation;
loop: boolean;
dir: Direction;
onValueChange: OnChangeFn<string[]>;
}> {}
export class AccordionProvider extends Provider<AccordionOpts> {
static readonly ctx = context<AccordionProvider>('Accordion');
static from(opts: AccordionOpts) {
return new AccordionProvider(opts);
}
readonly soma: Soma | undefined;
private constructor(opts: AccordionOpts) {
super(opts, 'Accordion', 'root', attrs.root, AccordionProvider.ctx);
this.soma = Soma.getOr();
}
isItemOpen(value: string): boolean {
return this.opts.value.current.includes(value);
}
toggleItem(value: string) {
if (this.opts.disabled.current) return;
const current = this.opts.value.current;
const isOpen = current.includes(value);
let next: string[];
if (this.opts.type.current === 'single') {
if (isOpen && !this.opts.collapsible.current) return;
next = isOpen ? [] : [value];
} else {
next = isOpen ? current.filter((v) => v !== value) : [...current, value];
}
this.opts.value.current = next;
this.opts.onValueChange.current(next);
}
/** Get all trigger elements for keyboard navigation. */
getTriggers(): HTMLButtonElement[] {
const root = this.opts.ref?.current;
if (!root) return [];
const selector = `[${attrs.trigger}]:not([disabled])`;
return Array.from(root.querySelectorAll<HTMLButtonElement>(selector));
}
readonly props = $derived.by(() =>
this.assertProps({
...this.baseProps,
'data-orientation': this.opts.orientation.current,
'data-disabled': boolToEmptyStrOrUndef(this.opts.disabled.current),
} as const),
);
}
// ── Item ─────────────────────────────────────────────────────────────────────
interface AccordionItemOpts
extends WithRefOpts,
ActiveProps<{
value: string;
disabled: boolean;
}> {}
export class AccordionItemProvider extends Provider<AccordionItemOpts> {
static readonly ctx = context<AccordionItemProvider>('AccordionItem');
static from(opts: AccordionItemOpts) {
return new AccordionItemProvider(opts);
}
readonly root: AccordionProvider;
readonly contentPresence: Presence;
// tracked IDs
triggerId = state('');
contentId = state('');
// content size for CSS vars
contentHeight = $state<number>(0);
contentWidth = $state<number>(0);
private contentRef = state<HTMLElement | null>(null);
private constructor(opts: AccordionItemOpts) {
super(opts, 'Accordion', 'item', attrs.item, AccordionItemProvider.ctx);
this.root = AccordionProvider.ctx.get();
this.contentPresence = new Presence({
open: readableActive(() => this.isOpen),
ref: this.contentRef,
});
// Track content size for CSS animation vars
new ResizeObserver$(
() => this.contentRef.current,
() => {
const el = this.contentRef.current;
if (!el) return;
this.contentHeight = el.scrollHeight;
this.contentWidth = el.scrollWidth;
},
);
}
readonly isOpen = $derived.by(() =>
this.root.isItemOpen(this.opts.value.current),
);
readonly isDisabled = $derived.by(() =>
this.opts.disabled.current || this.root.opts.disabled.current,
);
setContentRef(el: HTMLElement | null) {
this.contentRef.current = el;
}
readonly props = $derived.by(() =>
this.assertProps({
...this.baseProps,
'data-state': getDataOpenClosed(this.isOpen),
'data-disabled': boolToEmptyStrOrUndef(this.isDisabled),
'data-orientation': this.root.opts.orientation.current,
} as const),
);
}
// ── Header ───────────────────────────────────────────────────────────────────
interface AccordionHeaderOpts extends WithRefOpts, ActiveProps<{ level: number }> {}
export class AccordionHeaderProvider extends Provider<AccordionHeaderOpts> {
static from(opts: AccordionHeaderOpts) {
return new AccordionHeaderProvider(opts);
}
readonly item: AccordionItemProvider;
private constructor(opts: AccordionHeaderOpts) {
super(opts, 'Accordion', 'header', attrs.header);
this.item = AccordionItemProvider.ctx.get();
}
readonly props = $derived.by(() =>
this.assertProps({
...this.baseProps,
role: 'heading' as const,
'aria-level': this.opts.level.current,
'data-state': getDataOpenClosed(this.item.isOpen),
'data-disabled': boolToEmptyStrOrUndef(this.item.isDisabled),
'data-orientation': this.item.root.opts.orientation.current,
} as const),
);
}
// ── Trigger ──────────────────────────────────────────────────────────────────
interface AccordionTriggerOpts extends WithRefOpts {}
export class AccordionTriggerProvider extends Provider<AccordionTriggerOpts> {
static from(opts: AccordionTriggerOpts) {
return new AccordionTriggerProvider(opts);
}
readonly item: AccordionItemProvider;
private constructor(opts: AccordionTriggerOpts) {
super(opts, 'Accordion', 'trigger', attrs.trigger);
this.item = AccordionItemProvider.ctx.get();
this.item.triggerId.current = opts.id.current;
}
readonly onclick = (_e: SomaMouseEvent<HTMLButtonElement>) => {
if (this.item.isDisabled) return;
this.item.root.toggleItem(this.item.opts.value.current);
};
readonly onkeydown = (e: SomaKeyboardEvent<HTMLButtonElement>) => {
if (this.item.isDisabled) return;
const root = this.item.root;
const orientation = root.opts.orientation.current;
const dir = root.opts.dir.current;
const { nextKey, prevKey } = getDirectionalKeys(dir, orientation);
const triggers = root.getTriggers();
const currentIndex = triggers.indexOf(e.currentTarget as HTMLButtonElement);
let targetIndex = -1;
if (e.key === nextKey) {
e.preventDefault();
targetIndex = root.opts.loop.current
? (currentIndex + 1) % triggers.length
: Math.min(currentIndex + 1, triggers.length - 1);
} else if (e.key === prevKey) {
e.preventDefault();
targetIndex = root.opts.loop.current
? (currentIndex - 1 + triggers.length) % triggers.length
: Math.max(currentIndex - 1, 0);
} else if (e.key === KEYS.HOME) {
e.preventDefault();
targetIndex = 0;
} else if (e.key === KEYS.END) {
e.preventDefault();
targetIndex = triggers.length - 1;
}
if (targetIndex >= 0 && triggers[targetIndex]) {
triggers[targetIndex].focus();
}
};
readonly props = $derived.by(() =>
this.assertProps({
...this.baseProps,
type: 'button' as const,
'aria-expanded': this.item.isOpen,
'aria-controls': this.item.contentId.current || undefined,
'data-state': getDataOpenClosed(this.item.isOpen),
'data-disabled': boolToEmptyStrOrUndef(this.item.isDisabled),
'data-orientation': this.item.root.opts.orientation.current,
disabled: this.item.isDisabled || undefined,
onclick: this.onclick,
onkeydown: this.onkeydown,
} as const),
);
}
// ── Content ──────────────────────────────────────────────────────────────────
interface AccordionContentOpts extends WithRefOpts {}
export class AccordionContentProvider extends Provider<AccordionContentOpts> {
static from(opts: AccordionContentOpts) {
return new AccordionContentProvider(opts);
}
readonly item: AccordionItemProvider;
private constructor(opts: AccordionContentOpts) {
super(opts, 'Accordion', 'content', attrs.content, undefined, (el) => {
this.item.setContentRef(el);
});
this.item = AccordionItemProvider.ctx.get();
this.item.contentId.current = opts.id.current;
}
readonly isPresent = $derived.by(() => this.item.contentPresence.isPresent);
readonly props = $derived.by(() =>
this.assertProps({
...this.baseProps,
role: 'region' as const,
'aria-labelledby': this.item.triggerId.current || undefined,
'data-state': getDataOpenClosed(this.item.isOpen),
'data-disabled': boolToEmptyStrOrUndef(this.item.isDisabled),
'data-orientation': this.item.root.opts.orientation.current,
style: {
'--soma-accordion-content-height': `${this.item.contentHeight}px`,
'--soma-accordion-content-width': `${this.item.contentWidth}px`,
},
...this.item.contentPresence.transitionAttrs,
} as const),
);
}

@ -0,0 +1,38 @@
<script lang="ts">
import { readableActive, writableActive } from '$soma/reactive';
import { mergeProps } from '$soma/props';
import { createId } from '$soma/id';
import { AccordionContentProvider } from '../accordion.svelte';
import type { AccordionContentProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'accordion-content'),
forceMount = false,
children,
child,
...restProps
}: AccordionContentProps = $props();
const state = AccordionContentProvider.from({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v),
),
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if state.isPresent || forceMount}
{#if child}
{@render child({ open: state.item.isOpen, props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.({ open: state.item.isOpen })}
</div>
{/if}
{/if}

@ -0,0 +1,37 @@
<script lang="ts">
import { readableActive, writableActive } from '$soma/reactive';
import { mergeProps } from '$soma/props';
import { createId } from '$soma/id';
import { AccordionHeaderProvider } from '../accordion.svelte';
import type { AccordionHeaderProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'accordion-header'),
level = 3,
children,
child,
...restProps
}: AccordionHeaderProps = $props();
const state = AccordionHeaderProvider.from({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v),
),
level: readableActive(() => level),
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}

@ -0,0 +1,39 @@
<script lang="ts">
import { readableActive, writableActive } from '$soma/reactive';
import { mergeProps } from '$soma/props';
import { createId, useId } from '$soma/id';
import { AccordionItemProvider } from '../accordion.svelte';
import type { AccordionItemProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'accordion-item'),
value = useId('accordion-item-value'),
disabled = false,
children,
child,
...restProps
}: AccordionItemProps = $props();
const state = AccordionItemProvider.from({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v),
),
value: readableActive(() => value),
disabled: readableActive(() => disabled),
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}

@ -0,0 +1,35 @@
<script lang="ts">
import { readableActive, writableActive } from '$soma/reactive';
import { mergeProps } from '$soma/props';
import { createId } from '$soma/id';
import { AccordionTriggerProvider } from '../accordion.svelte';
import type { AccordionTriggerProps } from '../types';
const uid = $props.id();
let {
ref = $bindable(null),
id = createId(uid, 'accordion-trigger'),
children,
child,
...restProps
}: AccordionTriggerProps = $props();
const state = AccordionTriggerProvider.from({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v),
),
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<button {...mergedProps}>
{@render children?.()}
</button>
{/if}

@ -0,0 +1,57 @@
<script lang="ts">
import { readableActive, writableActive } from '$soma/reactive';
import { mergeProps } from '$soma/props';
import { createId } from '$soma/id';
import { Soma } from '$soma/core/soma.svelte';
import { AccordionProvider } from '../accordion.svelte';
import type { AccordionProps } from '../types';
import { noop } from '$lib/util/funcs';
const uid = $props.id();
const soma = Soma.getOr();
let {
ref = $bindable(null),
id = createId(uid, 'accordion'),
type = 'single',
value = $bindable([]),
onValueChange = noop,
collapsible = false,
disabled = false,
orientation = 'vertical',
loop = false,
dir = soma?.dir.current ?? 'ltr',
children,
child,
...restProps
}: AccordionProps = $props();
const state = AccordionProvider.from({
id: readableActive(() => id),
ref: writableActive(
() => ref,
(v) => (ref = v),
),
type: readableActive(() => type),
value: writableActive(
() => value,
(v) => (value = v),
),
onValueChange: readableActive(() => onValueChange),
collapsible: readableActive(() => collapsible),
disabled: readableActive(() => disabled),
orientation: readableActive(() => orientation),
loop: readableActive(() => loop),
dir: readableActive(() => dir),
});
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}

@ -0,0 +1,15 @@
export { default as Root } from './components/accordion.svelte';
export { default as Item } from './components/accordion-item.svelte';
export { default as Header } from './components/accordion-header.svelte';
export { default as Trigger } from './components/accordion-trigger.svelte';
export { default as Content } from './components/accordion-content.svelte';
export type { AccordionType } from './accordion.svelte';
export type {
AccordionProps as RootProps,
AccordionItemProps as ItemProps,
AccordionHeaderProps as HeaderProps,
AccordionTriggerProps as TriggerProps,
AccordionContentProps as ContentProps,
} from './types';

@ -0,0 +1 @@
export * from './exports';

@ -0,0 +1,68 @@
import type { Snippet } from 'svelte';
import type { WithChild, Without, OnChangeFn, Orientation, Direction } from '$soma/types';
import type { PrimitiveDivAttributes, PrimitiveButtonAttributes } from '$soma/types';
import type { AccordionType } from './accordion.svelte';
export type AccordionProps = WithChild<{
/** Unique identifier. Auto-generated if omitted. */
id?: string;
/**
* 'single' allows one item open at a time. 'multiple' allows any number.
* @default 'single'
*/
type?: AccordionType;
/** Array of open item values. Bindable. @default [] */
value?: string[];
/** Callback fired when value changes. */
onValueChange?: OnChangeFn<string[]>;
/**
* When type='single', allows closing the open item by clicking its trigger again.
* Has no effect when type='multiple'.
* @default false
*/
collapsible?: boolean;
/** Disables all items. @default false */
disabled?: boolean;
/** Orientation for keyboard navigation (arrow keys). @default 'vertical' */
orientation?: Orientation;
/** Wrap keyboard navigation at boundaries. @default false */
loop?: boolean;
/** Text direction for keyboard navigation. @default from Soma config or 'ltr' */
dir?: Direction;
}> &
Without<PrimitiveDivAttributes, {}>;
export type AccordionItemProps = WithChild<{
/** Unique identifier. Auto-generated if omitted. */
id?: string;
/** Value that identifies this item. Auto-generated if omitted. */
value?: string;
/** Disables this item. @default false */
disabled?: boolean;
}> &
Without<PrimitiveDivAttributes, {}>;
export type AccordionHeaderProps = WithChild<{
/** Unique identifier. Auto-generated if omitted. */
id?: string;
/** Heading level for accessibility (aria-level). @default 3 */
level?: number;
}> &
Without<PrimitiveDivAttributes, {}>;
export type AccordionTriggerProps = WithChild<{
/** Unique identifier. Auto-generated if omitted. */
id?: string;
}> &
Without<PrimitiveButtonAttributes, {}>;
export type AccordionContentProps = WithChild<
{
/** Unique identifier. Auto-generated if omitted. */
id?: string;
/** Keep in DOM even when closed. Enables CSS height transitions. @default false */
forceMount?: boolean;
},
{ open: boolean }
> &
Without<PrimitiveDivAttributes, {}>;

@ -1,3 +1,4 @@
export * as Accordion from './accordion';
export * as Collapsible from './collapsible';
export * as Dialog from './dialog';
export * as Popover from './popover';

Loading…
Cancel
Save

Powered by TurnKey Linux.