https://svelte.dev/docs/svelte/bind
Data flow in Svelte is top down. This means that a parent component can set props on a child component, and a component can set attributes on an element, but not the other way around.
We can use bindings to “break” this rule, and have a way to “bind” a property and a component together, and allow the data to flow from child to parent. This is mostly used in forms, to add dynamic inputs.
Note that the general, framework-agnostic version underneath bind is a callback-prop pattern - functions are values in JavaScript, just like strings or numbers. That means that a parent can define a function and hand it down to a child as an ordinary prop. The child then just calls that function whenever it wants to send something upward, passing whatever data it wants as the argument. This is exactly what you do with onClick, onChange, etc.
Example of bindings:
instead of this
<script>
let name = $state('world');
function oninput(event) {
name = event.target.value;
}
</script>
<input value={name} {oninput}/>
<h1>Hello {name}!</h1>we could have this:
<script>
let name = $state('world');
</script>
<input bind:value={name}/>
<h1>Hello {name}!</h1>Internally, Svelte still uses an event listener for accomplishing the same task.
Most bindings are two-way, meaning that changes to the value will affect the element, and vice-versa. A few bindings are readonly, meaning that changing their value will have no effect on the element.
In the DOM, every input value is a string, and that’s unhelpful when you’re dealing with numeric inputs such as type="number" and type="range", as it means that you will have to coerce (change the type of) input.value before using it. With bind:value though, Svelte takes care of it for you.
Readonly vs two-way bindings
In two-way bindings, the value and the DOM property stay in sync with each other, so there’s a two-way data flow. This means that in the example above if you change the name in the input, it will still sync with the variable; and on the other hand if you programatically assign a new value to the state name, you still sync it with the DOM.
Readonly bindings are where data flows in one direction - DOM/browser -> your variable. Assinging a new value to your variable does nothing to the element. These exist for values that are simply facts about the rendered element, and they’re computed.
Checkboxes
Checkboxes are used for toggling between states. Instead of binding to input.value we bind to input.checked.
ex.
<script>
let yes = $state(false);
</script>
<label>
<input type="checkbox" bind:checked={yes} />
Yes! Send me regular email spam
</label>Select elements
We can also use bind:value with <select> elements.
ex.
<form onsubmit={handleSubmit}>
<select
bind:value={selected}
onchange={() => (answer = '')}
>
{#each questions as question}
<option value={question}>
{question.text}
</option>
{/each}
</select>
<input bind:value={answer} />
<button disabled={!answer} type="submit">
Submit
</button>
</form>A <select> element can have a multiple attribute, in which case it will populate an array rather than selecting a single value.
ex.
<select multiple bind:value={flavours}>
{#each ['cookies and cream', 'mint choc chip', 'raspberry ripple'] as flavour}
<option>{flavour}</option>
{/each}
</select>See below example on group inputs using <input> here in the same context, instead of <select multiple>.
Group inputs
Inputs that work together can use bind:group. It only works if the inputs are in the same Svelte component. If you have multiple type="radio" or type="checkbox" inputs related to the same value, you can use this along with the value attribute. For radio buttons this value is mutually exclusive. For checkboxes, this is an array of selected values. Use this whenever you have a group of radios/checkboxes representing one logical selection or a set of selections, and want that reflected in a single state variable, rather than writing onchange handlers and checked logic yourself.
ex.
<script>
let scoops = $state(1);
let flavours = $state([]);
const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
</script>
<h2>Size</h2>
{#each [1, 2, 3] as number}
<label>
<input
type="radio"
name="scoops"
value={number}
bind:group={scoops}
/>
{number} {number === 1 ? 'scoop' : 'scoops'}
</label>
{/each}
<h2>Flavours</h2>
{#each ['cookies and cream', 'mint choco chip', 'raspberry ripple'] as flavour}
<label>
<input
type="checkbox"
name="flavours"
value={flavour}
bind:group={flavours}
/>
{flavour}
</label>
{/each}Textarea
The <textarea> element behaves similarly to a text input in Svelte, so bind:value can be used here as well:
<script>
import { marked } from 'marked';
let value = $state(`Some words are *italic*, some are **bold**\n\n- lists\n- are\n- cool`);
</script>
<div class="grid">
input
<textarea bind:value={value}></textarea>
output
<div>{@html marked(value)}</div>
</div>Contenteditable bindings
Elements with a contenteditable attribute support textContent and innerHTML bindings. For more info on contenteditable, refer to the MDN reference.
ex.
<div bind:innerHTML={html} contenteditable></div>Each block bindings
You can also bind to properties inside an each block.
ex.
{#each todos as todo}
<li class={{ done: todo.done }}>
<input
type="checkbox"
bind:checked={todo.done}
/>
<input
type="text"
placeholder="What needs to be done?"
bind:value={todo.text}
/>
</li>
{/each}Media elements
You can bind to properties of <audio> and <video> elements, making it easy to build a custom UI player, for example.
ex.
<!-- ... -->
<audio
{src}
bind:currentTime={time}
bind:duration
bind:paused
ondended={() => {
time = 0;
}}
>
</audio>
<!-- ... -->The complete set of bindings for <audio> and <video> is as follows:-
-
seven readonly bindings
duration: the total duration ins secondsbuffered: an array of{start, end}objectsseekable: same as aboveplayed: same as aboveseeking: booleanreadyState: number between and including 0 and 4
-
and five two-way bindings
currentTime: the current position of the playhead, in secondsplaybackRate: speed up or slow down,1is ‘normal’paused: whether the clip has been pausedvolume: a value between 0 and 1muted- a boolean value where true is muted
Videos additionally have videoWidth and videoHeight bindings.
Dimensions
You can add clientWidth, clientHeight, offsetWidth and offsetHeight bindings to any element and Svelte will update the bound values using a ResizeObserver.
ex.
<div bind:clientWidth={w} bind:clientHeight={h}>
<span style="font-size: {size}px" contenteditable>edit this text</span>
<span class="size">{w} x {h}px</span>
</div>Note that these bindings are readonly - changing the values of w or h will have no effect on the element itself here.
Also note that display: inline elements do not have a width or height (except for elements with ‘intrinsic’ dimensions like <img> and <canvas>) and cannot be observed with a ResizeObserver. You will need to change the display style of these elements to something else, like inline-block.
This
You can use the special bind:this directive to get a readonly binding to an element in your component.
ex.
<script>
import { paint } from './gradient.js';
let canvas;
$effect(() => {
const context = canvas.getContext('2d');
let frame = requestAnimationFrame(function loop(t) {
frame = requestAnimationFrame(loop);
paint(context, t);
});
return () => {
cancelAnimationFrame(frame);
};
});
</script>
<canvas bind:this={canvas}></canvas>The value of canvas here will remain undefined until the component is mounted, and thus you can’t access it until the $effect runs.
Use bind:this to get a reference to a DOM node and manipulate it, for example you could attach a bind:this={element} on an input element and later call .focus() on it inside a function.
Component bindings
You can bind to component props too, just as you can bind to properties of DOM elements. First, we need to mark the prop as bindable. We use the $bindable rune for this.
ex. in Keypad.svelte
<script>
let { value=$bindable(), onsubmit } = $props();
// ...
</script>and in App.svelte
<!-- ... -->
<Keypad bind:value={pin} {onsubmit} />Use component bindings sparingly, as it can be difficult to track the flow of data around your application if you have too many of them, especially if there’s no single source of truth. It can simplify your code though, if used carefully and sparingly.
If you can picture a real scenario where code outside the component needs to assign a new value (not just read the current one), and separately a real scenario where a component needs to change it from the inside, you can use $bindable. If only one side ever writes, a plain prop (child reads) or a callback prop pattern, as discussed before (child notifies) is the better fit.
Example of a real use case: Take a modal - a parent might open it externally and set open = true. But the modal also needs to close itself, from the inside - clicking the backdrop, pressing ESC or clicking a close button. Niether direction is clearly the “real one” and the other just observes - both are equally original places for the write to originate. $bindable helps in this symmetry.
Also note that the parent component doesn’t have to use bind: and can just pass a normal prop. As the docs say, “some parents don’t want to listen to what their children have to say”.
Binding to component instances
Just like you can bind to DOM elements, you can also bind to the component instances themselves using bind:this.
This is useful in the rare cases that you need to interact with a component programatically (rather than by providing it with updated props).
Here the reference you get is the component instance, and what you can do with it depends on what the component chooses to expose - any function it declares and exports becomes callable from the parent through that reference. This is different from props - props flow data in (or in/out using $bindable), while this lets the parent trigger behavior imperatively, like telling a child “focus your input” from the outside.
The docs use an example where a Canvas component was made to clear its canvas. Check that here.
A worked out example
Let’s take an example from the docs to understand what bindings really are. To me, component bindings specifically work best when there’s a certain symmetry, from my understanding of it. This is the case where the line between the parent and the child “blurs”, like in a modal component as discussed before, and we want two-way data manipulation.
Let’s take the example presented in the documentation for $bindable itself, on this page.
First, let’s look at App.svelte:
<script lang="ts">
import FancyInput from './FancyInput.svelte';
let message = $state('hello');
</script>
<FancyInput bind:value={message} />
<p>{message}</p>Hmm, what it’s doing is declaring a state, message and it’s binding the value “attribute” on the component and assigning that to the message. We realise here that value is actually a prop that is passed down from parent to the child, with the passed down value currently being the string 'hello', as it’s the value of the state message. As it’s bound using bind:, data can flow from child to parent too. We will see how that happens later.
Let’s check out FancyInput.svelte for the full picture:
<script lang="ts">
let { value = $bindable(), ...props } = $props();
</script>
<input bind:value={value} {...props} />
<style>
input {
font-family: 'Comic Sans MS';
color: deeppink;
}
</style>So here, the value prop is declared to be $bindable, which makes the prop able to be bound. We are reading the prop using the $props() rune and later we are inside an input where we are using bind:value={value}. This means that there is an HTML input element somewhere which currently has the value equal to the value prop as passed down from the parent. We also observe that this too is bound using the bind: declaration. This means that if we modify the input, for ex. type something, we will see that the bound value changes, as value is reassigned on every keystroke. We also remember that we put a binding on the parent too to listen to this (remember that not all parents listen to their children) - so value inside FancyInput propagates upward in a bottom-up data flow, and changes the value of the message state in App.svelte. This also means that the message state changes on every keystroke, and it’d be the same as you’d typed directly into a plain <input bind:value={message}> with no wrapper component involved at all. The wrapper component here, FancyInput.svelte adds styling, that’s its only job, without breaking the two-way sync.
Note that there’s no “check if changed and propagate” step happening anywhere, as bind: isn’t handing FancyInput a copy of the string - it’s establishing a live getter/setter link between the parent’s message and the child’s value. This is the reason it updates on every keystroke.