Accessible Collapsible Components in Svelte with Melt UI
Collapsible UI — accordions, panels, and disclosure widgets — are one of those deceptively simple frontend patterns that can break accessibility, keyboard navigation, and layout if implemented without care. This article walks through a practical, production-ready approach to building accessible collapsible components in Svelte (SvelteKit + TypeScript), styled with Tailwind CSS and easily swapped to Melt UI primitives or other UI component libraries.
Expect an implementation-first narrative: why accessibility matters, how WAI-ARIA maps onto the DOM for a collapsible, how to wire keyboard interactions, and how to keep things modular for webcomponents, frontend performance, and SEO. If you prefer to jump to a Melt UI-specific example, see this writeup on building collapsible components with Melt UI in Svelte for details and primitives. Read the Melt UI guide.
Code samples use TypeScript in Svelte with minimal dependencies. The patterns translate to JavaScript projects, and they’re friendly to hybrid approaches (native code with Melt UI wrappers or full Melt UI components).
Why Melt UI + Svelte (and when to prefer primitives)
Melt UI offers accessible, headless primitives that map well to Svelte’s reactive model. If you’re building many UI components or need consistent keyboard/screen-reader behavior, Melt UI speeds development and reduces regressions around WAI-ARIA. For small projects or when you need total control, a lightweight native implementation (using semantic elements and aria attributes) is often faster and smaller.
From a webdev and frontend engineering perspective, Melt UI is a pragmatic abstraction: it implements tested keyboard patterns and focus management while remaining unopinionated about styling. That makes it easy to layer TailwindCSS or custom themes over the primitives without fighting internal styles. It also pairs well with SvelteKit and TypeScript, keeping props strongly typed.
If your priority is component portability as webcomponents, prefer primitives or webcomponent-friendly wrappers and rely on consistent ARIA attributes. Melt UI’s primitives can be used as the control plane while you expose a webcomponent interface for other teams or a design system.
Implementing an Accessible Collapsible (Accordion) in SvelteKit + TypeScript
The smallest accessible pattern uses a button that toggles an associated region. The button carries aria-expanded and aria-controls; the content has role=”region” and an id the button references. This pattern is robust for screen readers and straightforward to test. The sample below uses Svelte syntax with TypeScript for clarity.
The example deliberately keeps styling separate (TailwindCSS classes inline) and focuses on semantics and keyboard behavior. It’s easy to swap internal toggle logic with Melt UI primitives or a library call: replace the local store/state with Melt’s hooks or components and keep the same ARIA attributes.
<script lang="ts">
import { onMount } from 'svelte';
export let title: string = 'Details';
let open = false;
let contentId = 'collapsible-' + Math.random().toString(36).slice(2,9);
// Optional: manage focus when opening for better UX
let contentEl: HTMLElement | null = null;
function toggle() {
open = !open;
if (open) {
// small delay before focus to preserve screen reader behavior
setTimeout(() => contentEl?.focus(), 50);
}
}
</script>
<div class="w-full max-w-xl mx-auto">
<button
class="w-full flex justify-between items-center p-3 bg-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
aria-expanded={open}
aria-controls={contentId}
on:click={toggle}
>
<span class="font-medium">{title}</span>
<svg class="w-5 h-5 transform {open ? 'rotate-180' : ''}" viewBox="0 0 20 20">
<path d="M5 8l5 5 5-5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div
id={contentId}
role="region"
aria-labelledby={contentId + '-label'}
class="mt-2 overflow-hidden transition-all duration-200"
bind:this={contentEl}
tabindex="-1"
hidden={!open}
>
<div class="p-4 bg-white border rounded-md">
<p>This is the collapsible content. It is keyboard-focusable when opened and hidden from assistive tech when closed.</p>
</div>
</div>
</div>
Notes: using hidden toggles both visibility and accessibility in many browsers; ensure you also manage tabindex and focus to create a smooth keyboard experience. The pattern above focuses the content region on open; alternate UX is to focus the first interactive element inside the region.
To convert this to use Melt UI primitives, replace the toggle state with the primitive’s controller (for example, a Collapsible or Disclosure hook/component), preserve aria-expanded/aria-controls semantics, and keep the same structural DOM for compatibility with assistive technologies. See the Melt UI example for exact component names and props: building collapsible components with Melt UI in Svelte.
Keyboard, Focus, and WAI-ARIA: Hard Requirements, Not Suggestions
For an accordion-like pattern, implement: Enter/Space to toggle the focused header, Arrow keys to move between headers (if you implement a multi-item accordion), Home/End to jump to first/last header, and clear focus styles. WAI-ARIA Authoring Practices spell out the exact behaviors — follow them when building UI components to avoid regressions in assistive tech.
The minimal, single-panel collapsible only needs aria-expanded and aria-controls plus proper focus management. Multi-panel accordions should use role=”button” on the headers or native button elements (preferred) and role=”region” on the content panels. Use ids consistently and programmatically generate them to avoid collisions in larger apps or webcomponents.
Automated accessibility tests help. Add axe-core checks and unit tests to assert aria attributes, keyboard behavior, and DOM updates. In CI, run a headless axe or Playwright run that simulates keyboard navigation to ensure repeated changes don’t break required interactions.
Styling with Tailwind CSS, Theming, and Performance Considerations
Tailwind CSS pairs well with the minimal DOM structure of accessible collapsibles. Use utility classes for transitions and state-based styles but avoid using CSS to hide content while leaving it visible to screen readers (e.g., use [hidden] or aria-hidden carefully). Tailwind’s JIT makes keeping CSS payload small straightforward.
Keep the collapse animation performant: animate max-height or transform where feasible, and avoid heavy layout-triggering properties. If you animate height, prefer a measured approach (set explicit heights or use the Web Animations API/transition with JavaScript) to avoid reflows on large content.
For SvelteKit and frontend optimization, prefer server-rendered closed state for accordions that should be indexable or visible to crawlers (improves SEO). For dynamically loaded content, ensure the content is reachable by screen readers after hydration and that your ARIA attributes reflect the post-hydration state.
Testing and Integration: TypeScript, Webcomponents, and Deployment
TypeScript gives you confidence for component contracts (props, events). Create interface types for your collapsible API: props for initial open state, optional id prefix, event callbacks (on:open, on:close), and accessibility overrides. Keep the API small and explicit.
If you need a webcomponent surface, wrap the Svelte component in a custom element with defined attributes mapping to props. Ensure attribute changes update aria attributes and that the webcomponent offers the same keyboard interactions as the native Svelte component.
For continuous delivery, include visual (Storybook) and accessibility tests. Storybook stories let designers and QA interact with the component in isolation; add Axe plugin and keyboard interaction stories to catch regressions early.
Practical Tips & Pitfalls
1) Never rely solely on display: none for accessibility decisions — pair visibility with aria-hidden or the hidden attribute where appropriate. 2) Avoid replacing semantics with complex div/button mixes; native button elements have built-in keyboard and role behavior which you’d otherwise reimplement. 3) Keep the collapsible’s content focusable and ensure focus is never lost off-screen after toggling.
If using Melt UI, you gain tested semantics and focus management; still validate the resulting DOM and aria attributes match your expectations. Tools like devtools accessibility pane and automated axe checks are your friends.
Finally, document the component’s behavior in your codebase: note the expected ARIA, keyboard interactions, and any caveats around animation or nested interactive elements inside the collapsible.
Semantic Core (Expanded)
- Primary: webdev, svelte, melt-ui, sveltekit, collapsible, accordion, accessibility
- Secondary: typescript, tailwindcss, javascript, frontend, ui-components, wai-aria, webcomponents
- Clarifying / LSI: keyboard navigation, aria-expanded, aria-controls, disclosure widget, focus management, accessible accordion, headless UI, component API, performance
FAQ
How do I create an accessible accordion in Svelte using Melt UI?
Use a headless primitive (or native semantic elements) that exposes the open/close state and manages focus. Preserve aria-expanded on the trigger and aria-controls on the region. You can implement the pattern yourself with a button + role=”region” or replace the state logic with Melt UI primitives to inherit keyboard/focus behavior; see the Melt UI guide for component examples.
Can I style Melt UI components with Tailwind CSS and TypeScript in SvelteKit?
Absolutely. Melt UI is headless, so you provide the markup and classes. In SvelteKit + TypeScript, pass Tailwind utility classes directly to the wrapper elements or use class composition. Typescript ensures props and events are typed while Tailwind controls the visual layer.
Does Melt UI support WAI-ARIA and keyboard navigation out of the box?
Melt UI primitives implement common accessibility patterns (WAI-ARIA attributes and keyboard interactions) so you get consistent behavior across components. Always verify the rendered DOM and run automated accessibility tests — libraries help, but verification is part of shipping accessible UI.
Useful links: the Melt UI guide for a hands-on walkthrough and examples (building collapsible components with Melt UI in Svelte), WAI-ARIA Authoring Practices, and the Tailwind docs for transitions and accessibility utilities.

