<div class="container">
<h2>Single Mode</h2>
<ui-accordion mode="single">
<ui-accordion-item>
<ui-accordion-trigger>
<button type="button">トリガー1</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ1 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item>
<ui-accordion-trigger>
<button type="button">トリガー2</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ2 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion>
<h2>Single Mode + Default value</h2>
<ui-accordion mode="single" value="item-2">
<ui-accordion-item value="item-1">
<ui-accordion-trigger>
<button type="button">トリガー1</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ1 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item value="item-2">
<ui-accordion-trigger>
<button type="button">トリガー2</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ2 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion>
<h2>Single Mode + Collapsible</h2>
<ui-accordion mode="single" collapsible>
<ui-accordion-item>
<ui-accordion-trigger>
<button type="button">トリガー1</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ1 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item>
<ui-accordion-trigger>
<button type="button">トリガー2</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ2 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion>
<h2>Single Mode + Nesting</h2>
<ui-accordion mode="single" collapsible>
<ui-accordion-item>
<ui-accordion-trigger>
<button type="button">トリガー1</button>
</ui-accordion-trigger>
<ui-accordion-content> <ui-accordion mode="single" collapsible>
<ui-accordion-item>
<ui-accordion-trigger>
<button type="button">トリガー1−1</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ1-1 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item>
<ui-accordion-trigger>
<button type="button">トリガー1−2</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ1-2 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion> </ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item>
<ui-accordion-trigger>
<button type="button">トリガー2</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ2 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion>
<h2>Multiple</h2>
<ui-accordion mode="multiple">
<ui-accordion-item>
<ui-accordion-trigger>
<button type="button">トリガー1</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ1 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item>
<ui-accordion-trigger>
<button type="button">トリガー2</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ2 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion>
<h2>Multiple Mode + Default value</h2>
<ui-accordion mode="multiple" value="item-1,item-3">
<ui-accordion-item value="item-1">
<ui-accordion-trigger>
<button type="button">トリガー1</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ1 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item value="item-2">
<ui-accordion-trigger>
<button type="button">トリガー2</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ2 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item value="item-3">
<ui-accordion-trigger>
<button type="button">トリガー3</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ3 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion>
<h2>Multiple Mode + Nesting</h2>
<ui-accordion mode="multiple" value="item-1,item-3">
<ui-accordion-item value="item-1">
<ui-accordion-trigger>
<button type="button">トリガー1</button>
</ui-accordion-trigger>
<ui-accordion-content> <ui-accordion mode="multiple" value="item-1,item-3">
<ui-accordion-item value="item-1">
<ui-accordion-trigger>
<button type="button">トリガー1-1</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ1-1 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item value="item-2">
<ui-accordion-trigger>
<button type="button">トリガー1-2</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ1-2 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item value="item-3">
<ui-accordion-trigger>
<button type="button">トリガー1-3</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ1-3 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion> </ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item value="item-2">
<ui-accordion-trigger>
<button type="button">トリガー2</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ2 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item value="item-3">
<ui-accordion-trigger>
<button type="button">トリガー3</button>
</ui-accordion-trigger>
<ui-accordion-content><div class="inner"> コンテンツ3 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion>
<h2>Header</h2>
<ui-accordion mode="multiple">
<ui-accordion-item value="item-1">
<ui-accordion-header role="heading" level="3">
<ui-accordion-trigger>
<button type="button">トリガー1</button>
</ui-accordion-trigger>
</ui-accordion-header>
<ui-accordion-content><div class="inner"> コンテンツ1 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item value="item-2">
<ui-accordion-header role="heading" level="3">
<ui-accordion-trigger>
<button type="button">トリガー2</button>
</ui-accordion-trigger>
</ui-accordion-header>
<ui-accordion-content><div class="inner"> コンテンツ2 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion>
<h2>Disabled all</h2>
<ui-accordion mode="multiple" disabled>
<ui-accordion-item value="item-1">
<ui-accordion-header role="heading" level="3">
<ui-accordion-trigger>
<button type="button">トリガー1</button>
</ui-accordion-trigger>
</ui-accordion-header>
<ui-accordion-content><div class="inner"> コンテンツ1 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item value="item-2">
<ui-accordion-header role="heading" level="3">
<ui-accordion-trigger>
<button type="button">トリガー2</button>
</ui-accordion-trigger>
</ui-accordion-header>
<ui-accordion-content><div class="inner"> コンテンツ2 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion>
<h2>Disabled item</h2>
<ui-accordion mode="multiple">
<ui-accordion-item value="item-1" disabled>
<ui-accordion-header level="3">
<ui-accordion-trigger>
<button type="button">トリガー1</button>
</ui-accordion-trigger>
</ui-accordion-header>
<ui-accordion-content><div class="inner"> コンテンツ1 </div></ui-accordion-content>
</ui-accordion-item>
<ui-accordion-item value="item-2">
<ui-accordion-header level="3">
<ui-accordion-trigger>
<button type="button">トリガー2</button>
</ui-accordion-trigger>
</ui-accordion-header>
<ui-accordion-content><div class="inner"> コンテンツ2 </div></ui-accordion-content>
</ui-accordion-item>
</ui-accordion>
</div>
<script>
// カスタムイベントを受け取るサンプル
document.querySelectorAll('ui-accordion')[0].addEventListener('onValueChange', (event) => {
console.log('Accordion value changed:', event.detail);
});
</script>
xxxxxxxxxx
:where(ui-accordion, ui-accordion-item, ui-accordion-trigger, ui-accordion-header, ui-accordion-content) {
display: block;
}
ui-accordion {
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
--accordion-content-height: auto;
}
ui-accordion-item {
}
ui-accordion-item + ui-accordion-item {
margin-top: 4px;
}
ui-accordion-trigger button {
width: 100%;
text-align: left;
padding: 12px;
background-color: #eee;
border: none;
border-radius: 4px;
cursor: pointer;
}
ui-accordion-content {
height: var(--accordion-content-height);
overflow: hidden;
transition: height 150ms ease-out;
&[data-starting-style],
&[data-ending-style] {
height: 0;
}
&[hidden] {
content-visibility: hidden;
}
}
/* chromeのみ以下のCSSでアニメーション可能
ui-accordion-content[data-state='open'] {
height: auto;
height: calc-size(auto, size);
} */
/* layout */
.container {
max-width: 500px;
margin: auto;
}
h2 {
margin-top: 32px;
margin-bottom: 8px;
font-size: 1.2rem;
}
.inner {
padding: 10px;
}
xxxxxxxxxx
import { createStore } from "https://esm.sh/zustand/vanilla";
import { subscribeWithSelector } from "https://esm.sh/zustand/middleware";
type AccordionMode = 'single' | 'multiple';
type AccordionValue = string | string[] | null;
interface AccordionStoreState {
value: AccordionValue;
mode: AccordionMode;
collapsible: boolean;
disabled: boolean;
items: UiAccordionItem[];
removeItems: (itemToRemove: UiAccordionItem) => void;
addItems: (newItems: UiAccordionItem[]) => void;
}
interface AccordionItemStoreState {
isOpen: boolean;
value: string | null;
disabled: boolean;
}
const setAttrsElement = (element: HTMLElement | null, attributes: { [key: string]: string | undefined }) => {
for (const [key, value] of Object.entries(attributes)) {
if (value === undefined) {
element?.removeAttribute(key);
} else {
element?.setAttribute(key, value);
}
}
};
export class UiAccordion extends HTMLElement {
unsubscribe: (() => void) | undefined = undefined;
useRootStore = createStore(
subscribeWithSelector<AccordionStoreState>((set) => ({
value: [],
mode: 'multiple',
collapsible: false,
disabled: false,
items: [],
removeItems: (itemToRemove) =>
set((state) => ({
items: state.items.filter((item) => item !== itemToRemove),
})),
addItems: (newItems) =>
set((state) => ({
items: [state.items, newItems],
})),
})),
);
static get observedAttributes() {
return ['disabled', 'value'];
}
constructor() {
super();
}
connectedCallback(): void {
const mode = (this.getAttribute('mode') as AccordionMode) || this.useRootStore.getState().mode;
const attrValue = this.getAttribute('value') || null;
const defaultValue = mode === 'single' ? attrValue : attrValue?.split(',').map((item) => item.trim()) || null;
// 初期状態を store に反映
this.useRootStore.setState({
value: defaultValue,
mode: mode,
collapsible: this.hasAttribute('collapsible'),
disabled: this.hasAttribute('disabled'),
items: [],
});
this.unsubscribe = this.useRootStore.subscribe(
(state) => ({
value: state.value,
}),
(state) => {
// valueの更新時、onValueChangeイベントを発行
this.dispatchEvent(
new CustomEvent('onValueChange', {
detail: {
value: state.value,
},
}),
);
},
);
// attributeChangedCallbackのために接続時に属性の追加
this.setAttribute('data-ready', '');
}
disconnectedCallback(): void {
if (this.unsubscribe) this.unsubscribe();
this.useRootStore.setState({ items: [] });
}
attributeChangedCallback(property: string, oldValue: string | null, newValue: string | null) {
// connectedCallback前にも実行されるためdata-ready属性が存在するか確認
const isReady = this.hasAttribute('data-ready');
if (!isReady) return;
// attribute [disabled] の変更を store に反映
if (property === 'disabled' && oldValue !== newValue) {
const isDisabled = newValue !== null;
this.useRootStore.setState({ disabled: isDisabled });
}
// attribute [value] の変更を store に反映
if (property === 'value' && oldValue !== newValue) {
// singleモードの場合はstring型、multipleモードの場合はstring[]型
if (this.useRootStore.getState().mode === 'single') {
this.useRootStore.setState({ value: newValue });
} else {
const newValues = newValue?.split(',').map((item) => item.trim()) || [];
this.useRootStore.setState({ value: newValues });
}
}
}
// 開いているUiAccordionItemのvalueを取得
private getAccordionValue(): AccordionValue {
const { mode, items } = this.useRootStore.getState();
// singleモードの場合はstring型、multipleモードの場合はstring[]型
if (mode === 'single') {
const openItem = items.find((item) => item.useItemStore.getState().isOpen);
return openItem ? openItem.useItemStore.getState().value : null;
} else {
const openValues = items
.filter((item) => item.useItemStore.getState().isOpen)
.map((item) => item.useItemStore.getState().value)
.filter((item) => item !== null);
return openValues.length ? openValues : null;
}
}
// UiAccordionのvalueを更新し、onValueChangeイベントを発行(UiAccordionItemから実行される)
updateToggledItems() {
const newValue = this.getAccordionValue();
this.useRootStore.setState({
value: newValue,
});
setAttrsElement(this, {
value: Array.isArray(newValue) ? newValue.join(',') : newValue || undefined,
});
}
}
export class UiAccordionItem extends HTMLElement {
private $root: UiAccordion | null = null;
useItemStore = createStore(
subscribeWithSelector<AccordionItemStoreState>((set) => ({
value: null,
isOpen: false,
disabled: false,
})),
);
unsubscribe: (() => void) | undefined = undefined;
unsubscribeRoot: (() => void) | undefined = undefined;
constructor() {
super();
}
static get observedAttributes() {
return ['disabled'];
}
connectedCallback(): void {
this.$root = this.closest('ui-accordion');
if (!this.$root) {
console.error('ui-accordion-item must be child of ui-accordion');
return;
}
const rootState = this.$root.useRootStore.getState();
const attrValue = this.getAttribute('value') || null;
// rootのdisabledを優先
const disabled = rootState.disabled || this.hasAttribute('disabled') || this.useItemStore.getState().disabled;
const isDefaultOpenSingle = attrValue !== null && rootState.value === attrValue;
const isDefaultOpenMultiple = !!rootState.value?.includes(attrValue || '');
const isDefaultOpen = rootState.mode === 'single' ? isDefaultOpenSingle : isDefaultOpenMultiple;
this.useItemStore.setState({
value: attrValue,
isOpen: isDefaultOpen,
disabled: disabled,
});
// store から disabledとisOpenを伝播
this.unsubscribe = this.useItemStore.subscribe(
(state) => ({
disabled: state.disabled,
isOpen: state.isOpen,
}),
(state) => {
setAttrsElement(this, {
'data-state': state.isOpen ? 'open' : 'closed',
'data-disabled': state.disabled ? '' : undefined,
});
},
);
// root store から disabledとvalueを伝播
this.unsubscribeRoot = this.$root.useRootStore.subscribe(
(state) => ({
disabled: state.disabled,
value: state.value,
}),
(state, oldState) => {
if (oldState.disabled !== state.disabled) {
// rootのdisabled 状態を UiAccordionItem で受け取り
const isItemDisabled = this.hasAttribute('disabled');
this.useItemStore.setState({ disabled: state.disabled || isItemDisabled });
}
if (state.value !== oldState.value) {
// rootのvalue を UiAccordionItem で受け取り
// UiAccordionItemのvalueがnullの場合(valueを持っていない場合)は更新しない
const { value, isOpen } = this.useItemStore.getState();
if (value === null) return;
const setOpen = Array.isArray(state.value) ? !!state.value.find((item) => item === value) : state.value === value;
if (setOpen !== isOpen) {
this.toggle(setOpen);
}
}
},
);
// UiAccordionItem を root store に反映
this.$root.useRootStore.getState().addItems([this]);
// attributeChangedCallbackのために接続時に属性の追加
this.setAttribute('data-ready', '');
}
disconnectedCallback(): void {
if (this.unsubscribe) this.unsubscribe();
if (this.unsubscribeRoot) this.unsubscribeRoot();
this.$root?.useRootStore.getState().removeItems(this);
}
attributeChangedCallback(property: string, oldValue: string, newValue: string) {
const isReady = this.hasAttribute('data-ready');
if (!isReady) return;
if (property === 'disabled' && oldValue !== newValue) {
const isDisabled = newValue !== null;
// rootのdisabledがtrueの場合のみdisabledを更新
if (this.$root?.useRootStore.getState().disabled) {
this.useItemStore.setState({ disabled: isDisabled });
}
}
}
toggle(_setOpen?: boolean): void {
if (this.$root) {
const { collapsible, items, mode } = this.$root.useRootStore.getState();
const setOpen = _setOpen ?? !this.useItemStore.getState().isOpen;
// collapsible が false の場合、setOpen = false は無視する
if (!collapsible && !setOpen && mode === 'single') return;
this.useItemStore.setState({ isOpen: setOpen });
// modeがsingleの場合、開いたアイテム以外を閉じる
if (mode === 'single' && setOpen) {
const filteredItems = items.filter((item) => item !== this && item.useItemStore.getState().isOpen);
filteredItems.forEach((item) => {
item.useItemStore.setState({ isOpen: false });
});
}
// root store に反映(ui-accordionのvalue属性が更新される)
this.$root.updateToggledItems();
}
}
open(): void {
this.toggle(true);
}
close(): void {
this.toggle(false);
}
}
export class UiAccordionTrigger extends HTMLElement {
private $parentItem: UiAccordionItem | null = null;
private $button: HTMLButtonElement | null = null;
private unsubscribe: (() => void) | undefined = undefined;
constructor() {
super();
}
connectedCallback(): void {
this.$parentItem = this.closest('ui-accordion-item');
if (!this.$parentItem) {
console.error('ui-accordion-trigger must be child of ui-accordion-item');
return;
}
this.$button = this.querySelector('button:not(:scope ui-accordion *)');
const $content = this.$parentItem.querySelector('ui-accordion-content:not(:scope ui-accordion *)');
const triggerId = this.$button?.id || `accordion-trigger-${Math.random().toString(36).slice(2)}`;
const contentId = $content?.id || `accordion-content-${Math.random().toString(36).slice(2)}`;
const { isOpen, disabled } = this.$parentItem.useItemStore.getState();
this.updateAttrs(isOpen, disabled, triggerId, contentId);
this.unsubscribe = this.$parentItem.useItemStore.subscribe((state) => {
this.updateAttrs(state.isOpen, state.disabled, triggerId, contentId);
});
this.$button?.addEventListener('click', this.handleClick);
}
disconnectedCallback(): void {
if (this.unsubscribe) this.unsubscribe();
this.$button?.removeEventListener('click', this.handleClick);
}
private updateAttrs(isOpen: boolean | undefined, isDisabled: boolean | undefined, triggerId: string, contentId: string): void {
setAttrsElement(this, {
'data-state': isOpen ? 'open' : 'closed',
'data-disabled': isDisabled ? '' : undefined,
});
setAttrsElement(this.$button, {
'data-state': isOpen ? 'open' : 'closed',
'data-disabled': isDisabled ? '' : undefined,
disabled: isDisabled ? '' : undefined,
'aria-expanded': isOpen ? 'true' : 'false',
'aria-controls': contentId,
id: triggerId,
});
}
private handleClick = (): void => {
this.$parentItem?.toggle();
};
}
export class UiAccordionHeader extends HTMLElement {
private $parentItem: UiAccordionItem | null = null;
private unsubscribe: (() => void) | undefined = undefined;
constructor() {
super();
}
connectedCallback(): void {
this.$parentItem = this.closest('ui-accordion-item');
if (!this.$parentItem) {
console.error('ui-accordion-trigger must be child of ui-accordion-item');
return;
}
const level = '3';
const role = this.getAttribute('role');
if (!role) {
this.setAttribute('role', 'heading');
}
if (this.getAttribute('role') === 'heading') {
const ariaLevel = this.getAttribute('level') || this.getAttribute('aria-level') || level;
this.setAttribute('aria-level', ariaLevel);
}
const { isOpen, disabled } = this.$parentItem.useItemStore.getState();
this.updateAttrs(isOpen, disabled);
this.unsubscribe = this.$parentItem.useItemStore.subscribe((state) => {
this.updateAttrs(state.isOpen, state.disabled);
});
}
disconnectedCallback(): void {
if (this.unsubscribe) this.unsubscribe();
}
private updateAttrs(isOpen: boolean | undefined, isDisabled: boolean | undefined): void {
setAttrsElement(this, {
'data-state': isOpen ? 'open' : 'closed',
'data-disabled': isDisabled ? '' : undefined,
});
}
}
export class UiAccordionContent extends HTMLElement {
private $parentItem: UiAccordionItem | null = null;
private unsubscribe: (() => void) | undefined = undefined;
connectedCallback(): void {
this.$parentItem = this.closest('ui-accordion-item');
if (!this.$parentItem) {
console.error('ui-accordion-trigger must be child of ui-accordion-item');
return;
}
const { isOpen, disabled } = this.$parentItem.useItemStore.getState();
const $button = this.$parentItem.querySelector('ui-accordion-trigger button');
const triggerId = $button?.id;
const contentId =
this.id || $button?.getAttribute('aria-controls') || `accordion-content-${Math.random().toString(36).slice(2)}`;
let isTransitioning = false;
this.updateAttrs(isOpen, disabled, triggerId, contentId);
const handleTransitionRun = () => {
this.removeEventListener('transitionrun', handleTransitionRun);
isTransitioning = true;
};
const handleTransitionEnd = () => {
this.removeEventListener('transitionend', handleTransitionEnd);
isTransitioning = false;
this.removeAttribute('data-ending-style');
this.style.removeProperty('--accordion-content-height');
if (this.$parentItem) {
// transition中にopen, closeが切り替わるためUiAccordionItemの状態を確認
const { isOpen, disabled } = this.$parentItem.useItemStore.getState();
if (!isOpen) {
// close
this.updateAttrs(false, disabled, triggerId, contentId);
}
}
};
this.unsubscribe = this.$parentItem.useItemStore.subscribe((state) => {
// transitionの有無を確認
const hasTransition = window.getComputedStyle(this).transitionDuration !== '0s';
if (!hasTransition) {
// transitionなしならシンプルに開閉のみ
this.updateAttrs(state.isOpen, state.disabled, triggerId, contentId);
return;
}
if (isTransitioning) {
// transition中
this.removeAttribute('data-ending-style');
if (state.isOpen) {
// open状態に変更
this.updateAttrs(true, state.disabled, triggerId, contentId);
// アニメーションのための準備
this.style.setProperty('--accordion-content-height', `${this.scrollHeight}px`);
} else {
// アニメーションのための準備
this.style.setProperty('--accordion-content-height', `0px`);
}
} else {
// transition中ではない時
this.style.removeProperty('--accordion-content-height');
if (state.isOpen) {
// open状態に変更
this.updateAttrs(true, state.disabled, triggerId, contentId);
// アニメーションのための準備
this.setAttribute('data-starting-style', '');
this.removeAttribute('data-ending-style');
this.style.setProperty('--accordion-content-height', `${this.scrollHeight}px`);
this.addEventListener('transitionrun', handleTransitionRun);
this.addEventListener('transitionend', handleTransitionEnd);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// アニメーション開始
this.removeAttribute('data-starting-style');
});
});
} else {
// close状態に変更
// アニメーションのための準備
this.style.setProperty('--accordion-content-height', `${this.scrollHeight}px`);
this.addEventListener('transitionrun', handleTransitionRun);
this.addEventListener('transitionend', handleTransitionEnd);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// アニメーション開始
this.setAttribute('data-ending-style', '');
});
});
}
}
});
}
disconnectedCallback(): void {
if (this.unsubscribe) this.unsubscribe();
}
private updateAttrs(
isOpen: boolean | undefined,
isDisabled: boolean | undefined,
triggerId: string | undefined,
contentId: string | undefined,
): void {
setAttrsElement(this, {
'data-state': isOpen ? 'open' : 'closed',
'data-disabled': isDisabled ? '' : undefined,
hidden: !isOpen ? 'until-found' : undefined,
role: 'region',
'aria-labelledby': triggerId,
id: contentId || undefined,
});
}
}
customElements.define('ui-accordion', UiAccordion);
customElements.define('ui-accordion-item', UiAccordionItem);
customElements.define('ui-accordion-header', UiAccordionHeader);
customElements.define('ui-accordion-trigger', UiAccordionTrigger);
customElements.define('ui-accordion-content', UiAccordionContent);
declare global {
interface HTMLElementTagNameMap {
'ui-accordion': UiAccordion;
'ui-accordion-item': UiAccordionItem;
'ui-accordion-header': UiAccordionHeader;
'ui-accordion-trigger': UiAccordionTrigger;
'ui-accordion-content': UiAccordionContent;
}
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.