How to use
Learn how Melt's API works, and the recommended patterns and practices for using Melt UI effectively.
Builders & Components
Melt UI ships Builders and Components. The way they work is fairly similar, but there are some key differences.
Using Builders
Builders can be called from a Svelte component, or svelte.js|ts
files.
<script lang="ts"> import { Toggle } from "melt/builders";
let value = $state(false); const toggle = new Toggle({ value: () => value, onValueChange: (v) => (value = v), disabled: false, });</script>
<button {...toggle.trigger}> {toggle.value ? "On" : "Off"}</button>
Builders are functions that return attributes
to be spread onto elements e.g. toggle.trigger
, and state
, e.g. toggle.value
and toggle.disabled
.
For each builder, we show a simple usage example in its documentation page.
Static vs Reactive
Builders accept, for most props, both static and reactive values.
In the above snippet, disabled is a static value, meaning it will never change.
If we want it to change, we can pass in a reference to an outside variable declared with $state
.
let disabled = $state(false);
const toggle = new Toggle({ disabled: () => disabled,});
toggle.disabled; // falsedisabled = true;toggle.disabled; // true
The type for the disabled prop is MaybeGetter<boolean>
. The MaybeGetter
type appears often on the Melt codebase.
Using Components
The component pattern provides a more traditional Svelte experience. It provides no elements
or styling, and instead provides you with a instance from the builder. The difference lies in being
able to use the bind:
directive.
<script lang="ts"> import { Toggle } from "melt/components";
let value = $state(false);</script>
<Toggle bind:value> {#snippet children(toggle)} <button {...toggle.trigger}> {toggle.value ? "On" : "Off"} </button> {/snippet}</Toggle>
Its more straight-forward to use, but can be a bit more verbose on the template, so Melt offers both APIs.
Controlled vs Uncontrolled
Melt’s builders and components, by default, have inner state, that’s not dictated by an outside source of truth.
If we take Toggle
, for example, we’ll see it comes with a value
field.
<script lang="ts"> import { Toggle } from "melt/builders";
const toggle = new Toggle(); toggle.value; // false</script>
Whenever you click the toggle, toggle.value
will change. You can also directly set this value, e.g. toggle.value = true
.
For more complex state management however, Melt offers you the ability to control state.
By defining outside state, and passing a reference to it via a getter function, Melt will use that as a source of truth.
<script lang="ts"> import { Toggle } from "melt/builders";
let isEnabled = $state(false);
const toggle = new Toggle({ value: () => isEnabled, });</script>
In the above snippet, the toggle
value will only change whenever isEnabled
changes. Meaning that, even if you click toggle.trigger
, it will not change its value. Only when isEnabled
is changed will toggle.value
change as well.
However, in most cases you do want to change isEnabled
whenever toggle.trigger
is clicked. So Toggle
ships a helpful onValueChange
prop, which is called whenever toggle.value
is supposed to change.
<script lang="ts"> import { Toggle } from "melt/builders";
let isEnabled = $state(false);
const toggle = new Toggle({ value: () => isEnabled, onValueChange(v: boolean) { isEnabled = v; }, });</script>
This basically mimics how Toggle
works under the hood. Why whould you want this then?
There are some reasons. The most common one is encapsulation.
Encapsulation
Melt is a low-level UI library. It provides a powerful API, but with it comes a lot of moving parts.
A common practice is to create a higher-level component that is built from Melt’s primitives. This makes it easier to understand and use the library, and you can re-use your own styles and definitions.
For example, here’s an eample of a styled pin-input.
<script lang="ts"> import type { ComponentProps } from "melt"; import { getters, PinInput, type PinInputProps } from "melt/builders";
type Props = ComponentProps<PinInputProps>; let { value = $bindable(""), ...rest }: Props = $props();
const pinInput = new PinInput({ value: () => value, onValueChange: (v) => (value = v), ...getters(rest), });</script>
<div {...pinInput.root}> {#each pinInput.inputs as input} <input {...input} /> {/each}</div>
In the above snippet, we use PinInput
in a controlled manner, allowing us to use component props, instead of inner state that is unacessible outside its context.
You may also spot some helpful utilities that makes encapsulating easier, such as ComponentProps
and getters
.
Overriding changes
Another reason to use controlled state is to be in control of when the state is changed.
A simple example would be a toggle button, which waits for the value to be saved to the backend before committing anything.
<script lang="ts"> import { Toggle } from "melt/builders";
let isEnabled = $state(false); let isSaving = $state(false);
async function onChange(v: boolean) { isSaving = true; try { const newValue = await save("key", v); isEnabled = newValue; } catch { console.error("Error! Do something dev!"); } finally { isSaving = false; } }
const toggle = new Toggle({ value: () => isEnabled, onValueChange: onChange, });</script>
<button {...toggle.trigger} aria-label="toggle favourite" class={isSaving && "cursor-not-allowed opacity-75"}> {toggle.value ? "✅" : "❌"}</button>
Multiple values
You may notice that some builders have MaybeMultiple
props. These props expect different values, depending if an accompanying prop is set to true
or false
.
A good example for this is the FileUpload
builder. It has a selected
prop, and a multiple
prop.
selected
’s type is MaybeMultiple<File, Multiple>
.
This means that, if multiple
is false
or undefined
, it will accept a MaybeGetter<File>
, which works how normal reactive values work, as explained earlier in this page.
However, is multiple
is set to true
, it accepts an IterableProp<File>
, which accepts an either a SvelteSet
, or a MaybeGetter<Iterable<File>>
.
If its a SvelteSet
, or a Getter
, its controlled. Otherwise, its uncontrolled.