Accordion

accordion.svelte

A collapsible row that toggles an indented content section.

Demo

package.json

Dependencies live here.

Usage
001 <script lang="ts">
002 import Accordion from '$lib/components/accordion.svelte';
003 </script>
004
005 <div class="flex flex-col gap-2">
006 <Accordion title="README.md">
007 <p>The self-contained file you can copy into any project.</p>
008 </Accordion>
009 <Accordion title="package.json" defaultOpen>
010 <p>Dependencies live here.</p>
011 </Accordion>
012 </div>
013
accordion.svelte
001 <script lang="ts" module>
002 export type AccordionProps = {
003 ref?: HTMLElement | null;
004 defaultOpen?: boolean;
005 title: string;
006 children?: import('svelte').Snippet;
007 };
008 </script>
009
010 <script lang="ts">
011 let { ref = $bindable(null), defaultOpen = false, title, children }: AccordionProps = $props();
012
013 // svelte-ignore state_referenced_locally
014 let open = $state(defaultOpen);
015
016 const toggle = () => (open = !open);
017
018 const onkeydown = (e: KeyboardEvent) => {
019 if (e.key === 'Enter' || e.key === ' ') {
020 e.preventDefault();
021 toggle();
022 }
023 };
024 </script>
025
026 <section
027 bind:this={ref}
028 class="head"
029 tabindex="0"
030 role="button"
031 aria-expanded={open}
032 onclick={toggle}
033 {onkeydown}
034 >
035 <div class="row {open ? 'active' : ''}">
036 <span class="icon" aria-hidden="true">{open ? '▾' : '▸'}</span>
037 <span class="content">{title}</span>
038 </div>
039 </section>
040 {#if open}
041 <section class="body">
042 {@render children?.()}
043 </section>
044 {/if}
045
046 <style>
047 .head {
048 display: block;
049 outline: 0;
050 border: 0;
051 transition: 200ms ease background;
052 }
053 .head:hover,
054 .head:focus-visible {
055 background: var(--focus-ring);
056 }
057 .head:focus-visible {
058 outline: none;
059 }
060
061 .row {
062 display: flex;
063 align-items: center;
064 justify-content: space-between;
065 }
066 .row:hover {
067 background: var(--focus-ring);
068 }
069
070 .icon {
071 flex-shrink: 0;
072 user-select: none;
073 cursor: pointer;
074 }
075
076 .content {
077 min-width: 10%;
078 width: 100%;
079 user-select: none;
080 cursor: pointer;
081 transition: padding 200ms ease;
082 padding-left: 0;
083 }
084
085 .active .content {
086 padding-left: 1ch;
087 }
088
089 .body {
090 display: block;
091 padding-left: 1ch;
092 }
093 </style>
094