Accordion
A flexible accordion component for organizing collapsible content sections. Built with native <details> and <summary> elements for accessibility and progressive enhancement.
Features
- 🎨 6 Variants: solid, flat, bordered, outline, ghost, text
- 🔄 Selection Modes: Single or multiple expansion
- 📏 3 Sizes: sm, md, lg- 🎬 Smooth Animation: content height animates via CSS
grid-template-rows— no layout thrashing- ♿ Accessible: Native HTML semantics, keyboard navigation, screen reader friendly - 🎯 Flexible Content: Support for icons, subtitles, and custom content
Source Code
View Source Code
import {
computed,
createContext,
defineComponent,
handle,
html,
typed,
provide,
type ReadonlySignal,
} from '@vielzeug/craftit';
import type { ComponentSize, VisualVariant } from '../../types';
/** Context provided by bit-accordion to its bit-accordion-item children. */
export type AccordionContext = {
notifyExpand: (expandedItem: HTMLElement) => void;
selectionMode: ReadonlySignal<'single' | 'multiple' | undefined>;
size: ReadonlySignal<ComponentSize | undefined>;
variant: ReadonlySignal<VisualVariant | undefined>;
};
/** Injection key for the accordion context. */
export const ACCORDION_CTX = createContext<AccordionContext>('AccordionContext');
import styles from './accordion.css?inline';
/** Accordion component properties */
export type BitAccordionEvents = {
change: { expandedItem: HTMLElement };
};
export type BitAccordionProps = {
/** Selection mode (single = only one opens, multiple = multiple can be open) */
selectionMode?: 'single' | 'multiple';
/** Size for all items (propagated via context) */
size?: ComponentSize;
/** Visual variant for all items (propagated via context) */
variant?: VisualVariant;
};
/**
* A container for accordion items with single or multiple selection modes.
*
* @element bit-accordion
*
* @attr {string} selection-mode - Selection mode: 'single' | 'multiple'
* @attr {string} size - Size for all items: 'sm' | 'md' | 'lg' (propagated to children)
* @attr {string} variant - Visual variant: 'solid' | 'flat' | 'bordered' | 'text' | 'glass' | 'frost' (propagated to children)
*
* @fires expand - Emitted when an item expands
* @fires change - Emitted when selection changes (single mode)
*
* @slot - Accordion item elements (bit-accordion-item)
*
* @example
* ```html
* <bit-accordion selection-mode="single"><bit-accordion-item>...</bit-accordion-item></bit-accordion>
* <bit-accordion variant="frost" size="lg"><bit-accordion-item>...</bit-accordion-item></bit-accordion>
* ```
*/
export const ACCORDION_TAG = defineComponent<BitAccordionProps, BitAccordionEvents>({
props: {
selectionMode: typed<BitAccordionProps['selectionMode']>(undefined),
size: typed<BitAccordionProps['size']>(undefined),
variant: typed<BitAccordionProps['variant']>(undefined),
},
setup({ emit, host, props }) {
const notifyExpand = (expandedItem: HTMLElement) => {
if (props.selectionMode.value === 'single') {
host.querySelectorAll('bit-accordion-item').forEach((item) => {
if (item !== expandedItem && item.hasAttribute('expanded')) {
item.removeAttribute('expanded');
}
});
emit('change', { expandedItem });
}
};
provide(ACCORDION_CTX, {
notifyExpand,
selectionMode: computed(() => props.selectionMode.value),
size: props.size,
variant: props.variant,
});
// Listen for expanded events bubbling up from child accordion-items.
// This allows single-selection management without tight coupling via context calls.
const handleExpand = (e: Event) => notifyExpand(e.target as HTMLElement);
handle(host, 'expand', handleExpand);
// Group-level arrow-key navigation between accordion item summaries (WAI-ARIA Accordion pattern).
handle(host, 'keydown', (e: KeyboardEvent) => {
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp' && e.key !== 'Home' && e.key !== 'End') return;
const items = [...host.querySelectorAll<HTMLElement>('bit-accordion-item:not([disabled])')];
const summaries = items
.map((item) => item.shadowRoot?.querySelector<HTMLElement>('summary'))
.filter(Boolean) as HTMLElement[];
if (!summaries.length) return;
const focused = summaries.indexOf(document.activeElement as HTMLElement);
if (focused === -1) return; // the focus is not on a summary — let native handling proceed
let next = focused;
if (e.key === 'ArrowDown') next = (focused + 1) % summaries.length;
else if (e.key === 'ArrowUp') next = (focused - 1 + summaries.length) % summaries.length;
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = summaries.length - 1;
e.preventDefault();
summaries[next]?.focus();
});
return html`<slot></slot>`;
},
styles: [styles],
tag: 'bit-accordion',
});View Source Code (Accordion Item)
import {
computed,
defineComponent,
handle,
html,
typed,
inject,
onMount,
ref,
syncContextProps,
watch,
} from '@vielzeug/craftit';
import type { ComponentSize, VisualVariant } from '../../types';
import { coarsePointerMixin } from '../../styles';
import { ACCORDION_CTX } from '../accordion/accordion';
import styles from './accordion-item.css?inline';
/** Accordion item component properties */
export type BitAccordionItemEvents = {
collapse: { expanded: boolean; item: HTMLElement };
expand: { expanded: boolean; item: HTMLElement };
};
export type BitAccordionItemProps = {
/** Disable accordion item interaction */
disabled?: boolean;
/** Whether the item is expanded/open */
expanded?: boolean;
/** Item size */
size?: ComponentSize;
/** Visual style variant */
variant?: VisualVariant;
};
/**
* An individual accordion item with expand/collapse functionality using native details/summary.
*
* @element bit-accordion-item
*
* @attr {boolean} expanded - Whether the item is expanded/open
* @attr {boolean} disabled - Disable accordion item interaction
* @attr {string} size - Item size: 'sm' | 'md' | 'lg'
* @attr {string} variant - Visual variant: 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost' | 'text' | 'glass' | 'frost'
*
* @fires expand - Emitted when item expands
* @fires collapse - Emitted when item collapses
*
* @slot prefix - Content before the title (e.g., icons)
* @slot title - Main accordion item title
* @slot subtitle - Optional subtitle text
* @slot suffix - Content after the title (e.g., badges)
* @slot - Accordion item content (shown when expanded)
*
* @cssprop --accordion-item-bg - Background color
* @cssprop --accordion-item-border-color - Border color
* @cssprop --accordion-item-title-color - Title text color
* @cssprop --accordion-item-subtitle-color - Subtitle text color
* @cssprop --accordion-item-body-color - Body text color
* @cssprop --accordion-item-radius - Border radius
* @cssprop --accordion-item-transition - Transition duration
* @cssprop --accordion-item-title - Title font size
* @cssprop --accordion-item-subtitle-size - Subtitle font size
* @cssprop --accordion-item-body - Body font size
* @cssprop --accordion-item-details-padding - Summary/header padding
* @cssprop --accordion-item-summary-padding - Content padding
*
* @example
* ```html
* <bit-accordion-item><span slot="title">Click to expand</span><p>Content</p></bit-accordion-item>
* <bit-accordion-item expanded variant="bordered"><span slot="title">Title</span><p>Content</p></bit-accordion-item>
* ```
*/
export const ACCORDION_ITEM_TAG = defineComponent<BitAccordionItemProps, BitAccordionItemEvents>({
props: {
disabled: typed<boolean>(false),
expanded: typed<boolean>(false),
size: typed<BitAccordionItemProps['size']>(undefined),
variant: typed<BitAccordionItemProps['variant']>(undefined),
},
setup({ emit, host, props }) {
// Inherit size/variant from a parent bit-accordion when present.
const accordionCtx = inject(ACCORDION_CTX, undefined);
syncContextProps(accordionCtx, props, ['size', 'variant']);
const titleId = 'accordion-item-title';
const detailsRef = ref<HTMLDetailsElement>();
const summaryRef = ref<HTMLElement>();
const handleToggle = () => {
const isOpen = detailsRef.value?.open ?? false;
// Notify accordion parent for single-selection management
if (isOpen && !host.hasAttribute('expanded')) {
host.setAttribute('expanded', '');
emit('expand', { expanded: true, item: host });
} else if (!isOpen && host.hasAttribute('expanded')) {
host.removeAttribute('expanded');
emit('collapse', { expanded: false, item: host });
}
};
onMount(() => {
const details = detailsRef.value;
const summary = summaryRef.value;
if (!details || !summary) return;
// Sync details.open when expanded prop changes (needs live DOM refs)
watch(
props.expanded,
(v) => {
const expanded = Boolean(v);
details.open = expanded;
summary.setAttribute('aria-expanded', expanded ? 'true' : 'false');
},
{ immediate: true },
);
handle(details, 'toggle', handleToggle);
});
const ariaExpanded = computed(() => (props.expanded.value ? 'true' : 'false'));
const ariaDisabled = computed(() => (props.disabled.value ? 'true' : 'false'));
return html` <details part="item" ?open=${props.expanded} ref=${detailsRef}>
<summary part="summary" :aria-expanded=${ariaExpanded} :aria-disabled=${ariaDisabled} ref=${summaryRef}>
<slot name="prefix"></slot>
<div class="header-content" part="header">
<span class="title" part="title" id="${titleId}">
<slot name="title"></slot>
</span>
<span class="subtitle" part="subtitle">
<slot name="subtitle"></slot>
</span>
</div>
<slot name="suffix"></slot>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="chevron"
part="chevron"
xmlns="http://www.w3.org/2000/svg">
<path d="m 14.999979,5.9999793 -5.9999997,5.9999997 5.9999997,6" />
</svg>
</summary>
<div class="content-wrapper" part="content" role="region" aria-labelledby="${titleId}">
<div class="content-inner">
<slot></slot>
</div>
</div>
</details>`;
},
styles: [coarsePointerMixin, styles],
tag: 'bit-accordion-item',
});Basic Usage
<bit-accordion>
<bit-accordion-item>
<span slot="title">First Section</span>
Content for the first section goes here.
</bit-accordion-item>
</bit-accordion>
<script type="module">
import '@vielzeug/buildit/accordion';
import '@vielzeug/buildit/accordion-item';
</script>Visual Options
Selection Modes
Multiple (Default)
Allow multiple items to be expanded simultaneously.
Single
Only one item can be expanded at a time.
Variants
Eight visual variants applied to all items via the parent accordion.
Glass & Frost Variants
Modern effects with backdrop blur for elevated UI elements.
Best Used With
Glass and frost variants work best when placed over colorful backgrounds or images to showcase the blur and transparency effects.
Sizes
Three sizes for different contexts.
Customization
Icons & Subtitles
Add icons or descriptive subtitles using slots.
States
Disabled
Prevent interaction with specific items.
API Reference
bit-accordion Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
selection-mode | 'single' | 'multiple' | 'multiple' | Whether multiple items can be expanded simultaneously |
variant | 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost' | 'text' | 'solid' | Visual variant applied to all items |
size | 'sm' | 'md' | 'lg' | 'md' | Size applied to all items |
bit-accordion-item Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
expanded | boolean | false | Whether the item is expanded |
disabled | boolean | false | Disable the item (prevents toggling) |
variant | 'solid' | 'flat' | 'bordered' | 'outline' | 'ghost' | 'text' | 'solid' | Visual variant (usually set via parent) |
size | 'sm' | 'md' | 'lg' | 'md' | Size (usually set via parent) |
Slots
bit-accordion-item
| Slot | Description |
|---|---|
| (default) | Content shown when item is expanded |
title | Title/summary content |
subtitle | Subtitle text shown below the title |
prefix | Content before the title (icons, etc.) |
suffix | Content after the title (badges, custom chevron, etc.) |
Events
bit-accordion
| Event | Detail | Description |
|---|---|---|
change | { expandedItem: HTMLElement | null } | Emitted when selection changes (single mode only) |
bit-accordion-item
| Event | Detail | Description |
|---|---|---|
expand | { expanded: true, item: HTMLElement } | Emitted when the item is expanded |
collapse | { expanded: false, item: HTMLElement } | Emitted when the item is collapsed |
CSS Custom Properties
| Property | Description | Default |
|---|---|---|
--accordion-item-bg | Background color | transparent |
--accordion-item-radius | Border radius | 0.375rem |
--accordion-item-padding | Inner padding | Size-dependent |
--accordion-item-transition | Transition duration for expand/collapse animation | var(--transition-normal) |
Accessibility
The accordion component follows WAI-ARIA Accordion Pattern best practices.
bit-accordion
✅ Native Semantics
- Built with native
<details>and<summary>elements. - Progressive enhancement - works without JavaScript.
✅ Smooth Animation
- Content height transitions via
grid-template-rows: 0fr → 1fr— no JavaScript height calculations and no layout thrashing. - Respects
prefers-reduced-motion: the transition plays only when the user hasn’t opted out of animations. - Override the speed with
--accordion-item-transition.
✅ Keyboard Navigation
EnterandSpacetoggle expansion.Tabmoves focus between accordion items.
Best Practices
Do:
- Use clear, descriptive titles.
- Use
singlemode for mutually exclusive content.
Don't:
- Nest accordions deeply (max 1-2 levels).
- Hide critical information in a collapsed state.