The Evolution of DOM Interaction in Svelte
Svelte has always championed the philosophy of writing less code while achieving more. From its inception, the framework provided the use: directive (actions) as the primary mechanism for imperatively interacting with DOM elements—attaching third-party libraries, managing focus, implementing drag-and-drop, and countless other scenarios where declarative templates fall short.
However, as Svelte evolved toward its reactive runes-based architecture in version 5, a fundamental tension emerged. Actions, while powerful, operated somewhat outside Svelte’s reactive paradigm. They received updates through an explicit update callback, creating a mental model disconnect with the rest of the framework’s push-pull reactivity system.
Attachments
Svelte 5.29 introduces attachments—a reimagined approach to imperative DOM interaction that integrates seamlessly with Svelte’s effect system. Attachments run within effects, automatically tracking reactive dependencies and re-running when those dependencies change.
This seemingly simple change has profound implications for how we architect component libraries, manage complex DOM interactions, and reason about reactivity across component boundaries.
This tutorial assumes familiarity with Svelte 5’s runes ($state, $derived, $effect, $props) and a solid understanding of JavaScript fundamentals including Proxies, Symbols, and the event loop. We will progressively build from fundamental concepts to advanced architectural patterns, examining not just how to use attachments, but why they represent a significant advancement in Svelte’s component model.
1. The Conceptual Level
1.1 What Is an Attachment?
At its core, an attachment is a function that receives a DOM element when that element is mounted to the DOM. The function executes within an effect context, meaning any reactive state read during execution becomes a dependency—when that state changes, the attachment automatically re-runs.
interface Attachment<T extends EventTarget = Element> {
(element: T): void | (() => void)
} This type signature reveals the essential contract:
- The attachment receives an element
- It optionally returns a cleanup function
- The cleanup runs before re-execution or when the element unmounts
1.2 The Effect-Based Execution Model
**Understanding that attachments execute within effects is crucial** for mastering their behavior. Consider the implications:
<script>
let color = $state('#ff3e00')
let size = $state(100)
function paintCanvas(canvas) {
// This runs in an effect context
const ctx = canvas.getContext('2d')
// Both `color` and `size` are read synchronously
// They become dependencies of this attachment
ctx.fillStyle = color
ctx.fillRect(0, 0, size, size)
return () => {
// Cleanup: runs before re-execution or unmount
ctx.clearRect(0, 0, canvas.width, canvas.height)
}
}
</script>
<canvas {@attach paintCanvas} width={200} height={200}></canvas> When either color or size changes, Svelte’s reactive system:
- Marks the attachment as dirty
- Invokes the cleanup function (if provided)
- Re-executes the attachment function
- Tracks any new dependencies read during execution
This mirrors exactly how $effect works elsewhere in Svelte 5, creating a unified mental model for reactive side effects.
1.3 Attachments vs. Actions: A Philosophical Shift
In Svelte’s evolution, the use: directive (actions) represented the original approach to imperative DOM interactions. With the introduction of runes in Svelte 5, attachments ({@attach ...}) offer a more integrated way to handle such interactions. This section explores the philosophical shift from actions to attachments, detailing what each is, when to use them, and how to apply them effectively.
What Were Actions?
Actions were Svelte’s initial mechanism for attaching imperative behavior to DOM elements. They followed an imperative update pattern where developers manually managed state changes through a separate update callback:
<!-- Traditional action pattern -->
<script>
function myAction(node, params) {
// Initial setup
return {
update(newParams) {
// Called when params change
},
destroy() {
// Cleanup
}
}
}
</script>
<div use:myAction={someValue}>...</div> This pattern required developers to:
- Manually handle updates in a separate callback
- Track which parameters changed
- Implement differential updates to avoid redundant work
Actions operated somewhat outside Svelte’s reactive paradigm, creating a disconnect with the framework’s push-pull reactivity system.
What Are Attachments?
Attachments are the modern, runes-integrated approach to DOM interactions. They run within Svelte’s effect system, automatically tracking reactive dependencies and re-running when those dependencies change:
<!-- Attachment pattern -->
<script>
function myAttachment(node) {
// Reads `someValue` reactively
doSomethingWith(someValue)
return () => {
// Automatic cleanup before re-run
}
}
</script>
<div {@attach myAttachment}>...</div> The attachment approach is declarative about what happens (the function body) while Svelte handles when it happens (dependency tracking and scheduling).
When to Use Actions vs. Attachments
While @attach is the preferred approach for new Svelte 5 code, use: actions are still supported and can be useful for:
- Legacy codebases migrating gradually.
- Actions that need to work across different Svelte versions or even other frameworks (though rare).
- Complex scenarios where manual control over updates is beneficial.
For new imperative DOM interactions in Svelte 5, prefer @attach to align with the framework’s reactive philosophy and reduce cognitive load.
How to Use Attachments
Attachments are functions that receive a DOM element and optionally return a cleanup function. They integrate seamlessly with Svelte’s reactivity:
Basic usage: {@attach myFunction} attaches a simple function.
<script>
function logMount(el) {
console.log('Element mounted')
}
</script>
<div {@attach logMount}>Basic example</div> Parameterized: {@attach factory(param)} uses a factory for dynamic behavior.
<script>
function tooltip(text) {
return (el) => {
// Setup tooltip with text
return () => {
/* cleanup */
}
}
}
</script>
<button {@attach tooltip('Hover me')}>Button</button> Inline: {@attach (el) => { /* setup */ return () => { /* cleanup */ } }} for one-off logic.
<div
{@attach (el) => {
el.style.color = 'red'
return () => (el.style.color = '')
}}
>
Inline example
</div> Multiple: Apply several attachments to the same element for combined behaviors.
<script>
function attach1(el) {
/* ... */
}
function attach2(el) {
/* ... */
}
</script>
<div {@attach attach1} {@attach attach2}>Multiple attachments</div> Components: Attachments pass through components that spread {...props} (rest props).
<!-- MyComponent.svelte -->
<script>
let { ...props } = $props()
</script>
<!-- Usage -->
<MyComponent {@attach someAttach} />
<div {...props}>Component content</div> Passing Options to Attachments
While attachments themselves only receive the element, use factories to inject options or parameters:
<script>
function attachWithOptions(options) {
return (element) => {
// Use options here
element.style.color = options.color
element.textContent = options.text
return () => {
element.style.color = ''
element.textContent = ''
}
}
}
</script>
<div {@attach attachWithOptions({ color: 'red', text: 'Hello' })}>Styled element</div> This pattern allows attachments to be parameterized without changing the core (element) signature.
How to Migrate from Actions
For existing actions, use fromAction to convert them into attachments:
<script>
import { fromAction } from 'svelte/attachments'
import { someLibraryAction } from 'some-library'
let actionParam = $state('value')
</script>
<!-- Original action -->
<div use:someLibraryAction={actionParam}>...</div>
<!-- Equivalent attachment -->
<div {@attach fromAction(someLibraryAction, () => actionParam)}>...</div> For custom actions, rewrite them as native attachments by removing the update callback and leveraging reactive reads directly in the function body.
The Philosophical Shift in Detail
This shift represents a move from imperative programming to declarative reactivity. In the action model, developers had to anticipate and handle parameter changes manually, often leading to complex state management and potential bugs from missed updates. Attachments, by contrast, embrace Svelte’s reactive paradigm: you describe the desired behavior, and the framework ensures it stays synchronized with the data.
Benefits of Attachments Over Actions
- Reduced Boilerplate: No need for separate
updateanddestroymethods; everything is in one function. - Automatic Dependency Tracking: Svelte automatically knows when to re-run the attachment based on reactive reads.
- Unified Effect System: Attachments behave like
$effect, making the codebase more consistent. - Easier Testing: Since attachments are just functions, they can be unit-tested in isolation.
- Better Performance: Less overhead from manual update checks; Svelte’s scheduler optimizes re-runs.
2. Fundamental Patterns and Syntax
2.1 Basic Attachment Declaration
The simplest attachment is a function that performs setup when an element mounts:
<script>
/** @type {import('svelte/attachments').Attachment} */
function logMount(element) {
console.log(`Mounted: ${element.tagName}`)
}
</script>
<div {@attach logMount}>This div logs when it mounts</div> TypeScript users benefit from the Attachment type exported from svelte/attachments, which provides proper typing for the element parameter and return type.
2.2 Cleanup Functions: The Return Value Contract
Returning a cleanup function is optional but essential for:
- Removing event listeners
- Canceling timers or animations
- Disposing of third-party library instances
- Releasing resources
<script>
function trackMouse(element) {
function handleMove(event) {
element.style.setProperty('--mouse-x', `${event.clientX}px`)
element.style.setProperty('--mouse-y', `${event.clientY}px`)
}
document.addEventListener('mousemove', handleMove)
return () => {
document.removeEventListener('mousemove', handleMove)
}
}
</script>
<div {@attach trackMouse} class="mouse-tracker">Tracks mouse position</div>
<style>
.mouse-tracker::before {
content: '';
position: fixed;
left: var(--mouse-x, 0);
top: var(--mouse-y, 0);
/* ... */
}
</style> 2.3 Multiple Attachments on a Single Element
Elements can receive any number of attachments, each operating independently:
<script>
function trackVisibility(el) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
console.log(`Visible: ${entry.isIntersecting}`)
})
})
observer.observe(el)
return () => observer.disconnect()
}
function trackResize(el) {
const observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
console.log(`Size: ${entry.contentRect.width}x${entry.contentRect.height}`)
})
})
observer.observe(el)
return () => observer.disconnect()
}
function applyStyles(el) {
el.style.border = '2px solid blue'
}
</script>
<div {@attach trackVisibility} {@attach trackResize} {@attach applyStyles}>
This element has three independent attachments
</div> Each attachment:
- Has its own effect context
- Tracks its own dependencies
- Manages its own cleanup lifecycle
3. Attachment Factories—Parameterized DOM Interaction
3.1 The Factory Pattern Explained
Raw attachments work well for static behavior, but real applications require parameterization. An attachment factory is a function that returns an attachment, allowing you to pass configuration:
<script>
/**
* Creates a tooltip attachment
* @param {string} content - Tooltip text
* @returns {import('svelte/attachments').Attachment}
*/
function tooltip(content) {
return (element) => {
// Setup tooltip with `content`
const tip = createTooltip(element, content)
return () => tip.destroy()
}
}
let message = $state('Hello, world!')
</script>
<button {@attach tooltip(message)}> Hover for tooltip </button> 3.2 Reactivity in Factories: Understanding Re-execution
When using factory patterns with reactive values, the entire expression re-evaluates when dependencies change:
<script>
import { computePosition, autoUpdate, offset, shift } from '@floating-ui/dom'
let content = $state('Initial tooltip')
function tooltip(content) {
return (element) => {
const tooltipEl = document.createElement('div')
tooltipEl.textContent = content
tooltipEl.style.position = 'absolute'
tooltipEl.style.background = 'rgba(0, 0, 0, 0.8)'
tooltipEl.style.color = 'white'
tooltipEl.style.padding = '4px 8px'
tooltipEl.style.borderRadius = '4px'
tooltipEl.style.fontSize = '14px'
tooltipEl.style.pointerEvents = 'none'
tooltipEl.style.display = 'none'
document.body.appendChild(tooltipEl)
const updatePosition = () => {
computePosition(element, tooltipEl, {
placement: 'top',
middleware: [offset(6), shift()]
}).then(({ x, y }) => {
Object.assign(tooltipEl.style, {
left: `${x}px`,
top: `${y}px`
})
})
}
const cleanup = autoUpdate(element, tooltipEl, updatePosition)
const show = () => {
tooltipEl.style.display = 'block'
updatePosition()
}
const hide = () => {
tooltipEl.style.display = 'none'
}
element.addEventListener('mouseenter', show)
element.addEventListener('mouseleave', hide)
return () => {
cleanup()
element.removeEventListener('mouseenter', show)
element.removeEventListener('mouseleave', hide)
document.body.removeChild(tooltipEl)
}
}
}
</script>
<input bind:value={content} />
<!-- When `content` changes:
1. tooltip(content) is called with new value
2. Previous attachment's cleanup runs
3. New attachment runs (new tooltip element created)
-->
<button {@attach tooltip(content)}> Dynamic tooltip </button> This automatic re-creation is the default behavior. For many use cases—especially lightweight operations—this is perfectly acceptable. However, for expensive setup operations, we need finer control.
3.3 Real-World Factory Example: Floating UI Integration
Let’s build a production-ready tooltip factory using Floating UI for positioning:
<script>
import { computePosition, autoUpdate, offset, shift } from '@floating-ui/dom'
/**
* Creates a fully-featured tooltip attachment
*
* @param {string} content - Tooltip content
* @param {Object} [options={}] - Tooltip options
* @param {string} [options.placement='top'] - Placement of the tooltip
* @returns {import('svelte/attachments').Attachment<HTMLElement>}
*/
function tooltip(content, options = {}) {
return (element) => {
const tooltipEl = document.createElement('div')
tooltipEl.textContent = content
tooltipEl.style.position = 'absolute'
tooltipEl.style.background = 'rgba(0, 0, 0, 0.8)'
tooltipEl.style.color = 'white'
tooltipEl.style.padding = '8px 12px'
tooltipEl.style.borderRadius = '6px'
tooltipEl.style.fontSize = '14px'
tooltipEl.style.pointerEvents = 'none'
tooltipEl.style.zIndex = '9999'
tooltipEl.style.display = 'none'
document.body.appendChild(tooltipEl)
const updatePosition = () => {
computePosition(element, tooltipEl, {
placement: options.placement || 'top',
middleware: [offset(8), shift({ padding: 5 })]
}).then(({ x, y }) => {
Object.assign(tooltipEl.style, {
left: `${x}px`,
top: `${y}px`
})
})
}
const cleanup = autoUpdate(element, tooltipEl, updatePosition)
const show = () => {
tooltipEl.style.display = 'block'
updatePosition()
}
const hide = () => {
tooltipEl.style.display = 'none'
}
element.addEventListener('mouseenter', show)
element.addEventListener('mouseleave', hide)
return () => {
cleanup()
element.removeEventListener('mouseenter', show)
element.removeEventListener('mouseleave', hide)
document.body.removeChild(tooltipEl)
}
}
}
let tooltipText = $state('Hover over me!')
let placement = $state('top')
</script>
<div class="demo">
<input type="text" bind:value={tooltipText} placeholder="Edit tooltip text" />
<select bind:value={placement}>
<option value="top">Top</option>
<option value="bottom">Bottom</option>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
<button {@attach tooltip(tooltipText, { placement })}> Hover Me </button>
</div> 4. Inline Attachments—When Brevity Wins
4.1 Inline Syntax for Simple Cases
For simple, one-off attachments, inline arrow functions eliminate boilerplate:
<script>
let color = $state('#3498db')
</script>
<canvas
width={200}
height={200}
{@attach (canvas) => {
const ctx = canvas.getContext('2d')
ctx.fillStyle = color
ctx.fillRect(0, 0, 200, 200)
}}
></canvas>
<input type="color" bind:value={color} /> Advantages of Inline Attachments
- No separate function definition: Keeps logic close to the template.
- Immediate context: Access to local variables without passing parameters.
- Quick prototyping: Ideal for simple, non-reusable behaviors.
When to Use Named Attachments Instead
- Reusability: If the logic is used in multiple places.
- Complexity: For attachments with significant setup or cleanup.
- Testing: Named functions are easier to unit-test.
- Readability: For longer logic, named functions improve code organization.
Inline attachments shine for lightweight, component-specific interactions, while named attachments are better for shared or complex behaviors.
4.2 Nested Effects Within Inline Attachments
Inline attachments can leverage nested $effect calls for fine-grained reactivity control:
<script>
let backgroundColor = $state('#ffffff')
let foregroundColor = $state('#000000')
let text = $state('Hello')
</script>
<canvas
width={400}
height={100}
{@attach (canvas) => {
const ctx = canvas.getContext('2d')
// Outer effect runs once (no reactive reads yet)
// This is where we'd do expensive setup
ctx.font = '24px sans-serif'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
// Nested effect for background (runs when backgroundColor changes)
$effect(() => {
ctx.fillStyle = backgroundColor
ctx.fillRect(0, 0, canvas.width, canvas.height)
})
// Another nested effect for text (runs when text or foregroundColor changes)
$effect(() => {
// Must redraw background first since we clear nothing
ctx.fillStyle = backgroundColor
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.fillStyle = foregroundColor
ctx.fillText(text, canvas.width / 2, canvas.height / 2)
})
}}
></canvas> This pattern demonstrates a crucial optimization technique: the outer attachment runs once for expensive setup, while nested effects handle granular reactive updates.
5. Passing Attachments to Components
5.1 The Symbol-Based Prop Mechanism
One of attachments’ most powerful features is their ability to pass through component boundaries via prop spreading. When you use {@attach ...} on a component, Svelte creates a prop with a unique Symbol key:
<!-- Parent.svelte -->
<script>
import Button from './Button.svelte'
function focusOnMount(element) {
element.focus()
}
</script>
<Button {@attach focusOnMount}>I'll be focused on mount</Button> <!-- Button.svelte -->
<script>
/** @type {import('svelte/elements').HTMLButtonAttributes} */
let { children, ...props } = $props()
</script>
<!-- Spreading `props` includes the attachment Symbol -->
<button {...props}>
{@render children?.()}
</button> 5.2 How This Works Under the Hood
When {@attach fn} is used on a component:
- Svelte generates a unique Symbol
- The attachment function is assigned to that Symbol key in the props object
- When props are spread onto an element, Svelte recognizes the Symbol and attaches the function
This mechanism means wrapper components “just work” without explicit attachment forwarding:
<!-- IconButton.svelte -->
<script>
import Icon from './Icon.svelte'
let { icon, children, ...props } = $props()
</script>
<button {...props}>
<Icon name={icon} />
{@render children?.()}
</button> <!-- Usage -->
<script>
import IconButton from './IconButton.svelte'
function trackClick(element) {
element.addEventListener('click', () => {
analytics.track('button_clicked')
})
return () => {
// cleanup...
}
}
</script>
<!-- The attachment reaches the inner <button> element -->
<IconButton icon="save" {@attach trackClick}>Save Document</IconButton> 5.3 Building Wrapper Components That Preserve Attachments
A critical pattern for component library authors is ensuring attachments flow through wrapper components:
<!-- Card.svelte -->
<script>
/**
* @type {{
* variant?: 'default' | 'elevated' | 'outlined',
* children: import('svelte').Snippet,
* [key: string]: any
* }}
*/
let { variant = 'default', children, ...props } = $props()
</script>
<article class="card card--{variant}" {...props}>
{@render children()}
</article>
<style>
.card {
/* ... */
}
.card--elevated {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card--outlined {
border: 1px solid currentColor;
}
</style> By using rest props (...props) and spreading them onto the root element, any attachments passed to <Card> automatically apply to the <article> element.
6. Controlling Attachment Re-execution
6.1 The Re-execution Challenge
By default, attachments re-run whenever any dependency changes. For expensive operations, this can be problematic:
<script>
function expensiveSetup(bar) {
return (node) => {
// This runs on EVERY change to `bar`
veryExpensiveSetupWork(node) // 😬
update(node, bar)
}
}
let bar = $state('initial')
</script>
<div {@attach expensiveSetup(bar)}>...</div> Every time bar changes, veryExpensiveSetupWork runs again—clearly undesirable.
Understanding Re-execution Triggers
Re-execution occurs when:
- Reactive state read during the attachment’s execution changes.
- The attachment factory is called with new parameters (recreating the attachment function).
- The component re-mounts the element.
Signs That Re-execution is a Problem
- Performance issues: Expensive setup running unnecessarily.
- Visual glitches: Libraries reinitializing, causing flickering.
- Resource leaks: Incomplete cleanup from rapid re-executions.
- Unexpected behavior: State not preserved across re-runs.
When these issues arise, the getter pattern provides a solution.
6.2 The Getter Pattern for Deferred Reading
The solution is to pass a getter function instead of the value directly, deferring the reactive read to a nested effect:
<script>
/**
* @param {() => string} getBar - Getter for the reactive value
* @returns {import('svelte/attachments').Attachment}
*/
function expensiveSetup(getBar) {
return (node) => {
// Runs once—no reactive reads here
veryExpensiveSetupWork(node)
// Nested effect reads the value reactively
$effect(() => {
update(node, getBar())
})
}
}
let bar = $state('initial')
</script>
<!-- Pass a getter, not the value --><div {@attach expensiveSetup(() => bar)}>...</div> Now:
expensiveSetup(() => bar)creates the attachment onceveryExpensiveSetupWorkruns once on mount- The nested
$effectruns wheneverbarchanges
6.3 Practical Example: Chart Library Integration
Consider integrating a charting library where initialization is expensive:
<script>
import { Chart, registerables } from 'chart.js'
Chart.register(...registerables)
/**
* Creates a reactive chart attachment
* @param {() => import('chart.js').ChartData} getData
* @param {import('chart.js').ChartOptions} options
*/
function createChart(getData, options = {}) {
return (canvas) => {
// Expensive: runs once
const chart = new Chart(canvas, {
type: 'line',
data: getData(),
options: {
responsive: true,
maintainAspectRatio: false,
...options
}
})
// Cheap: runs on data changes
$effect(() => {
const newData = getData()
chart.data = newData
chart.update('none') // Skip animations for updates
})
return () => chart.destroy()
}
}
let salesData = $state({
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
datasets: [
{
label: 'Sales',
data: [12, 19, 3, 5, 2]
}
]
})
function addDataPoint() {
const months = ['Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
const nextMonth = months[salesData.labels.length - 5]
if (nextMonth) {
salesData.labels = [...salesData.labels, nextMonth]
salesData.datasets[0].data = [...salesData.datasets[0].data, Math.floor(Math.random() * 20)]
}
}
</script>
<div class="chart-container">
<canvas {@attach createChart(() => salesData)}></canvas>
</div>
<button onclick={addDataPoint}>Add Data Point</button> 7. Programmatic Attachment Creation
7.1 The createAttachmentKey Function
Sometimes you need to create attachments programmatically—especially in library code where you’re constructing prop objects dynamically. The createAttachmentKey function from svelte/attachments generates Symbol keys recognized by Svelte’s spreading mechanism:
<script>
import { createAttachmentKey } from 'svelte/attachments'
// Create a prop object with an attachment
const props = {
class: 'interactive-element',
'data-testid': 'my-element',
onclick: () => console.log('clicked'),
[createAttachmentKey()]: (node) => {
node.setAttribute('data-mounted', 'true')
return () => node.removeAttribute('data-mounted')
}
}
</script>
<button {...props}> Click me </button> 7.2 Library Development Use Case
This pattern shines when building component libraries that need to attach behavior dynamically:
<script module>
import { createAttachmentKey } from 'svelte/attachments'
/**
* Creates a props object for draggable elements
* @param {Object} options
* @param {(position: {x: number, y: number}) => void} options.onDrag
* @param {() => void} [options.onDragStart]
* @param {() => void} [options.onDragEnd]
*/
export function createDraggable({ onDrag, onDragStart, onDragEnd }) {
return {
draggable: true,
[createAttachmentKey()]: (element) => {
let startX, startY, initialX, initialY
function handleDragStart(e) {
startX = e.clientX
startY = e.clientY
const rect = element.getBoundingClientRect()
initialX = rect.left
initialY = rect.top
onDragStart?.()
}
function handleDrag(e) {
if (e.clientX === 0 && e.clientY === 0) return
const dx = e.clientX - startX
const dy = e.clientY - startY
onDrag({ x: initialX + dx, y: initialY + dy })
}
function handleDragEnd() {
onDragEnd?.()
}
element.addEventListener('dragstart', handleDragStart)
element.addEventListener('drag', handleDrag)
element.addEventListener('dragend', handleDragEnd)
return () => {
element.removeEventListener('dragstart', handleDragStart)
element.removeEventListener('drag', handleDrag)
element.removeEventListener('dragend', handleDragEnd)
}
}
}
}
</script>
<script>
let position = $state({ x: 100, y: 100 })
const draggableProps = createDraggable({
onDrag: (pos) => (position = pos),
onDragStart: () => console.log('Started dragging'),
onDragEnd: () => console.log('Finished dragging')
})
</script>
<div {...draggableProps} style="position: absolute; left: {position.x}px; top: {position.y}px;">
Drag me!
</div> 7.3 Multiple Programmatic Attachments
You can include multiple attachments in a single props object by calling createAttachmentKey() multiple times:
<script>
import { createAttachmentKey } from 'svelte/attachments'
const props = {
class: 'multi-behavior',
[createAttachmentKey()]: (el) => {
// First attachment: logging
console.log('Element mounted:', el)
return () => console.log('Element unmounting:', el)
},
[createAttachmentKey()]: (el) => {
// Second attachment: intersection observer
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
el.classList.toggle('visible', entry.isIntersecting)
})
})
observer.observe(el)
return () => observer.disconnect()
},
[createAttachmentKey()]: (el) => {
// Third attachment: focus trap
// ...implementation
}
}
</script>
<div {...props}>Multiple behaviors attached programmatically</div> 8. Migrating from Actions to Attachments
8.1 The fromAction Utility
Svelte provides fromAction to convert existing actions into attachments, enabling gradual migration and interoperability with action-based libraries:
<script>
import { fromAction } from 'svelte/attachments'
import { someLibraryAction } from 'some-library'
let actionParam = $state('value')
</script>
<!-- Original action syntax -->
<div use:someLibraryAction={actionParam}>...</div>
<!-- Equivalent attachment syntax -->
<div {@attach fromAction(someLibraryAction, () => actionParam)}>...</div> Critical note: The second argument must be a function that returns the parameter, not the parameter itself. This ensures proper reactivity tracking.
8.2 Why Migrate? The Composability Advantage
While actions still work, attachments offer superior composability:
- Component passthrough: Attachments pass through component boundaries via spreading
- Unified reactivity: No separate
updatecallback to maintain - Effect integration: Can use
$effectinside for fine-grained control
<!-- This doesn't work with actions -->
<MyComponent use:someAction={param}>Content</MyComponent>
<!-- This works with attachments -->
<MyComponent {@attach fromAction(someAction, () => param)}>Content</MyComponent> 8.3 Migration Strategy for Existing Codebases
For large codebases with many actions, migrate incrementally:
- Identify action usage patterns - catalog all actions
- Wrap critical library actions - use
fromActionfor third-party code - Rewrite custom actions - convert your own actions to native attachments
- Update component interfaces - ensure wrapper components spread props
<!-- Before: Custom action -->
<script>
export function tooltip(node, text) {
const tip = document.createElement('div');
tip.className = 'tooltip';
tip.textContent = text;
function show() { document.body.appendChild(tip); }
function hide() { tip.remove(); }
node.addEventListener('mouseenter', show);
node.addEventListener('mouseleave', hide);
return {
update(newText) {
tip.textContent = newText;
},
destroy() {
node.removeEventListener('mouseenter', show);
node.removeEventListener('mouseleave', hide);
tip.remove();
}
};
}
</script>
<!-- After: Native attachment -->
<script>
export function tooltip(text) {
return (node) => {
const tip = document.createElement('div');
tip.className = 'tooltip';
// Text is read reactively through the factory closure
tip.textContent = text;
function show() { document.body.appendChild(tip); }
function hide() { tip.remove(); }
node.addEventListener('mouseenter', show);
node.addEventListener('mouseleave', hide);
return () => {
node.removeEventListener('mouseenter', show);
node.removeEventListener('mouseleave', hide);
tip.remove();
};
};
}
</script> Common Migration Pitfalls
- Forgetting the getter: Using
fromAction(action, param)instead offromAction(action, () => param). - Missing cleanup: Actions with manual cleanup need proper destroy handling.
- Component boundaries: Actions don’t pass through components unless spread.
- Reactivity differences: Attachments track dependencies automatically, unlike actions’ manual updates.
Testing Your Migration
- Ensure attachments behave identically to actions.
- Test reactivity: changes should trigger updates without manual
updatecalls. - Verify cleanup: no memory leaks or lingering effects.
- Check component passthrough: attachments should work on wrapper components.
9. Real-World Use Cases and Patterns
9.1 Focus Management System
Managing focus is critical for accessibility. Here’s a comprehensive focus trap attachment:
<script>
/**
* Creates a focus trap attachment for modal dialogs
* @param {Object} options
* @param {boolean} [options.autoFocus=true] - Focus first focusable element on mount
* @param {boolean} [options.restoreFocus=true] - Restore focus on unmount
* @returns {import('svelte/attachments').Attachment}
*/
function focusTrap({ autoFocus = true, restoreFocus = true } = {}) {
return (container) => {
const previouslyFocused = document.activeElement
const focusableSelectors = [
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'a[href]',
'[tabindex]:not([tabindex="-1"])'
].join(', ')
function getFocusableElements() {
return Array.from(container.querySelectorAll(focusableSelectors))
}
function handleKeyDown(event) {
if (event.key !== 'Tab') return
const focusable = getFocusableElements()
if (focusable.length === 0) return
const first = focusable[0]
const last = focusable[focusable.length - 1]
if (event.shiftKey && document.activeElement === first) {
event.preventDefault()
last.focus()
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault()
first.focus()
}
}
container.addEventListener('keydown', handleKeyDown)
if (autoFocus) {
const focusable = getFocusableElements()
if (focusable.length > 0) {
focusable[0].focus()
}
}
return () => {
container.removeEventListener('keydown', handleKeyDown)
if (restoreFocus && previouslyFocused instanceof HTMLElement) {
previouslyFocused.focus()
}
}
}
}
let showModal = $state(false)
</script>
{#if showModal}
<div class="modal-backdrop">
<div class="modal" role="dialog" aria-modal="true" {@attach focusTrap()}>
<h2>Modal Title</h2>
<p>Modal content goes here.</p>
<button onclick={() => (showModal = false)}>Close</button>
</div>
</div>
{/if} 9.2 Intersection Observer for Lazy Loading
<script>
/**
* Lazy loading attachment with placeholder support
* @param {Object} options
* @param {string} options.src - Image source to load
* @param {string} [options.placeholder] - Placeholder while loading
* @param {string} [options.rootMargin='100px'] - Load ahead margin
* @returns {import('svelte/attachments').Attachment<HTMLImageElement>}
*/
function lazyLoad({ src, placeholder = '', rootMargin = '100px' }) {
return (img) => {
// Set placeholder immediately
if (placeholder) {
img.src = placeholder
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// Load the real image
img.src = src
img.classList.add('loaded')
observer.disconnect()
}
})
},
{ rootMargin }
)
observer.observe(img)
return () => observer.disconnect()
}
}
const images = [
{ src: '/images/hero-1.jpg', alt: 'Hero image 1' },
{ src: '/images/hero-2.jpg', alt: 'Hero image 2' },
{ src: '/images/hero-3.jpg', alt: 'Hero image 3' }
]
</script>
<div class="image-gallery">
{#each images as image}
<img
alt={image.alt}
{@attach lazyLoad({
src: image.src,
placeholder: '/images/placeholder.svg'
})}
/>
{/each}
</div> 9.3 Real-Time Collaboration Cursor
<script>
import { onMount } from 'svelte'
/**
* Tracks cursor position and broadcasts to collaboration server
* @param {Object} options
* @param {WebSocket} options.socket - WebSocket connection
* @param {string} options.userId - User identifier
* @param {string} options.color - Cursor color
*/
function collaborativeCursor({ socket, userId, color }) {
return (container) => {
let throttleTimer = null
function handleMouseMove(event) {
if (throttleTimer) return
throttleTimer = setTimeout(() => {
throttleTimer = null
}, 50) // Throttle to 20fps
const rect = container.getBoundingClientRect()
const x = (event.clientX - rect.left) / rect.width
const y = (event.clientY - rect.top) / rect.height
socket.send(
JSON.stringify({
type: 'cursor',
userId,
position: { x, y },
color
})
)
}
function handleMouseLeave() {
socket.send(
JSON.stringify({
type: 'cursor',
userId,
position: null
})
)
}
container.addEventListener('mousemove', handleMouseMove)
container.addEventListener('mouseleave', handleMouseLeave)
return () => {
container.removeEventListener('mousemove', handleMouseMove)
container.removeEventListener('mouseleave', handleMouseLeave)
if (throttleTimer) clearTimeout(throttleTimer)
}
}
}
let socket = $state(null)
const userId = crypto.randomUUID()
const cursorColor = `hsl(${Math.random() * 360}, 70%, 50%)`
onMount(() => {
socket = new WebSocket('wss://collab.example.com')
return () => socket.close()
})
</script>
{#if socket}
<div
class="collaboration-area"
{@attach collaborativeCursor({ socket, userId, color: cursorColor })}
>
<!-- Collaborative content -->
</div>
{/if} 9.4 Form Validation with Visual Feedback
<script>
/**
* Validates input and provides visual feedback
* @param {Object} options
* @param {(value: string) => string | null} options.validate - Returns error message or null
* @param {(isValid: boolean) => void} [options.onValidityChange] - Callback on validity change
* @returns {import('svelte/attachments').Attachment<HTMLInputElement>}
*/
function validateInput({ validate, onValidityChange }) {
return (input) => {
let currentValidity = true
const errorElement = document.createElement('div')
errorElement.className = 'validation-error'
errorElement.setAttribute('role', 'alert')
function checkValidity() {
const error = validate(input.value)
const isValid = error === null
if (isValid !== currentValidity) {
currentValidity = isValid
onValidityChange?.(isValid)
}
input.classList.toggle('invalid', !isValid)
input.setAttribute('aria-invalid', String(!isValid))
if (error) {
errorElement.textContent = error
if (!errorElement.parentElement) {
input.parentElement?.appendChild(errorElement)
}
} else {
errorElement.remove()
}
}
input.addEventListener('input', checkValidity)
input.addEventListener('blur', checkValidity)
// Initial check
checkValidity()
return () => {
input.removeEventListener('input', checkValidity)
input.removeEventListener('blur', checkValidity)
errorElement.remove()
}
}
}
let email = $state('')
let emailValid = $state(false)
function validateEmail(value) {
if (!value) return 'Email is required'
if (!value.includes('@')) return 'Please enter a valid email'
if (!value.includes('.')) return 'Please enter a valid email domain'
return null
}
</script>
<form>
<label>
Email:
<input
type="email"
bind:value={email}
{@attach validateInput({
validate: validateEmail,
onValidityChange: (valid) => (emailValid = valid)
})}
/>
</label>
<button type="submit" disabled={!emailValid}> Submit </button>
</form>
<style>
.invalid {
border-color: red;
}
.validation-error {
color: red;
font-size: 0.875rem;
margin-top: 0.25rem;
}
</style> 10. Advanced Patterns and Architectural Considerations
10.1 Composing Multiple Attachments
Create higher-order attachments that combine multiple behaviors:
<script>
import { createAttachmentKey } from 'svelte/attachments'
/**
* Composes multiple attachments into a single props object
* @param {...import('svelte/attachments').Attachment} attachments
* @returns {Record<symbol, import('svelte/attachments').Attachment>}
*/
function compose(...attachments) {
return attachments.reduce((props, attachment) => {
props[createAttachmentKey()] = attachment
return props
}, {})
}
// Individual attachments
function logMounts(el) {
console.log('Mounted:', el)
return () => console.log('Unmounted:', el)
}
function addRipple(el) {
function createRipple(e) {
const ripple = document.createElement('span')
ripple.className = 'ripple'
ripple.style.left = `${e.offsetX}px`
ripple.style.top = `${e.offsetY}px`
el.appendChild(ripple)
setTimeout(() => ripple.remove(), 600)
}
el.addEventListener('click', createRipple)
return () => el.removeEventListener('click', createRipple)
}
// Composed props object
const interactiveProps = compose(logMounts, addRipple)
</script>
<button {...interactiveProps}> Interactive Button </button> When to Use Composition
- Reusable behavior bundles: Combine related functionalities into a single prop.
- Programmatic attachment: When building props objects dynamically in libraries.
- Complex components: Where multiple behaviors are always applied together.
Alternative: Multiple {@attach} Directives
For simple cases, multiple {@attach} on the same element is often clearer and doesn’t require a helper function:
<div {@attach logMounts} {@attach addRipple}>...</div> Composition is powerful for advanced scenarios, but multiple directives suffice for most use cases.
10.2 Conditional Attachments
Attachments can be conditionally applied using ternary expressions:
<script>
let enableTracking = $state(true)
function analyticsTracker(el) {
// Track interactions
const handleClick = () => analytics.track('click', { element: el.id })
el.addEventListener('click', handleClick)
return () => el.removeEventListener('click', handleClick)
}
// No-op attachment for when tracking is disabled
function noop() {
return () => {}
}
</script>
<button id="cta-button" {@attach enableTracking ? analyticsTracker : noop}> Call to Action </button> 10.3 Context-Aware Attachments
Leverage Svelte’s context API for attachments that need shared state:
<!-- FormContext.svelte -->
<script>
import { setContext } from 'svelte'
let { children } = $props()
const formState = $state({
fields: {},
errors: {},
isSubmitting: false
})
setContext('form', {
registerField(name, validate) {
formState.fields[name] = { validate, value: '' }
},
updateField(name, value) {
formState.fields[name].value = value
const error = formState.fields[name].validate?.(value)
formState.errors[name] = error
},
getError(name) {
return formState.errors[name]
}
})
</script>
{@render children()} <!-- FormField.svelte -->
<script>
import { getContext } from 'svelte'
let { name, validate, ...props } = $props()
const form = getContext('form')
function formFieldAttachment(input) {
form.registerField(name, validate)
function handleInput(e) {
form.updateField(name, e.target.value)
}
input.addEventListener('input', handleInput)
$effect(() => {
const error = form.getError(name)
input.setCustomValidity(error || '')
})
return () => {
input.removeEventListener('input', handleInput)
}
}
</script>
<input {name} {...props} {@attach formFieldAttachment} /> 10.4 Testing Attachments
Attachments can be tested by extracting the core logic:
// tooltip.js
/**
* @param {string} content
* @returns {import('svelte/attachments').Attachment}
*/
export function tooltip(content) {
return (element) => {
const tip = createTooltipElement(content)
const show = () => positionAndShow(tip, element)
const hide = () => tip.remove()
element.addEventListener('mouseenter', show)
element.addEventListener('mouseleave', hide)
return () => {
element.removeEventListener('mouseenter', show)
element.removeEventListener('mouseleave', hide)
tip.remove()
}
}
}
// Internal functions exposed for testing
export function createTooltipElement(content) {
const tip = document.createElement('div')
tip.className = 'tooltip'
tip.textContent = content
tip.setAttribute('role', 'tooltip')
return tip
}
export function positionAndShow(tip, anchor) {
const rect = anchor.getBoundingClientRect()
tip.style.left = `${rect.left + rect.width / 2}px`
tip.style.top = `${rect.top - 8}px`
document.body.appendChild(tip)
} // tooltip.test.js
import { describe, it, expect, vi } from 'vitest'
import { createTooltipElement, tooltip } from './tooltip.js'
describe('tooltip', () => {
describe('createTooltipElement', () => {
it('creates element with correct content', () => {
const tip = createTooltipElement('Hello')
expect(tip.textContent).toBe('Hello')
expect(tip.className).toBe('tooltip')
expect(tip.getAttribute('role')).toBe('tooltip')
})
})
describe('tooltip attachment', () => {
it('adds and removes event listeners', () => {
const element = document.createElement('button')
const addSpy = vi.spyOn(element, 'addEventListener')
const removeSpy = vi.spyOn(element, 'removeEventListener')
const cleanup = tooltip('Test')(element)
expect(addSpy).toHaveBeenCalledWith('mouseenter', expect.any(Function))
expect(addSpy).toHaveBeenCalledWith('mouseleave', expect.any(Function))
cleanup()
expect(removeSpy).toHaveBeenCalledWith('mouseenter', expect.any(Function))
expect(removeSpy).toHaveBeenCalledWith('mouseleave', expect.any(Function))
})
})
}) 11. Pitfalls, Edge Cases, and Anti-Patterns
11.1 Reactivity Traps
Problem: Unintended dependency tracking
<script>
let unrelatedState = $state('hello')
let targetState = $state('world')
function myAttachment(el) {
// BUG: This reads `unrelatedState` creating an unwanted dependency
console.log('Current unrelated state:', unrelatedState)
// This is the actual work we want to do
el.textContent = targetState
}
</script> Solution: Use untrack for reads that shouldn’t create dependencies:
<script>
import { untrack } from 'svelte'
let unrelatedState = $state('hello')
let targetState = $state('world')
function myAttachment(el) {
// Now this doesn't create a dependency
untrack(() => {
console.log('Current unrelated state:', unrelatedState)
})
el.textContent = targetState
}
</script> 11.2 Memory Leaks from Missing Cleanup
Problem: Forgetting to clean up subscriptions
<script>
function leakyAttachment(el) {
const subscription = someObservable.subscribe((value) => {
el.textContent = value
})
// BUG: No cleanup! Subscription persists after unmount
}
</script> Solution: Always return cleanup for subscriptions:
<script>
function safeAttachment(el) {
const subscription = someObservable.subscribe((value) => {
el.textContent = value
})
return () => subscription.unsubscribe()
}
</script> 11.3 Cross-Component Data Leakage
Problem: Mutating shared state in attachments
<script>
// Shared mutable object
const sharedConfig = { count: 0 }
function counterAttachment(el) {
// BUG: Mutating shared state affects all components using this config
sharedConfig.count += 1
el.textContent = sharedConfig.count
}
</script> Solution: Use component-local state or immutable patterns:
<script>
function counterAttachment(el) {
// Local state per attachment instance
let count = 0
function increment() {
count += 1
el.textContent = String(count)
}
el.addEventListener('click', increment)
return () => el.removeEventListener('click', increment)
}
</script> 11.4 Infinite Re-execution Loops
Problem: Writing to state that’s also read in the same attachment
<script>
let counter = $state(0)
function infiniteLoop(el) {
// BUG: Reads `counter`, then writes to it, triggering re-run
el.textContent = String(counter)
counter += 1 // This triggers a re-run!
}
</script> Solution: Use nested effects or defer writes:
<script>
let counter = $state(0)
function safeAttachment(el) {
// Read in outer attachment
el.textContent = String(counter)
// Write deferred to next tick
queueMicrotask(() => {
counter += 1
})
}
</script> 11.5 Server-Side Rendering Considerations
Attachments only run in the browser, not during SSR. This is usually desirable, but be aware:
<script>
function browserOnlySetup(el) {
// This never runs on the server
// Initial content should be set via regular attributes/content
}
</script>
<!-- Set initial state declaratively for SSR -->
<div class="widget" data-initialized="false" {@attach browserOnlySetup}>Loading...</div> 12. Performance Optimization Strategies
12.1 Debouncing and Throttling in Attachments
For high-frequency events, wrap handlers appropriately:
<script>
function throttle(fn, delay) {
let lastCall = 0
return (...args) => {
const now = Date.now()
if (now - lastCall >= delay) {
lastCall = now
fn(...args)
}
}
}
function scrollTracker(el) {
const handleScroll = throttle(() => {
const scrollPercent = el.scrollTop / (el.scrollHeight - el.clientHeight)
analytics.track('scroll_depth', { percent: Math.round(scrollPercent * 100) })
}, 100)
el.addEventListener('scroll', handleScroll, { passive: true })
return () => el.removeEventListener('scroll', handleScroll)
}
</script> 12.2 Lazy Initialization
Defer expensive operations until actually needed:
<script>
function lazyChart(getData) {
return (canvas) => {
let chart = null
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !chart) {
// Only initialize when visible
import('chart.js').then(({ Chart, registerables }) => {
Chart.register(...registerables)
chart = new Chart(canvas, {
type: 'line',
data: getData()
})
})
observer.disconnect()
}
})
observer.observe(canvas)
return () => {
observer.disconnect()
chart?.destroy()
}
}
}
</script> 12.3 Batching DOM Operations
Minimize layout thrashing by batching reads and writes:
<script>
function efficientLayout(el) {
function updateLayout() {
// Batch all reads first
const width = el.offsetWidth
const height = el.offsetHeight
const parentWidth = el.parentElement?.offsetWidth ?? 0
// Then batch all writes
requestAnimationFrame(() => {
el.style.transform = `scale(${parentWidth / width})`
el.dataset.aspectRatio = String(width / height)
})
}
const resizeObserver = new ResizeObserver(updateLayout)
resizeObserver.observe(el)
resizeObserver.observe(el.parentElement)
return () => resizeObserver.disconnect()
}
</script> Conclusion: The Future of DOM Interaction in Svelte
Svelte 5’s {@attach ...} directive represents more than a syntactic improvement over actions—it embodies a philosophical shift toward unified reactivity. By executing within effects, attachments inherit all the benefits of Svelte’s reactive system: automatic dependency tracking, efficient batched updates, and intuitive cleanup semantics.
As you adopt attachments in your codebase, remember these key principles:
- Embrace the factory pattern for parameterized attachments
- Use nested effects for fine-grained reactivity control
- Always consider cleanup for subscriptions and event listeners
- Leverage prop spreading for component composition
- Migrate incrementally using
fromActionfor existing libraries
The transition from actions to attachments isn’t merely about new syntax—it’s about thinking in terms of reactive effects rather than imperative callbacks. This mental model shift, once internalized, makes complex DOM interactions feel natural and maintainable.
As the Svelte ecosystem continues to evolve, expect library authors to embrace attachments for their superior composability. The ability to pass DOM behaviors through component hierarchies via prop spreading opens new possibilities for building truly encapsulated, reusable component systems.
Reference: API Summary
Template Syntax
<!-- Basic attachment -->
<div {@attach myAttachment}>...</div>
<!-- Factory pattern -->
<div {@attach tooltip('Hello')}>...</div>
<!-- Inline attachment -->
<div
{@attach (el) => {
/* setup */ return () => {
/* cleanup */
}
}}
>
...
</div>
<!-- Multiple attachments -->
<div {@attach a} {@attach b} {@attach c}>...</div>
<!-- On components (spreads to inner element) -->
<Component {@attach myAttachment}>...</Component> Module Exports (svelte/attachments)
// Create Symbol keys for programmatic attachments
function createAttachmentKey(): symbol
// Convert actions to attachments
function fromAction<E extends EventTarget, T>(action: Action<E, T>, fn: () => T): Attachment<E>
function fromAction<E extends EventTarget>(action: Action<E, void>): Attachment<E>
// Type definition
interface Attachment<T extends EventTarget = Element> {
(element: T): void | (() => void)
} Conclusion
The {@attach} directive represents Svelte 5’s modern approach to element-level behavior extension, combining the flexibility of the legacy use: action system with first-class support for Svelte’s reactive primitives. By enabling inline attachment definitions, full reactivity within attachment functions, and seamless composition of multiple attachments, it transforms element augmentation from an imperative pattern into a declarative, reactive paradigm that feels natural within Svelte’s component model.
Mastering {@attach} requires understanding both its capabilities and its appropriate use cases. It excels at element-specific behaviors—tooltips, click-outside detection, drag-and-drop, accessibility enhancements—while complementing rather than replacing lifecycle hooks, $effect, and event handlers for their respective domains. The fromAction migration path ensures backward compatibility with the ecosystem, while the attachment’s reactive nature and composition patterns open new architectural possibilities for building reusable, maintainable element behaviors.
Key Takeaways
{@attach}provides element-level behavior extension introduced in Svelte 5.29, binding functions to DOM elements with automatic lifecycle management (setup on mount, cleanup on unmount)- Full reactivity inside attachments - closures capture reactive state and re-run when dependencies change, unlike legacy
use:actions which need manual update calls - Syntax supports inline functions and references -
{@attach (el) => { ... }}for inline definitions or{@attach myAttachment}for reusable references - Return a cleanup function for teardown - return
() => {}from attachments to handle event listener removal, timer cleanup, and resource disposal - Multiple attachments compose seamlessly with
{@attach a} {@attach b} {@attach c}syntax, executing in order during mount and in reverse during unmount - Component usage spreads to inner element -
<Component {@attach myAttachment}>applies the attachment to the component’s root DOM element, not the component instance fromActionmigration helper converts legacyuse:actions to attachments withfromAction(action)for zero-parameter actions orfromAction(action, () => params)for parameterized ones- TypeScript support with
Attachment<T>interface provides type safety for element types, with generic constraints ensuring attachments only apply to compatible targets
See Also
- Official Svelte 5 Documentation -
{@attach} - Svelte Actions (Legacy) - The
use:directive that{@attach}supersedes svelte/attachmentsmodule -fromActionandcreateAttachmentKeyutilities$effect- Reactive side effects at component level- Element Event Handlers - The
on*attributes for event handling - Lifecycle Hooks -
onMountandonDestroyfor component-level lifecycle - TypeScript with Svelte - Type-safe attachment definitions
This article was written for Svelte 5.29+. Always consult the official Svelte documentation for the most current API details and best practices.