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

Attachments in Svelte 5: real-world lessons and examples

A modern replacement for actions.

Expona Admin6 min read

Attachments in Svelte 5: real-world lessons and examples. A modern replacement for actions.

Svelte 5 introduces attachments as a streamlined replacement for actions—a more intuitive way to manage element lifecycle and side effects. If you've built complex interactive features or worked with third-party libraries in production, you've likely hit the limits of traditional event handlers and lifecycle functions. Attachments solve that problem by giving you a declarative syntax to run setup and cleanup logic directly on your elements.

What are attachments and why they matter

Attachments in Svelte 5 let you bind arbitrary setup logic to DOM elements using the {@attach fn} syntax. Unlike actions, which required you to create separate function definitions and pass them as directives, attachments are inline and reactive to your component's state. They run when an element mounts, respond to reactive dependencies, and handle cleanup automatically through returned functions.

In a real e-commerce checkout flow we built last quarter, we needed to initialize a payment form library on mount, re-validate it when the cart total changed, and clean up event listeners on unmount. Attachments made this elegant instead of scattering logic across multiple lifecycle hooks and event handlers.

{@attach fn} on elements

The syntax is straightforward. You attach a function directly to a DOM node:

typescript
<input
  type="text"
  placeholder="Enter promo code"
  {@attach validatePromo}
/>

The validatePromo function receives the DOM element as its first argument. This is where you initialize third-party libraries, set up custom event listeners, or configure element-specific behavior. The function signature is simple:

typescript
function validatePromo(element: HTMLInputElement) {
  // Setup logic here
  return () => {
    // Cleanup logic here
  };
}

We've used this pattern in a ride-hailing dashboard to attach analytics tracking to various interactive elements. Each attachment function knows exactly which element it's managing, so there's no ambiguity or need for data attributes as identifiers.

Run when the element mounts

Attachments execute at the moment an element enters the DOM, which is the ideal time to initialize state-dependent behavior. This differs from component-level setup where you might not know which elements are actually rendered.

Consider a form with conditionally rendered fields. In our travel booking system, certain input groups only appear based on trip type. With attachments, initialization happens exactly when the field mounts—not earlier, not later:

typescript
<script>
  let tripType = 'roundtrip';
  let returnDate = '';

  function initDatePicker(element: HTMLInputElement) {
    const picker = new DatePicker(element, {
      minDate: new Date(),
      format: 'YYYY-MM-DD'
    });

    return () => {
      picker.destroy();
    };
  }
</script>

{#if tripType === 'roundtrip'}
  <label>
    Return date
    <input
      type="text"
      bind:value={returnDate}
      {@attach initDatePicker}
      placeholder="Select date"
    />
  </label>
{/if}

The initDatePicker function runs only when the input is actually rendered. If a user switches the trip type back to one-way, the cleanup function destroys the picker. This prevents memory leaks and keeps your component predictable.

Reactive to state changes

A powerful feature of attachments is their reactivity. You can make an attachment function depend on component state, and it will re-run when those dependencies change. This is critical in scenarios where external integrations need to be reconfigured based on user input.

In our SaaS CRM product, we have a customer detail view where users can toggle between different data visualization modes. Each mode uses a different charting library with different configuration. Here's how we handle that:

typescript
<script>
  let viewMode = 'timeline';
  let data = [];

  function attachChart(element: HTMLDivElement) {
    // Chart setup depends on viewMode
    const chart = initializeChart(element, {
      type: viewMode === 'timeline' ? 'line' : 'bar',
      data: data,
      responsive: true
    });

    // This runs whenever viewMode or data changes
    return () => {
      chart.destroy();
    };
  }
</script>

<div class="chart-container">
  <button on:click={() => viewMode = 'timeline'}>Timeline</button>
  <button on:click={() => viewMode = 'bar'}>Summary</button>

  <div {@attach attachChart}></div>
</div>

When viewMode changes, the attachment re-runs: it destroys the old chart and creates a new one with updated configuration. You declare the dependency implicitly by using the variable within the function. Svelte 5 tracks this and handles the re-execution automatically.

Return cleanup functions

Every attachment should return a cleanup function. This is how you handle unmounting, event listener removal, and resource disposal. Without proper cleanup, you'll accumulate memory leaks, duplicate event handlers, and orphaned timers—issues we've debugged in production apps where developers skipped this step.

In a real-time trading dashboard we maintain, we attach WebSocket listeners to chart containers. The cleanup function is essential:

typescript
<script>
  let instrument = 'AAPL';
  let priceStream = null;

  function attachPriceListener(element: HTMLDivElement) {
    priceStream = new WebSocket(`wss://api.example.com/prices/${instrument}`);

    priceStream.onmessage = (event) => {
      const price = JSON.parse(event.data);
      element.textContent = `$${price.toFixed(2)}`;
    };

    priceStream.onerror = (error) => {
      console.error('Stream error:', error);
    };

    // Cleanup: close the connection and remove listeners
    return () => {
      if (priceStream) {
        priceStream.close();
        priceStream = null;
      }
    };
  }
</script>

<div class="price-display" {@attach attachPriceListener}>Loading...</div>

When the component unmounts or the attachment re-runs, the cleanup function executes. The WebSocket closes, freeing resources. This pattern scales well—whether you're managing a single listener or coordinating complex multi-step teardown logic.

Common mistakes

  • Forgetting to return a cleanup function. Omitting the return statement means your setup logic never tears down. In long-lived apps, this causes performance degradation and memory bloat. Always return a function, even if it's empty for simple cases.
  • Relying on closure variables without considering reactivity. If you capture a state variable at mount time but don't re-run the attachment when it changes, you're operating with stale data. Make sure your attachment function body references the variable so Svelte tracks the dependency.
  • Treating attachments as a direct one-to-one replacement for actions. While attachments cover most action use cases, they're focused on element lifecycle. Complex directive logic that modifies element properties repeatedly might still benefit from action patterns. Use attachments for initialization and cleanup; use bindings and event handlers for ongoing updates.
  • Not handling errors in setup or cleanup. If your attachment throws during setup, the element still mounts, but your logic is broken. If cleanup throws, subsequent re-runs may not trigger. Wrap critical sections in try-catch and log errors for debugging in production.
  • Creating closures over loop variables. If you're attaching the same function to multiple elements in a loop, be careful about variable capture. Use factory functions to create fresh closures for each element.

Key takeaways

  • Attachments ({@attach fn}) provide a cleaner, more declarative way to manage element lifecycle and side effects compared to actions.
  • Attachment functions receive the DOM element and can initialize third-party libraries, set up listeners, or configure element-specific behavior.
  • They run exactly when an element mounts, making them ideal for conditional rendering scenarios.
  • Reactive dependencies within the function body cause the attachment to re-run automatically when those dependencies change.
  • Always return a cleanup function to handle unmounting, event removal, and resource disposal.
  • In production, attachments reduce boilerplate, improve maintainability, and make it easier to reason about element-specific state management.
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