Skip to main content

Controlled

Melt UI components are uncontrolled by default, but offer the ability to be controlled.

Controlled components are an optional advanced feature and should only be used when necessary. If you're unsure if you should be using the components in a controlled way, you likely don't need to.

Controlled vs. Uncontrolled

There are various definitions for controlled/uncontrolled components, but in the context of Melt UI, uncontrolled means that the state and stores of a component are created and managed automatically. This is the default behavior, and in most cases, it is more than enough.

    <script lang="ts">
  import { createDialog } from '@melt-ui/svelte'
 
  const {
    elements: { trigger, overlay, content, title, description, close },
    states: { open }
  } = createDialog()
</script>
	
    <script lang="ts">
  import { createDialog } from '@melt-ui/svelte'
 
  const {
    elements: { trigger, overlay, content, title, description, close },
    states: { open }
  } = createDialog()
</script>
	

In the example above, open is a store that createDialog returns. You can read its value to determine whether the Dialog is open or not. Since we're not altering the state in any way besides user interaction, we consider this to be uncontrolled.

Controlled means that the user (you) can create and/or manage the state and/or stores.

Our goal is to provide as much flexibility as possible, so we offer a few ways to give you more control over the state and behavior of the components.

For the following examples, we'll use the Dialog builder, but the same concepts also apply to other builders. Be sure to checkout the builder's API reference to see what controlled options are available.

Modify Writable States

By default, we provide an open store from the Dialog builder that will have its state updated whenever a trigger or close element is pressed.

You can update the open store that's returned at any moment, even without expecting an user interaction.

    <script lang="ts">
  import { createDialog } from '@melt-ui/svelte'
 
  const {
    elements: { trigger, overlay, content, title, description, close },
    states: { open }
  } = createDialog()
 
  open.set(true)
</script>
	
    <script lang="ts">
  import { createDialog } from '@melt-ui/svelte'
 
  const {
    elements: { trigger, overlay, content, title, description, close },
    states: { open }
  } = createDialog()
 
  open.set(true)
</script>
	

Syncing State

A common use case for controlling the state of a component is to sync it with props, or other internal state. You can do it manually...

    <script lang="ts">
  import { createDialog } from '@melt-ui/svelte'
 
  export let open = false
 
  const {
    elements: { trigger, overlay, content, title, description, close },
    states: { open: localOpen }
  } = createDialog()
 
  $: $localOpen = open
  $: open = $localOpen
</script>
	
    <script lang="ts">
  import { createDialog } from '@melt-ui/svelte'
 
  export let open = false
 
  const {
    elements: { trigger, overlay, content, title, description, close },
    states: { open: localOpen }
  } = createDialog()
 
  $: $localOpen = open
  $: open = $localOpen
</script>
	

But it's harder to manage, can be error prone, and with multiple states and options, it can get messy quickly.

    <script lang="ts">
  import { createDialog } from '@melt-ui/svelte'
 
  export let a
  export let b
  export let c
  export let d
  export let e
 
  const {
    states: { a: localA, b: localB, c: localC, d: localD, e: localE }
  } = createDialog()
 
  $: $localA = a
  $: a = $localA
  $: $localB = b
  $: b = $localB
  $: $localC = c
  $: c = $localC
  $: $localD = d
  $: d = $localD
  $: $localE = e
  $: e = $localE
</script>
	
    <script lang="ts">
  import { createDialog } from '@melt-ui/svelte'
 
  export let a
  export let b
  export let c
  export let d
  export let e
 
  const {
    states: { a: localA, b: localB, c: localC, d: localD, e: localE }
  } = createDialog()
 
  $: $localA = a
  $: a = $localA
  $: $localB = b
  $: b = $localB
  $: $localC = c
  $: c = $localC
  $: $localD = d
  $: d = $localD
  $: $localE = e
  $: e = $localE
</script>
	

We provide a createSync function that will improve this situation.

    <script lang="ts">
  import { createDialog, createSync } from '@melt-ui/svelte'
 
  export let a
  export let b
  export let c
  export let d
  export let e
 
  const { states } = createDialog()
 
  const sync = createSync(states)
  $: sync.a(a, (v) => (a = v))
  $: sync.b(b, (v) => (b = v))
  $: sync.c(c, (v) => (c = v))
  $: sync.d(d, (v) => (d = v))
  $: sync.e(e, (v) => (e = v))
</script>
	
    <script lang="ts">
  import { createDialog, createSync } from '@melt-ui/svelte'
 
  export let a
  export let b
  export let c
  export let d
  export let e
 
  const { states } = createDialog()
 
  const sync = createSync(states)
  $: sync.a(a, (v) => (a = v))
  $: sync.b(b, (v) => (b = v))
  $: sync.c(c, (v) => (c = v))
  $: sync.d(d, (v) => (d = v))
  $: sync.e(e, (v) => (e = v))
