| --- |
| 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> |
| |
| 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; |
| |
| |
| wrapper.style.overflow = 'hidden'; |
| wrapper.style.height = acc.open ? 'auto' : '0px'; |
| |
| const open = () => { |
| wrapper.style.height = '0px'; |
| void wrapper.offsetHeight; |
| 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; |
| 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: var(--table-border-radius); |
| 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[open] .accordion__summary { |
| border-bottom: 1px solid var(--border-color); |
| } |
| |
| .accordion__summary { |
| margin: 0; |
| list-style: none; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 4px; |
| padding: var(--spacing-2) var(--spacing-3); |
| cursor: pointer; |
| color: var(--text-color); |
| user-select: none; |
| } |
| |
| |
| |
| |
| .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 { |
| flex: 0 0 auto; |
| transition: transform 220ms ease; |
| opacity: .85; |
| } |
| |
| .accordion[open] .accordion__chevron { |
| transform: rotate(180deg); |
| } |
| |
| |
| .accordion__content-wrapper { |
| overflow: hidden; |
| height: 0px; |
| will-change: height; |
| position: relative; |
| } |
| |
| |
| .accordion[open] .accordion__content-wrapper::before { |
| content: ""; |
| position: absolute; |
| left: 0; |
| right: 0; |
| top: 0; |
| height: 6px; |
| background: linear-gradient(to bottom, rgba(0,0,0,0.025), rgba(0,0,0,0)); |
| pointer-events: none; |
| z-index: 1; |
| } |
| |
| .accordion__content { |
| margin: 0; |
| padding: 0; |
| } |
| |
| |
| .accordion .accordion__content > :global(*:last-child) { |
| margin-bottom: 0 !important; |
| } |
| |
| .accordion .accordion__content > :global(:not(.code-output):last-child) { |
| padding-bottom: 0 !important; |
| } |
| |
| |
| .accordion .accordion__content > :global(*:first-child) { |
| margin-top: 0 !important; |
| } |
| |
| |
| .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; |
| } |
| |
| |
| .accordion__summary:focus-visible { |
| outline: 2px solid var(--primary-color); |
| outline-offset: 3px; |
| border-radius: var(--table-border-radius); |
| } |
| |
| |
| </style> |
| |
| |
| |