Skip to main content
Technology & EngineeringUi Components Services185 lines

Melt UI

Melt UI: headless, accessible UI component builders for Svelte using the builder pattern with full styling freedom

Quick Summary20 lines
You are an expert in building interfaces with Melt UI.

## Key Points

- Use the `melt` action (`use:melt={$builder}`) rather than manually spreading attributes; it handles both attribute spreading and event listeners in one step.
- Leverage the preprocessor (`@melt-ui/pp`) for a cleaner template syntax that avoids excessive `use:melt` directives in complex component trees.
- Keep builder creation in `<script>` and destructure `elements` and `states` separately for clarity and IDE autocompletion.
- Forgetting to subscribe to builder stores with `$` in templates: `use:melt={$trigger}` not `use:melt={trigger}`. Without the `$`, Svelte won't reactively update attributes.

## Quick Example

```bash
npm install @melt-ui/svelte
```

```bash
npm install -D @melt-ui/pp
```
skilldb get ui-components-services-skills/Melt UIFull skill: 185 lines
Paste into your CLAUDE.md or agent config

Melt UI — UI Components

You are an expert in building interfaces with Melt UI.

Core Philosophy

Overview

Melt UI is a headless UI component library designed specifically for Svelte. It provides accessible, unstyled component "builders" that return reactive attributes and event handlers you spread onto your own HTML elements. This builder pattern gives you complete control over markup and styling while Melt UI handles keyboard navigation, ARIA attributes, focus management, and state logic. It supports Svelte 4 and integrates naturally with SvelteKit.

Setup & Configuration

Installation

npm install @melt-ui/svelte

Optionally install the preprocessor for a cleaner API:

npm install -D @melt-ui/pp

Add the preprocessor to svelte.config.js:

import { preprocessMeltUI, sequence } from '@melt-ui/pp'

const config = {
  preprocess: sequence([
    vitePreprocess(),
    preprocessMeltUI(),
  ]),
}

export default config

Core Patterns

Builder pattern (standard API)

Every component in Melt UI starts with a create* function that returns builders. Each builder provides an action and reactive attributes to spread onto elements:

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

  const {
    elements: { trigger, overlay, content, title, description, close },
    states: { open },
  } = createDialog()
</script>

<button use:melt={$trigger}>Open Dialog</button>

{#if $open}
  <div use:melt={$overlay} class="fixed inset-0 bg-black/50" />
  <div use:melt={$content} class="fixed inset-1/2 -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg">
    <h2 use:melt={$title}>Dialog Title</h2>
    <p use:melt={$description}>Dialog description text.</p>
    <button use:melt={$close}>Close</button>
  </div>
{/if}

Select component

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

  const {
    elements: { trigger, menu, option, label },
    states: { selectedLabel, open },
  } = createSelect({
    forceVisible: true,
    positioning: { placement: 'bottom', fitViewport: true },
  })

  const options = [
    { value: 'react', label: 'React' },
    { value: 'svelte', label: 'Svelte' },
    { value: 'vue', label: 'Vue' },
  ]
</script>

<div>
  <label use:melt={$label}>Framework</label>
  <button use:melt={$trigger}>
    {$selectedLabel || 'Select a framework'}
  </button>

  {#if $open}
    <ul use:melt={$menu}>
      {#each options as item}
        <li use:melt={$option({ value: item.value, label: item.label })}>
          {item.label}
        </li>
      {/each}
    </ul>
  {/if}
</div>

Tooltip

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

  const {
    elements: { trigger, content, arrow },
    states: { open },
  } = createTooltip({
    positioning: { placement: 'top' },
    openDelay: 200,
    closeDelay: 0,
  })
</script>

<button use:melt={$trigger}>Hover me</button>

{#if $open}
  <div use:melt={$content} class="bg-gray-900 text-white px-3 py-1.5 rounded text-sm">
    <div use:melt={$arrow} />
    Tooltip content
  </div>
{/if}

Controlled state

Builders accept an optional initial value and return writable stores so you can control state externally:

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

  const {
    elements: { root, input },
    states: { checked },
  } = createSwitch({ defaultChecked: true })

  // Programmatic control
  function reset() {
    checked.set(false)
  }
</script>

<button use:melt={$root} class="w-11 h-6 rounded-full" class:bg-blue-600={$checked} class:bg-gray-300={!$checked}>
  <span class="block w-5 h-5 rounded-full bg-white transition-transform" class:translate-x-5={$checked} />
</button>
<input use:melt={$input} />

Best Practices

  • Use the melt action (use:melt={$builder}) rather than manually spreading attributes; it handles both attribute spreading and event listeners in one step.
  • Leverage the preprocessor (@melt-ui/pp) for a cleaner template syntax that avoids excessive use:melt directives in complex component trees.
  • Keep builder creation in <script> and destructure elements and states separately for clarity and IDE autocompletion.

Common Pitfalls

  • Forgetting to subscribe to builder stores with $ in templates: use:melt={$trigger} not use:melt={trigger}. Without the $, Svelte won't reactively update attributes.
  • Using {#if $open} for content that needs exit transitions: Svelte removes the element immediately. Use the forceVisible: true option and handle visibility with CSS or Svelte transitions instead.

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add ui-components-services-skills

Get CLI access →