Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 011 / 100

Bindings in Svelte: a practical Svelte & SvelteKit guide

Two-way data binding in Svelte with bind: for inputs, checkboxes, groups, element references and custom getters.

Expona Admin6 min read

Understanding Two-Way Data Binding in Svelte

Svelte's binding system is one of its most powerful features, yet it's often misunderstood by developers transitioning from other frameworks. Unlike React's unidirectional data flow, Svelte's bind: directive creates genuine two-way binding—when the user updates an input, your data updates automatically, and when your data changes, the DOM reflects it instantly. In this practical Svelte & SvelteKit guide, we'll explore how to leverage bindings effectively across inputs, checkboxes, element references, and custom logic to write less boilerplate and build more intuitive applications.

bind:value for Input Elements

The most common binding you'll use is bind:value on text inputs. Instead of manually wiring up change handlers and state updates, Svelte synchronizes the input's value with your variable bidirectionally.

Here's a typical scenario: you're building a form in a SvelteKit application where users search through product listings in real time.

typescript
<script>
  let searchQuery = '';

  $: filteredProducts = products.filter(p => 
    p.name.toLowerCase().includes(searchQuery.toLowerCase())
  );
</script>

<input 
  type="text" 
  bind:value={searchQuery}
  placeholder="Search products..."
>

