Spaces:
Running
Running
--- | |
const { title, open = false, class: className, ...props } = Astro.props; | |
const wrapperClass = ["accordion", className].filter(Boolean).join(" "); | |
--- | |
<details class={wrapperClass} open={open} {...props}> | |
<summary class="accordion__summary"> | |
<span class="accordion__title">{title}</span> | |
<svg class="accordion__chevron" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"> | |
<path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> | |
</svg> | |
</summary> | |
<div class="accordion__content-wrapper"> | |
<div class="accordion__content"> | |
<slot /> | |
</div> | |
</div> | |
</details> | |
<script> | |
// Animate open/close by transitioning explicit height | |
document.addEventListener('DOMContentLoaded', () => { | |
const accordions = document.querySelectorAll('.accordion') as NodeListOf<HTMLDetailsElement>; | |
accordions.forEach((acc) => { | |
const summary = acc.querySelector('summary.accordion__summary') as HTMLElement | null; | |
const wrapper = acc.querySelector('.accordion__content-wrapper') as HTMLElement | null; | |
const content = acc.querySelector('.accordion__content') as HTMLElement | null; | |
if (!summary || !wrapper || !content) return; | |
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; | |
const duration = reduceMotion ? 0 : 240; | |
// Initial state | |
wrapper.style.overflow = 'hidden'; | |
wrapper.style.height = acc.open ? 'auto' : '0px'; | |
const open = () => { | |
wrapper.style.height = '0px'; | |
void wrapper.offsetHeight; // reflow | |
const target = content.scrollHeight; | |
wrapper.style.transition = `height ${duration}ms ease`; | |
wrapper.style.height = `${target}px`; | |
const onEnd = () => { | |
wrapper.style.transition = ''; | |
wrapper.style.height = 'auto'; | |
wrapper.removeEventListener('transitionend', onEnd); | |
}; | |
wrapper.addEventListener('transitionend', onEnd); | |
}; | |
const close = () => { | |
const start = wrapper.offsetHeight || content.scrollHeight; | |
wrapper.style.height = `${start}px`; | |
void wrapper.offsetHeight; // reflow | |
wrapper.style.transition = `height ${duration}ms ease`; | |
wrapper.style.height = '0px'; | |
const onEnd = () => { | |
wrapper.style.transition = ''; | |
wrapper.removeEventListener('transitionend', onEnd); | |
acc.removeAttribute('open'); | |
}; | |
wrapper.addEventListener('transitionend', onEnd); | |
}; | |
summary.addEventListener('click', (e) => { | |
e.preventDefault(); | |
if (acc.open) { | |
close(); | |
} else { | |
acc.setAttribute('open', ''); | |
open(); | |
} | |
}); | |
}); | |
}); | |
</script> | |
<style> | |
.accordion { | |
margin: 0 0 var(--spacing-4); | |
padding: 0; | |
border: 1px solid var(--border-color); | |
border-radius: 10px; | |
background: var(--surface-bg); | |
transition: box-shadow 180ms ease, border-color 180ms ease; | |
} | |
.accordion[open] { | |
border-color: color-mix(in oklab, var(--border-color), var(--primary-color) 20%); | |
} | |
.accordion__summary { | |
list-style: none; | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
gap: 4px; | |
padding: 4px; | |
cursor: pointer; | |
color: var(--text-color); | |
user-select: none; | |
position: relative; | |
} | |
/* Remove conditional padding to avoid jump on close */ | |
/* Remove native marker */ | |
.accordion__summary::-webkit-details-marker { | |
display: none; | |
} | |
.accordion__summary::marker { | |
content: ""; | |
} | |
.accordion[size="big"] .accordion__summary { | |
padding: 16px; | |
} | |
.accordion__title { | |
font-weight: 600; | |
} | |
.accordion__chevron { | |
position: absolute; | |
right: 8px; | |
transition: transform 220ms ease; | |
opacity: .85; | |
} | |
.accordion[open] .accordion__chevron { | |
transform: rotate(180deg); | |
} | |
/* Animated expand/collapse using height (controlled in JS) */ | |
.accordion__content-wrapper { | |
overflow: hidden; | |
height: 0px; | |
will-change: height; | |
position: relative; | |
} | |
.accordion__content { | |
margin: 0; | |
padding: 0; | |
} | |
/* Ensure the very last slotted element has no bottom spacing */ | |
.accordion .accordion__content > :global(*:last-child) { | |
padding-bottom: 0 ; | |
margin-bottom: 0 ; | |
} | |
/* Ensure the very first slotted element has no top spacing */ | |
.accordion .accordion__content > :global(*:first-child) { | |
margin-top: 0 ; | |
} | |
/* Content padding: default for direct children, opt-out for code/tables */ | |
.accordion .accordion__content > :global(*) { | |
padding: 8px; | |
} | |
.accordion .accordion__content > :global(.table-scroll), | |
.accordion .accordion__content > :global(pre), | |
.accordion .accordion__content > :global(.code-card) { | |
padding: 0; | |
} | |
/* Separator between header and content when open (edge-to-edge) */ | |
.accordion[open] .accordion__content-wrapper::before { | |
content: ""; | |
position: absolute; | |
left: 0; | |
right: 0; | |
top: 0px; /* space below header */ | |
height: 1px; | |
background: var(--border-color); | |
pointer-events: none; | |
} | |
/* Focus styles for accessibility */ | |
.accordion__summary:focus-visible { | |
outline: 2px solid var(--primary-color); | |
outline-offset: 3px; | |
border-radius: 8px; | |
} | |
</style> | |