</script>
	

createSync takes an object of writable stores and returns an object with sync functions. The first argument of the sync function is the value that the store should be set to, and the second argument is a setter function that will be called with the new value of the store.

It's still a bit to write, but it's much more manageable. The sync function will also ignore updates that are the same as the current value, so you don't have to worry about unnecessary updates.

Bring Your Own Store

If you wanted to define your own open store so that its state could be shared and updated by other parts of your app, then we offer a way for you to supply your own.

It's as simple as passing your own open store to the createDialog builder.

    <script lang="ts">
  import { createDialog } from '@melt-ui/svelte'
  import { writable } from 'svelte/store'
 
  const customOpen = writable(false)
 
  const {
    elements: { trigger, overlay, content, title, description, close }
  } = createDialog({
    open: customOpen
  })
</script>
	
    <script lang="ts">
  import { createDialog } from '@melt-ui/svelte'
  import { writable } from 'svelte/store'
 
  const customOpen = writable(false)
 
  const {
    elements: { trigger, overlay, content, title, description, close }
  } = createDialog({
    open: customOpen
  })
</script>
	

Behind the scenes, we're using the custom open store you passed in instead of creating our own. Which means your store will be updated as the default open store normally would, but you'll be able to create it and share it with other parts of your app before initializing the Dialog.

Also note that we still return an open store on the states object. It uses the same custom store you pass in though, so modifying it will modify your custom store as well.

We can't guarantee that everything will work as expected if you choose to modify certain state stores outside of the builder, so use this feature with caution. These controls are provided to cover those edge cases where you want to do something that the library won't support out of the box.

Change Functions

A change function is called when the value of a state store would normally change. It receives an object with curr and next properties, whose values are the current and next values of the store.

The next value is what the store would have been set to by default if the change function wasn't used. The curr value is the current value of the store.

Whatever is returned from the change function will be used as the new value of the store.

The snippet below shows an example of how the change function could be used to prevent the open store from being set to true based on some arbitrary condition.

    <script lang="ts">
  import { createDialog, type CreateDialogProps } from '@melt-ui/svelte'
 
  const someCondition = false
 
  const handleOpen: CreateDialogProps['onOpenChange'] = ({ curr, next }) => {
    if (!someCondition) {
      return curr
    }
    return next
  }
 
  const {
    elements: { trigger, overlay, content, title, description, close }
  } = createDialog({
    onOpenChange: handleOpen
  })
</script>
	
    <script lang="ts">
  import { createDialog, type CreateDialogProps } from '@melt-ui/svelte'
 
  const someCondition = false
 
  const handleOpen: CreateDialogProps['onOpenChange'] = ({ curr, next }) => {
    if (!someCondition) {
      return curr
    }
    return next
  }
 
  const {
    elements: { trigger, overlay, content, title, description, close }
  } = createDialog({
    onOpenChange: handleOpen
  })
</script>
	

Of course, this is a contrived example, but it nicely demonstrates the power of this feature.

Custom Events

Melt automatically attaches listeners for and handles events for various elements to provide functionality. However, there may be times where you disagree with a particular way we handle the event, or you just want to add some extra functionality on top of what we provide.

For each event that we listen to and handle, we also provide a way for you to override the default behavior, or add your own in addition to ours. The custom event that we dispatch is always prefixed with m- and what follows is the name of the original event.

For example, if we're listening to and handling a click event on a trigger element, then we'll dispatch an m-click event. If it was a pointerdown event, we'd dispatch an m-pointerdown. The attributes for the elements are typed to include these custom events, so you'll get type support for them in your editor.

Here's what the custom events we dispatch looks like:

    const customMeltEvent: MeltEvent<typeof originalEvent> = new CustomEvent(
  `m-${originalEvent.type}`,
  {
    detail: {
      originalEvent
    },
    cancelable: true
  }
)
	
    const customMeltEvent: MeltEvent<typeof originalEvent> = new CustomEvent(
  `m-${originalEvent.type}`,
  {
    detail: {
      originalEvent
    },
    cancelable: true
  }
)
	

By marking the custom event as cancelable, you may use preventDefault on it, which signals Melt to immediately stop handling the event, thus preventing the builder's default behavior from happening.

We also pass the original event as a property on the detail object so that you can access it in case you'd like to do something else with it.

So if you in fact wanted to override the default behavior of a click event on a trigger element, then you could do something like this:

    <button
  use:melt={$trigger}
  on:m-click={(e) => {
    e.preventDefault()
    // do something else
  }}>
  Trigger
</button>
	
    <button
  {...$trigger} use:trigger
  on:m-click={(e) => {
    e.preventDefault()
    // do something else
  }}>
  Trigger
</button>
	

Overriding the default behavior of a component should be done with caution. We can't guarantee that everything will work as expected if you override certain events, but we provide this feature to cover those edge cases where you want to do something that the library won't support out of the box.