Building Collapsible Components with Melt UI in Svelte
Source: Dev.to
Melt UI Collapsible Guide
Melt UI provides headless, accessible component builders for Svelte that follow WAI‑ARIA guidelines. The Collapsible builder lets you 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‑world 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
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"
}
}
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}>
This content can be expanded and collapsed.
</div>
</div>
What createCollapsible() Returns
- elements – Svelte stores for
root,content, andtriggerelements. - states – Reactive stores like
openthat 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">
<button use:melt={trigger} class="px-4 py-2 bg-gray-200">
Toggle Content <span class="ml-2">▼</span>
</button>
<div use:melt={content} class="p-4">
This is the collapsible content area.
</div>
</div>
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>
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(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) {
const isOpen = writable(openIndex.get() === index);
const unsubscribe = openIndex.subscribe(value => {
isOpen.set(value === index);
});
return {
collapsible: createCollapsible({ open: isOpen }),
unsubscribe
};
}
</script>
{#each items as item, index}
{@const { collapsible, unsubscribe } = createItemCollapsible(index)}
{@const { elements: { root, content, trigger }, states: { open } } = collapsible}
<div use:melt={root} class="border-b">
<button
use:melt={trigger}
on:click={() => openIndex.set($open ? null : index)}
class="w-full px-4 py-2 text-left"
>
{item.title}
</button>
{#if $open}
<div use:melt={content} class="p-4">
{item.content}
</div>
{/if}
</div>
{/each}
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}>
{$open ? 'Hide' : 'Show'} details
</button>
{#if $open}
<div use:melt={content} transition:slide>
Animated collapsible content.
</div>
{/if}
</div>
Conclusion
Melt UI’s headless, accessible Collapsible builder gives you the flexibility to craft fully custom, ARIA‑compliant expandable sections in Svelte. By leveraging the builder pattern, you retain complete control over markup, styling, and state management while benefiting from built‑in accessibility and keyboard support. Happy coding!
<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>
This content slides in and out.
</div>
{/if}
</div>
Fullscreen Controls
- Enter fullscreen mode
- Exit fullscreen mode
Common Problems / Troubleshooting
Collapsible doesn’t toggle
- Ensure the
meltaction is applied to all three elements (root,trigger,content). - Check that stores are accessed with the
$prefix:$open. - Verify the builder is called once during component initialization (not conditionally).
Accessibility attributes missing
- Make sure the
meltaction is applied to the elements and isn’t removed by conditional rendering. - Do not manually add ARIA attributes – the builder handles them automatically.
TypeScript errors
- Import types from
@melt-ui/sveltewhen needed. - Use Svelte v4+ for the best TypeScript support.
Content not animating
- When using
{#if}conditional rendering, ensure themeltaction remains applied. - Apply Svelte transitions (e.g.,
transition:slide) to the content element, not the root.
Production Best Practices
- Always use the
meltaction on all builder elements to guarantee accessibility. - Test keyboard navigation (
Tab,Enter,Space,Escape). - Verify ARIA attributes with screen readers.
- Keep styling separate from logic – Melt UI handles behavior; you handle appearance.
- Prefer CSS transitions over JavaScript animations for better performance.
- For accordions, allow only one item to be open at a time for improved UX.
- Clean up subscriptions when components are destroyed to avoid memory leaks.
Final Notes
Melt UI’s Collapsible builder gives you a solid foundation for expandable content with minimal setup. Its headless approach lets you fully control styling while maintaining accessibility compliance.
Once collapsibles are 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.