<ul>
  {#each filteredProducts as product (product.id)}
    <li>{product.name}</li>
  {/each}
</ul>

Without binding, you'd need to add an on:input handler, extract the value from the event, and manually update state. Svelte eliminates this friction. The reactive declaration $: automatically reruns whenever searchQuery changes, keeping your filtered list in sync.

This works seamlessly with SvelteKit's form handling too. If you're building a contact form that should validate and update as the user types, bindings make the pattern obvious:

typescript
<script>
  let email = '';
  let message = '';
  let isValid = false;

  $: isValid = email.includes('@') && message.length > 0;
</script>

<input bind:value={email} type="email" />
<textarea bind:value={message}></textarea>
<button disabled={!isValid}>Send</button>

bind:checked and bind:group for Selections

Checkboxes and radio buttons need different binding approaches. Use bind:checked for individual checkboxes to bind their checked state to a boolean variable, and use bind:group when you have multiple checkboxes or radio buttons that should share a single array or value.

Imagine building an e-commerce filter sidebar where users select multiple categories:

typescript
<script>
  let selectedCategories = [];
  const categories = ['Electronics', 'Clothing', 'Books', 'Home'];

  $: filteredItems = items.filter(item =>
    selectedCategories.length === 0 || 
    selectedCategories.includes(item.category)
  );
</script>

{#each categories as category}
  <label>
    <input 
      type="checkbox" 
      bind:group={selectedCategories}
      value={category}
    />
    {category}
  </label>
{/each}

When a user checks "Electronics", Svelte automatically adds it to the selectedCategories array. Unchecking removes it. The reactive statement filters items in real time without any explicit event handling.

For radio buttons (where only one option can be selected), the pattern is identical:

typescript
<script>
  let sortBy = 'price';
</script>

<label>
  <input type="radio" bind:group={sortBy} value="price" />
  Price (Low to High)
</label>
<label>
  <input type="radio" bind:group={sortBy} value="rating" />
  Rating (High to Low)
</label>

The key difference: with checkboxes, bind:group expects an array, so multiple values can be selected. With radio buttons, bind:group expects a single variable, ensuring only one selection at a time.

bind:this for Element References

Sometimes you need direct access to DOM elements—to focus an input field, trigger animations, or measure dimensions. That's where bind:this comes in. It lets you store a reference to the underlying HTML element in a JavaScript variable.

A practical example: building a modal that should auto-focus the input field when it appears:

typescript
<script>
  let isModalOpen = false;
  let inputElement;

  function openModal() {
    isModalOpen = true;
  }

  function handleKeydown(e) {
    if (e.key === 'Escape') {
      isModalOpen = false;
    }
  }

  $: if (isModalOpen && inputElement) {
    inputElement.focus();
  }
</script>

<button on:click={openModal}>Open Modal</button>

{#if isModalOpen}
  <div class="modal" on:keydown={handleKeydown}>
    <input bind:this={inputElement} type="text" />
    <button on:click={() => (isModalOpen = false)}>Close</button>
  </div>
{/if}

Notice we're using a reactive declaration to check when both isModalOpen is true and the element reference exists, then call .focus() on it. This is much cleaner than managing useEffect-style lifecycles.

Another common use case in data-heavy applications: measuring a container's scroll position to implement infinite scroll or sticky headers:

typescript
<script>
  let scrollContainer;
  let scrollY = 0;

  function handleScroll() {
    if (scrollContainer) {
      scrollY = scrollContainer.scrollTop;
    }
  }
</script>

<div bind:this={scrollContainer} on:scroll={handleScroll} class="list">
  {#each items as item}
    <div>{item.title}</div>
  {/each}
</div>

Function Bindings for Custom Logic

Beyond the built-in bind:value, bind:checked, and bind:this, Svelte allows custom getter and setter functions through bindings. This is powerful when you need to transform data or trigger side effects during binding.

Imagine you're managing user preferences where the underlying data is stored in a specific format (like booleans for feature flags), but you want to display and bind to them using getters and setters:

typescript
<script>
  let preferences = {
    features: {
      darkMode: true,
      notifications: false
    }
  };

  let darkModeEnabled = preferences.features.darkMode;
  let notificationsEnabled = preferences.features.notifications;

  $: preferences.features.darkMode = darkModeEnabled;
  $: preferences.features.notifications = notificationsEnabled;
</script>

<label>
  <input type="checkbox" bind:checked={darkModeEnabled} />
  Dark Mode
</label>

<label>
  <input type="checkbox" bind:checked={notificationsEnabled} />
  Notifications
</label>

While Svelte doesn't have getter/setter syntax like some frameworks, you achieve the same effect through reactive declarations. The $: statements ensure that whenever the bound variable changes, the underlying preferences object updates, and vice versa.

For more complex scenarios, consider a numeric input that should be clamped between 0 and 100:

typescript
<script>
  let rawOpacity = 50;

  function getOpacity() {
    return Math.max(0, Math.min(100, rawOpacity));
  }

  function setOpacity(value) {
    rawOpacity = value;
  }
</script>

<input 
  type="range" 
  min="0" 
  max="100" 
  on:input={(e) => setOpacity(+e.target.value)}
  value={getOpacity()}
/>
<p>Opacity: {getOpacity()}%</p>

Common Mistakes

Binding to computed values: Don't try to bind directly to derived data. If you write bind:value={computed} where computed is a reactive declaration, Svelte will throw an error because bindings require a writable target. Use intermediate variables instead.

Forgetting bind:group for arrays: A frequent mistake is using bind:checked on multiple checkboxes when you should use bind:group. This works initially but won't add or remove values from an array automatically, leaving your state out of sync.

Overusing bind:this: While powerful, binding to DOM elements can make testing harder and create tight coupling between component logic and the DOM. Prefer bindings like bind:value when possible, and reserve bind:this for imperative DOM operations that can't be avoided.

Binding in forms without proper submission handling: Svelte bindings update state in real time, but in a SvelteKit form context, you may still need a proper form submission flow. Don't rely solely on bindings for data persistence—always validate and handle submission server-side.

Key Takeaways

  • Use bind:value on inputs to eliminate manual event handling and state updates.
  • bind:group simplifies multiple selections—arrays for checkboxes, single values for radio buttons.
  • bind:checked is for individual checkboxes when you only need a boolean state.
  • bind:this provides DOM element references for imperative operations like focus, scroll measurement, or animations.
  • Combine bindings with reactive declarations ($:) to build responsive, intuitive interfaces with minimal boilerplate.
  • Remember that bindings work both ways—your code updates the DOM, and user input updates your state.
  • In SvelteKit applications, bindings speed up form development but don't replace server-side validation and form submission handling.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles