DEV Community

Cover image for Building Collapsible Components with Melt UI in Svelte
Fabricio Viskor
Fabricio Viskor

Posted on

Building Collapsible Components with Melt UI in Svelte

Melt UI provides headless, accessible component builders for Svelte that follow WAI-ARIA guidelines. The Collapsible builder allows you to create expandable/collapsible content sections with full keyboard navigation and screen reader support out of the box.

This guide walks through a practical, production-ready implementation of collapsible components using Melt UI's createCollapsible builder. The focus is on real usage patterns, not abstractions.

Why Use @melt-ui/svelte (Melt UI) with Svelte

Svelte applications often need expandable content sections for FAQs, accordions, navigation menus, and detail views. Melt UI's Collapsible builder fits naturally into this ecosystem because it provides:

  • Full WAI-ARIA compliance for accessibility
  • Headless design (you control all styling)
  • TypeScript support with excellent type safety
  • Simple, declarative API
  • Built-in keyboard navigation
  • No external dependencies beyond Svelte

The builder pattern allows you to attach collapsible behavior to any elements you want, giving you complete control over the markup and styles.

Requirements

Before starting, make sure you have:

  • A Svelte project (SvelteKit or standalone Svelte)
  • Node.js 16+ and npm/pnpm/yarn
  • Basic familiarity with Svelte components and stores

Installation

Install Melt UI using your preferred package manager:

npm install @melt-ui/svelte
# or
pnpm add @melt-ui/svelte
# or
yarn add @melt-ui/svelte
Enter fullscreen mode Exit fullscreen mode

The package includes all builders and their TypeScript definitions.

Configuration

No additional configuration is required. Melt UI works out of the box with Svelte. If you're using TypeScript, ensure your tsconfig.json includes proper module resolution:

{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "target": "ES2020"
  }
}
Enter fullscreen mode Exit fullscreen mode

Basic Usage

The simplest way to create a collapsible section is using the createCollapsible builder:

<script>
  import { createCollapsible, melt } from '@melt-ui/svelte'

  const {
    elements: { root, content, trigger },
    states: { open }
  } = createCollapsible()
</script>

<div use:melt={$root}>
  <button use:melt={$trigger}>
    {$open ? 'Close' : 'Open'}
  </button>
  <div use:melt={$content}>
    <p>This content can be expanded and collapsed.</p>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

The createCollapsible() function returns:

  • elements: Svelte stores for root, content, and trigger elements
  • states: Reactive stores like open that track the collapsible state

Use the melt action to attach the builder's behavior to your elements. The open store is reactive and updates automatically when the collapsible state changes.

Advanced Features

Styling with Tailwind CSS

Since Melt UI is headless, you have complete control over styling:

<script>
  import { createCollapsible, melt } from '@melt-ui/svelte'

  const {
    elements: { root, content, trigger },
    states: { open }
  } = createCollapsible()
</script>

<div 
  use:melt={$root}
  class="border rounded-lg overflow-hidden"
>
  <button 
    use:melt={$trigger}
    class="w-full px-4 py-3 text-left font-medium bg-gray-100 hover:bg-gray-200 flex items-center justify-between transition-colors"
  >
    <span>Toggle Content</span>
    <span class="transform transition-transform {$open ? 'rotate-180' : ''}"></span>
  </button>
  <div 
    use:melt={$content}
    class="px-4 py-3 bg-white"
  >
    <p>This is the collapsible content area.</p>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Controlled State

To control the collapsible state externally, pass a writable store:

<script>
  import { createCollapsible, melt } from '@melt-ui/svelte'
  import { writable } from 'svelte/store'

  const isOpen = writable(false)

  const {
    elements: { root, content, trigger },
    states: { open }
  } = createCollapsible({
    open: isOpen
  })

  function toggle() {
    isOpen.update(n => !n)
  }
</script>

<div use:melt={$root}>
  <button use:melt={$trigger} on:click={toggle}>
    {$open ? 'Close' : 'Open'}
  </button>
  <div use:melt={$content}>
    Controlled content
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Creating an Accordion

Build an accordion by managing multiple collapsible states:

<script>
  import { createCollapsible, melt } from '@melt-ui/svelte'
  import { writable } from 'svelte/store'

  const openIndex = writable<number | null>(null)

  const items = [
    { id: 1, title: 'Item 1', content: 'Content for item 1' },
    { id: 2, title: 'Item 2', content: 'Content for item 2' },
    { id: 3, title: 'Item 3', content: 'Content for item 3' }
  ]

  function createItemCollapsible(index: number) {
    const isOpen = writable(openIndex.get() === index)

    const unsubscribe = openIndex.subscribe(value => {
      isOpen.set(value === index)
    })

    return {
      collapsible: createCollapsible({ open: isOpen }),
      unsubscribe
    }
  }
</script>

<div class="space-y-2">
  {#each items as item, index}
    {@const { collapsible, unsubscribe } = createItemCollapsible(index)}
    {@const { elements: { root, content, trigger }, states: { open } } = collapsible}

    <div use:melt={$root} class="border rounded">
      <button 
        use:melt={$trigger}
        on:click={() => openIndex.set($open ? null : index)}
        class="w-full px-4 py-2 text-left"
      >
        {item.title}
      </button>
      <div use:melt={$content}>
        <div class="px-4 py-2">{item.content}</div>
      </div>
    </div>
  {/each}
</div>
Enter fullscreen mode Exit fullscreen mode

Custom Animations

Add animations using Svelte's transition directives:

<script>
  import { createCollapsible, melt } from '@melt-ui/svelte'
  import { slide } from 'svelte/transition'

  const {
    elements: { root, content, trigger },
    states: { open }
  } = createCollapsible()
</script>

<div use:melt={$root}>
  <button use:melt={$trigger}>Toggle</button>
  {#if $open}
    <div use:melt={$content} transition:slide>
      <p>This content slides in and out.</p>
    </div>
  {/if}
</div>
Enter fullscreen mode Exit fullscreen mode

Common Problems / Troubleshooting

Collapsible doesn't toggle

  • Ensure melt action is applied to all three elements (root, trigger, content)
  • Check that stores are accessed with $ prefix: $root, $trigger, $content
  • Verify the builder is called in component initialization, not conditionally

Accessibility attributes missing

  • Make sure melt action is applied to elements, not removed by conditional rendering
  • Don't manually add ARIA attributes - the builder handles them automatically

TypeScript errors

  • Import types from @melt-ui/svelte if needed
  • Ensure Svelte version is 4+ for best TypeScript support

Content not animating

  • When using conditional rendering with {#if}, ensure the melt action is still applied
  • Use Svelte transitions on the content element, not the root

Production Best Practices

  • Always use the melt action on all builder elements to ensure accessibility
  • Test with keyboard navigation (Tab, Enter, Space, Escape)
  • Use screen readers to verify ARIA attributes work correctly
  • Keep styling separate from logic - Melt UI handles behavior, you handle appearance
  • Consider using CSS transitions instead of JavaScript animations for better performance
  • If building an accordion, ensure only one item is open at a time for better UX
  • Clean up subscriptions when components are destroyed to prevent memory leaks

Final Notes

Melt UI's Collapsible builder provides a solid foundation for expandable content with minimal setup. The headless approach gives you complete control over styling while ensuring accessibility compliance.

Once you have collapsibles working, common next steps include:

  • Building accordion components with shared state
  • Adding custom animations and transitions
  • Integrating with routing for expandable navigation menus
  • Creating detail views with collapsible sections

Top comments (0)