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

Form inputs and bind:group — what developers should know

Radio buttons and checkbox groups.

Expona Admin7 min read

Form handling in Svelte is one of those areas where the framework truly shines—but only if you understand how bind:group and related directives work. We've all written form-heavy applications where managing radio buttons, checkboxes, and select dropdowns becomes tangled quickly; Svelte's two-way binding transforms that mess into clean, reactive code.

In this installment of the 100-day series, we're diving deep into form inputs and bind:group—what developers should know to build maintainable, validated forms without the boilerplate.

Understanding bind:group for radio sets

Radio button groups are a perfect use case for Svelte's bind:group directive. Unlike checkboxes, radio buttons enforce single selection—only one option in a group can be active at any time. The directive keeps your component state synchronized with the selected value automatically.

Here's how it works in practice. Let's say we're building a user preference form where customers choose a shipping method:

typescript
<script>
  let shippingMethod = 'standard';

  const methods = [
    { id: 'standard', label: 'Standard (5-7 days)', price: 0 },
    { id: 'express', label: 'Express (2-3 days)', price: 10 },
    { id: 'overnight', label: 'Overnight', price: 25 }
  ];
</script>

<fieldset>
  <legend>Shipping Method</legend>
  {#each methods as method}
    <label>
      <input
        type="radio"
        bind:group={shippingMethod}
        value={method.id}
      />
      {method.label} +${method.price}
    </label>
  {/each}
</fieldset>

<p>Selected: {shippingMethod}</p>

Notice that every radio input in the group has the same bind:group directive pointing to shippingMethod. Each input specifies its own value. When a user clicks a radio button, Svelte updates the shippingMethod variable to match that value, and any other radio in the group automatically becomes unchecked. This happens reactively and without manual event handlers.

The key insight: the variable you bind doesn't have to be a string. It can be any primitive—number, boolean, even an object reference if you use value={method} instead of value={method.id}. This is especially useful when you need to track the entire selected object, not just an identifier.

Array binding for checkboxes

Checkboxes are fundamentally different from radio buttons: multiple selections are allowed. Svelte handles this elegantly by using array binding. When you bind a checkbox to an array, Svelte automatically adds the value to the array when checked and removes it when unchecked.

Let's build a feature selection screen for an e-commerce admin dashboard:

typescript
<script>
  let selectedFeatures = [];

  const features = [
    { id: 'inventory', name: 'Inventory Management' },
    { id: 'analytics', name: 'Analytics Dashboard' },
    { id: 'automation', name: 'Order Automation' },
    { id: 'api', name: 'REST API Access' }
  ];
</script>

<fieldset>
  <legend>Select Features</legend>
  {#each features as feature}
    <label>
      <input
        type="checkbox"
        bind:group={selectedFeatures}
        value={feature.id}
      />
      {feature.name}
    </label>
  {/each}
</fieldset>

<p>Enabled features: {selectedFeatures.join(', ')}</p>
<p>Total cost: ${selectedFeatures.length * 5}/month</p>

Here's the magic: we declare selectedFeatures as an empty array. Every checkbox in the group uses the same bind:group={selectedFeatures}. When a user checks a box, that checkbox's value gets pushed into the array. Uncheck it, and it's removed. Svelte maintains array integrity for us—no duplicates, automatic ordering, clean reactivity.

You can also use objects as values instead of strings. Just replace value={feature.id} with value={feature}. Then selectedFeatures will contain the full feature objects, giving you access to all their properties downstream.

Select dropdowns with bind:value

While not technically part of the "bind:group" family, select elements use a similar pattern with bind:value. This keeps the selected option synchronized with your component state, and like radio buttons, only one selection is valid at a time.

A typical scenario: filtering orders by status in a ride-hailing or delivery platform:

typescript
<script>
  let selectedStatus = 'pending';
  let filteredOrders = [];

  const statuses = [
    { value: 'pending', label: 'Pending' },
    { value: 'accepted', label: 'Accepted' },
    { value: 'completed', label: 'Completed' },
    { value: 'cancelled', label: 'Cancelled' }
  ];

  $: {
    // This reactive block runs whenever selectedStatus changes
    filteredOrders = fetchOrdersByStatus(selectedStatus);
  }
</script>

<select bind:value={selectedStatus}>
  {#each statuses as status}
    <option value={status.value}>
      {status.label}
    </option>
  {/each}
</select>

<p>Showing {filteredOrders.length} orders</p>

The reactive statement $: automatically re-runs whenever selectedStatus changes, fetching new data. This pattern keeps the UI and state perfectly aligned without imperative event listeners.

For multi-select dropdowns, use bind:value with an array just as you would with checkboxes:

typescript
<script>
  let selectedTags = [];
</script>

<select bind:value={selectedTags} multiple>
  <option value="urgent">Urgent</option>
  <option value="follow-up">Follow-up</option>
  <option value="review">Review</option>
</select>

Validate on submit

Two-way binding feels effortless until you hit validation. The best practice: validate when the form is submitted, not on every keystroke. This keeps the UX clean—users shouldn't see errors until they've finished interacting with a field.

Here's a complete example combining radio, checkbox, and select fields with form-level validation:

typescript
<script>
  let formData = {
    shippingMethod: '',
    addOns: [],
    giftWrap: false,
    priority: 'standard'
  };

  let errors = {};
  let submitted = false;

  function validate() {
    errors = {};

    if (!formData.shippingMethod) {
      errors.shippingMethod = 'Please select a shipping method';
    }

    if (formData.addOns.length === 0) {
      errors.addOns = 'Select at least one add-on';
    }

    if (!formData.priority) {
      errors.priority = 'Priority is required';
    }

    return Object.keys(errors).length === 0;
  }

  function handleSubmit(e) {
    e.preventDefault();

    if (validate()) {
      console.log('Form is valid:', formData);
      submitted = true;
      // send to server
    }
  }
</script>

<form on:submit={handleSubmit}>
  <fieldset>
    <legend>Shipping</legend>
    <label>
      <input
        type="radio"
        bind:group={formData.shippingMethod}
        value="standard"
      />
      Standard
    </label>
    <label>
      <input
        type="radio"
        bind:group={formData.shippingMethod}
        value="express"
      />
      Express
    </label>
    {#if errors.shippingMethod}
      <p style="color: red;">{errors.shippingMethod}</p>
    {/if}
  </fieldset>

  <fieldset>
    <legend>Add-ons</legend>
    <label>
      <input
        type="checkbox"
        bind:group={formData.addOns}
        value="insurance"
      />
      Shipping Insurance
    </label>
    <label>
      <input
        type="checkbox"
        bind:group={formData.addOns}
        value="tracking"
      />
      Premium Tracking
    </label>
    {#if errors.addOns}
      <p style="color: red;">{errors.addOns}</p>
    {/if}
  </fieldset>

  <label>
    Priority:
    <select bind:value={formData.priority}>
      <option value="">Select...</option>
      <option value="standard">Standard</option>
      <option value="high">High</option>
    </select>
  </label>
  {#if errors.priority}
    <p style="color: red;">{errors.priority}</p>
  {/if}

  <button type="submit">Submit</button>

  {#if submitted}
    <p style="color: green;">Form submitted successfully!</p>
  {/if}
</form>

<style>
  fieldset {
    margin-bottom: 1.5rem;
    border: 1px solid #ccc;
    padding: 1rem;
  }

  legend {
    font-weight: bold;
  }

  label {
    display: block;
    margin: 0.5rem 0;
  }
</style>

This approach separates validation logic from binding logic. The form validates only on submit, keeping the state clean until the user explicitly submits. Errors are shown conditionally, providing clear feedback without annoying the user during input.

Common mistakes

  • Forgetting the value attribute: Every radio and checkbox needs a value prop. Without it, the browser uses the element's text content, which breaks in many scenarios. Always be explicit.
  • Mixing bind:group and bind:checked: Don't combine these on the same element. Use bind:group for sets, bind:checked only for standalone checkboxes.
  • Treating checkboxes like radio buttons: Checking a box doesn't uncheck others. If you need single selection, use radio buttons or select elements. Checkboxes are for multi-select.
  • Validating too early: Showing errors on every keystroke frustrates users. Wait for blur events or form submission instead.
  • Not initializing arrays: If you use checkbox binding with an array, declare it as let items = [];, not let items;. Svelte won't add to undefined arrays.
  • Using objects without identity checks: If you bind checkboxes to object values, remember that Svelte uses referential equality. Two objects with identical properties aren't equal, so Svelte won't recognize pre-selected items.

Key takeaways

  • bind:group handles both radio buttons (single selection) and checkboxes (multi-select via arrays) with zero manual event handling.
  • Every radio or checkbox in a group must share the same bind:group directive and have a unique value attribute.
  • For checkboxes, declare the bound variable as an array; Svelte automatically manages additions and removals.
  • Select dropdowns use bind:value with a single value for normal dropdowns or an array for multi-select.
  • Validate on form submission, not on every input change, for better UX and cleaner error handling.
  • Use reactive statements ($:) to respond to form state changes without event listeners.
  • Always provide explicit value attributes; never rely on implicit values or text content.
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