Accordion ​

Expandable sections with single or multiple open values. Accordion owns selection; AccordionItem toggles and resolves data-state="open"|"closed".

PreviewAccordion
Focus, dismiss, and ARIA live in the core engines.

Usage ​

tsx
import { Accordion, AccordionItem } from "@sometic/react/structure";

export function Example(): JSX.Element {
    return (
        <Accordion type="single" defaultValue="a11y">
            <AccordionItem value="a11y" title="Accessibility">
                Accessibility lives in core engines.
            </AccordionItem>
            <AccordionItem value="style" title="Styling">
                Bring your own design tokens.
            </AccordionItem>
        </Accordion>
    );
}
vue
<script setup>
import { Accordion, AccordionItem } from "@sometic/vue/structure";
</script>

<template>
    <Accordion type="single" default-value="a11y">
        <AccordionItem value="a11y" title="Accessibility">
            Accessibility lives in core engines.
        </AccordionItem>
        <AccordionItem value="style" title="Styling"> Bring your own design tokens. </AccordionItem>
    </Accordion>
</template>
js
import {
    createAccordionController,
    resolveAccordion,
    resolveAccordionItem,
} from "@sometic/dom/accordion";

const root = document.querySelector("#accordion");
const rootView = resolveAccordion({ type: "single" });
for (const [key, value] of Object.entries(rootView.attributes)) {
    root.setAttribute(key, value);
}

const controller = createAccordionController({
    type: "single",
    defaultValue: "a11y",
    onValueChange: (value) => {
        for (const item of root.querySelectorAll("[data-item]")) {
            const open = value === item.dataset.item;
            const view = resolveAccordionItem({
                value: item.dataset.item,
                open,
                disabled: item.hasAttribute("disabled"),
            });
            for (const [key, attr] of Object.entries(view.attributes)) {
                item.setAttribute(key, attr);
            }
            const content = item.querySelector("[data-slot='content']");
            if (content) {
                content.hidden = !open;
            }
        }
    },
});

for (const trigger of root.querySelectorAll("[data-slot='trigger']")) {
    trigger.addEventListener("click", () => {
        const item = trigger.closest("[data-item]");
        if (item && !item.hasAttribute("disabled")) {
            controller.toggle(item.dataset.item);
        }
    });
}
html
<!-- CE not shipped for this surface. Use React, Vue, or @sometic/dom (Vanilla) above. -->
html
<!-- CDN not available for this surface yet (no shipped custom element). Use npm adapters or Vanilla. -->

Custom element not shipped in this beta; use the DOM controller.

Custom element not shipped for Accordion. Vanilla uses @sometic/dom/accordion. React and Vue ship Accordion / AccordionItem from @sometic/*/structure.

How it works ​

  1. Resolve: resolveAccordion / resolveAccordionItem.
  2. Controller (createAccordionController): type: "single" | "multiple", controllable value.
  3. React: context + toggle. Prefer title for a header button; children then render only while open.

Anatomy ​

PartRole / slot
AccordionRoot container
AccordionItemItem with data-state
Triggerdata-slot="trigger" (when title)
Contentdata-slot="content" while open

Props / attributes ​

React AccordionProps ​

Extends HTMLAttributes<HTMLDivElement>. Remaining native div attrs are forwarded to the root.

PropTypeDefaultDescription
type"single" | "multiple""single"One vs many open
valuestring | string[]—Controlled value
defaultValuestring | string[]"" / []Uncontrolled initial
onValueChange(value: string | string[]) => void—Changes
childrenReactNode—Accordion items
Native attrsremaining div HTML attrs—Forwarded to root

React AccordionItemProps ​

Extends HTMLAttributes<HTMLDivElement>. Remaining native div attrs are forwarded to the item root.

PropTypeDefaultDescription
valuestring—Item id (required)
titleReactNode—Header button; when set, children render only while open
disabledboolean—Blocks toggle
childrenReactNode—Body (or full content when title omitted)
Native attrsremaining div HTML attrs—Forwarded to the item root

When title is omitted, clicking the item root toggles (plus any onClick you pass).

Vue ​

Accordion and AccordionItem from @sometic/vue/structure. Props: type, value, defaultValue, collapsible, lazyMount, forceMount on the root; value, disabled, title on items. Emits update:value and valueChange.

vue
<script setup lang="ts">
import { Accordion, AccordionItem } from "@sometic/vue/structure";
</script>

<template>
    <Accordion type="single" default-value="a">
        <AccordionItem value="a" title="Accessibility">
            Focus, dismiss, and ARIA live in the core engines.
        </AccordionItem>
        <AccordionItem value="b" title="Styling">
            Unstyled by default. Own tokens and layout.
        </AccordionItem>
    </Accordion>
</template>

Custom element ​

CE not shipped. Use Vanilla DOM controller, React, or Vue.

Events / callbacks ​

SurfaceEventPayload
ReactonValueChangestring | string[]
VuevalueChange / update:valuestring | string[]
Custom element——
DOM controlleronValueChangestring | string[]

Controlled vs uncontrolled ​

Pass value + onValueChange for controlled. Omit value and use defaultValue ("" for single, [] for multiple when omitted).

Accessibility ​

  • Reflect open state with data-state.
  • Prefer a button header pattern (title) for keyboard activation.
  • When title is set, the trigger uses aria-expanded.

Styling ​

Target [data-state="open"|"closed"], [data-slot="trigger"|"content"].

When to use / When not ​

Use for FAQs and progressive disclosure.

Do not use when only one panel should ever exist as tabs (Tabs).

Vs Radix / React Aria. Same portable story as Tabs: shared @sometic/dom controller plus unstyled adapters. Prefer Radix/React Aria when you are React-only and already invested.

FAQ ​

Must AccordionItem live under Accordion? Yes. Context is required.

Single vs multiple? type="single" stores a string; type="multiple" stores string[].

Lazy mount? Adapters default to lazyMount (inactive panels are not mounted). Pass forceMount to keep DOM for CSS transitions or SSR hydration needs.

Is there an sometic-accordion? No. CE not shipped.

Vue components? Yes. @sometic/vue/structure.

Does React forward native attrs? Yes, onto Accordion and AccordionItem roots.