Skip to content

Commit 57c2c0c

Browse files
committed
Merge branch 'main' into ensure-reactivity
2 parents 4c9a776 + 0d67ff4 commit 57c2c0c

File tree

336 files changed

+6350
-1703
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

336 files changed

+6350
-1703
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ jobs:
2828
os: ubuntu-latest
2929
- node-version: 22
3030
os: ubuntu-latest
31+
- node-version: 24
32+
os: ubuntu-latest
3133

3234
steps:
3335
- uses: actions/checkout@v4

benchmarking/compare/index.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,23 +67,37 @@ for (let i = 0; i < results[0].length; i += 1) {
6767
for (const metric of ['time', 'gc_time']) {
6868
const times = results.map((result) => +result[i][metric]);
6969
let min = Infinity;
70+
let max = -Infinity;
7071
let min_index = -1;
7172

7273
for (let b = 0; b < times.length; b += 1) {
73-
if (times[b] < min) {
74-
min = times[b];
74+
const time = times[b];
75+
76+
if (time < min) {
77+
min = time;
7578
min_index = b;
7679
}
80+
81+
if (time > max) {
82+
max = time;
83+
}
7784
}
7885

7986
if (min !== 0) {
80-
console.group(`${metric}: fastest is ${branches[min_index]}`);
87+
console.group(`${metric}: fastest is ${char(min_index)} (${branches[min_index]})`);
8188
times.forEach((time, b) => {
82-
console.log(`${branches[b]}: ${time.toFixed(2)}ms (${((time / min) * 100).toFixed(2)}%)`);
89+
const SIZE = 20;
90+
const n = Math.round(SIZE * (time / max));
91+
92+
console.log(`${char(b)}: ${'◼'.repeat(n)}${' '.repeat(SIZE - n)} ${time.toFixed(2)}ms`);
8393
});
8494
console.groupEnd();
8595
}
8696
}
8797

8898
console.groupEnd();
8999
}
100+
101+
function char(i) {
102+
return String.fromCharCode(97 + i);
103+
}

documentation/docs/02-runes/02-$state.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ Unlike other frameworks you may have encountered, there is no API for interactin
2020

2121
If `$state` is used with an array or a simple object, the result is a deeply reactive _state proxy_. [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) allow Svelte to run code when you read or write properties, including via methods like `array.push(...)`, triggering granular updates.
2222

23-
> [!NOTE] Classes like `Set` and `Map` will not be proxied, but Svelte provides reactive implementations for various built-ins like these that can be imported from [`svelte/reactivity`](./svelte-reactivity).
24-
25-
State is proxified recursively until Svelte finds something other than an array or simple object. In a case like this...
23+
State is proxified recursively until Svelte finds something other than an array or simple object (like a class). In a case like this...
2624

2725
```js
2826
let todos = $state([
@@ -67,16 +65,15 @@ todos[0].done = !todos[0].done;
6765

6866
### Classes
6967

70-
You can also use `$state` in class fields (whether public or private):
68+
Class instances are not proxied. Instead, you can use `$state` in class fields (whether public or private), or as the first assignment to a property immediately inside the `constructor`:
7169

7270
```js
7371
// @errors: 7006 2554
7472
class Todo {
7573
done = $state(false);
76-
text = $state();
7774

7875
constructor(text) {
79-
this.text = text;
76+
this.text = $state(text);
8077
}
8178

8279
reset() {
@@ -110,10 +107,9 @@ You can either use an inline function...
110107
// @errors: 7006 2554
111108
class Todo {
112109
done = $state(false);
113-
text = $state();
114110

115111
constructor(text) {
116-
this.text = text;
112+
this.text = $state(text);
117113
}
118114

119115
+++reset = () => {+++
@@ -123,6 +119,8 @@ class Todo {
123119
}
124120
```
125121

122+
> Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity).
123+
126124
## `$state.raw`
127125

128126
In cases where you don't want objects and arrays to be deeply reactive you can use `$state.raw`.
@@ -147,6 +145,8 @@ person = {
147145

148146
This can improve performance with large arrays and objects that you weren't planning to mutate anyway, since it avoids the cost of making them reactive. Note that raw state can _contain_ reactive state (for example, a raw array of reactive objects).
149147

148+
As with `$state`, you can declare class fields using `$state.raw`.
149+
150150
## `$state.snapshot`
151151

152152
To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`:

documentation/docs/02-runes/04-$effect.md

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -269,11 +269,11 @@ In general, `$effect` is best considered something of an escape hatch — useful
269269
270270
If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25.
271271

272-
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAACpVRy26DMBD8FcvKgUhtoIdeHBwp31F6MGSJkBbHwksEQvx77aWQqooq9bgzOzP7mGTdIHipPiZJowOpGJAv0po2VmfnDv4OSBErjYdneHWzBJaCjcx91TWOToUtCIEE3cig0OIty44r5l1oDtjOkyFIsv3GINQ_CNYyGegd1DVUlCR7oU9iilDUcP8S8roYs9n8p2wdYNVFm4csTx872BxNCcjr5I11fdgonEkXsjP2CoUUZWMv6m6wBz2x7yxaM-iJvWeRsvSbSVeUy5i0uf8vKA78NIeJLSZWv1I8jQjLdyK4XuTSeIdmVKJGGI4LdjVOiezwDu1yG74My8PLCQaSiroe5s_5C2PHrkVGAgAA)):
272+
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAAE5WRTWrDMBCFryKGLBJoY3fRjWIHeoiu6i6UZBwEY0VE49TB-O6VxrFTSih0qe_Ne_OjHpxpEDS8O7ZMeIAnqC1hAP3RA1990hKI_Fb55v06XJA4sZ0J-IjvT47RcYyBIuzP1vO2chVHHFjxiQ2pUr3k-SZRQlbBx_LIFoEN4zJfzQph_UMQr4hRXmBd456Xy5Uqt6pPKHmkfmzyPAZL2PCnbRpg8qWYu63I7lu4gswOSRYqrPNt3CgeqqzgbNwRK1A76w76YqjFspfcQTWmK3vJHlQm1puSTVSeqdOc_r9GaeCHfUSY26TXry6Br4RSK3C6yMEGT-aqVU3YbUZ2NF6rfP2KzXgbuYzY46czdgyazy0On_FlLH3F-UDXhgIO35UGlA1rAgAA)):
273273

274274
```svelte
275275
<script>
276-
let total = 100;
276+
const total = 100;
277277
let spent = $state(0);
278278
let left = $state(total);
279279
@@ -297,32 +297,26 @@ You might be tempted to do something convoluted with effects to link one value t
297297
</label>
298298
```
299299

300-
Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE51SsW6DMBT8FcvqABINdOhCIFKXTt06lg4GHpElYyz8iECIf69tcIIipo6-u3f3fPZMJWuBpvRzkBXyTpKSy5rLq6YRbbgATdOfmeKkrMgCBt9GPpQ66RsItFjJNBzhVScRJBobmumq5wovhSxQABLskAmSk7ckOXtMKyM22ItGhhAk4Z0R0OwIN-tIQzd-90HVhvy2HsGNiQFCMltBgd7XoecV2xzXNV7XaEcth7ZfRv7kujnsTX2Qd7USb5rFjwZkJlgJwpWRcakG04cpOS9oz-QVCuoeInXW-RyEJL-sG0b7Wy6kZWM-u7CFxM5tdrIl9qg72vB74H-y7T2iXROHyVb0CLanp1yNk4D1A1jQ91hzrQSbUtIIGLcir0ylJDm9Q7urz42bX4UwIk2xH2D5Xf4A7SeMcMQCAAA=)):
300+
Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE5VRvW7CMBB-FcvqECQK6dDFJEgsnfoGTQdDLsjSxVjxhYKivHvPBwFUsXS8774_nwftbQva6I_e78gdvNo6Xzu_j3quG4cQtfkaNJ1DIiWA8atkE8IiHgEpYVsb4Rm-O3gCT2yji7jrXKB15StiOJKiA1lUpXrL81VCEUjFwHTGXiJZgiyf3TYIjSxq6NwR6uyifr0ohMbEZnpHH2rWf7ImS8KZGtK6osl_UqelRIyVL5b3ir5AuwWUtoXzoee6fIWy0p31e6i0XMocLfZQDuI6qtaeykGcR7UU6XWznFAZU9LN_X9B2UyVayk9f3ji0-REugen6U9upDOCcAWcLlS7GNCejWoQTqsLtrfBqHzxDu3DrUTOf0xwIm2o62H85sk6_OHG2jQWI4y_3byXXGMCAAA=)):
301301

302302
```svelte
303303
<script>
304-
let total = 100;
304+
const total = 100;
305305
let spent = $state(0);
306-
let left = $state(total);
307-
308-
function updateSpent(value) {
309-
spent = value;
310-
left = total - spent;
311-
}
306+
let left = $derived(total - spent);
312307
313-
function updateLeft(value) {
314-
left = value;
308+
+++ function updateLeft(left) {
315309
spent = total - left;
316-
}
310+
}+++
317311
</script>
318312
319313
<label>
320-
<input type="range" bind:value={() => spent, updateSpent} max={total} />
314+
<input type="range" bind:value={spent} max={total} />
321315
{spent}/{total} spent
322316
</label>
323317
324318
<label>
325-
<input type="range" bind:value={() => left, updateLeft} max={total} />
319+
<input type="range" +++bind:value={() => left, updateLeft}+++ max={total} />
326320
{left}/{total} left
327321
</label>
328322
```

documentation/docs/02-runes/07-$inspect.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ This rune, added in 5.14, causes the surrounding function to be _traced_ in deve
5252
import { doSomeWork } from './elsewhere';
5353
5454
$effect(() => {
55+
+++// $inspect.trace must be the first statement of a function body+++
5556
+++$inspect.trace();+++
5657
doSomeWork();
5758
});

documentation/docs/03-template-syntax/01-basic-markup.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,14 @@ As with elements, `name={name}` can be replaced with the `{name}` shorthand.
8282
<Widget foo={bar} answer={42} text="hello" />
8383
```
8484

85+
## Spread attributes
86+
8587
_Spread attributes_ allow many attributes or properties to be passed to an element or component at once.
8688

87-
An element or component can have multiple spread attributes, interspersed with regular ones.
89+
An element or component can have multiple spread attributes, interspersed with regular ones. Order matters — if `things.a` exists it will take precedence over `a="b"`, while `c="d"` would take precedence over `things.c`:
8890

8991
```svelte
90-
<Widget {...things} />
92+
<Widget a="b" {...things} c="d" />
9193
```
9294

9395
## Events

documentation/docs/03-template-syntax/03-each.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,9 @@ An each block can also specify an _index_, equivalent to the second argument in
4343
{#each expression as name, index (key)}...{/each}
4444
```
4545

46-
If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to diff the list when data changes, rather than adding or removing items at the end. The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
46+
If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle.
47+
48+
The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
4749

4850
```svelte
4951
{#each items as item (item.id)}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
---
2+
title: {@attach ...}
3+
---
4+
5+
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
6+
7+
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
8+
9+
> [!NOTE]
10+
> Attachments are available in Svelte 5.29 and newer.
11+
12+
```svelte
13+
<!--- file: App.svelte --->
14+
<script>
15+
/** @type {import('svelte/attachments').Attachment} */
16+
function myAttachment(element) {
17+
console.log(element.nodeName); // 'DIV'
18+
19+
return () => {
20+
console.log('cleaning up');
21+
};
22+
}
23+
</script>
24+
25+
<div {@attach myAttachment}>...</div>
26+
```
27+
28+
An element can have any number of attachments.
29+
30+
## Attachment factories
31+
32+
A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment ([demo](/playground/untitled#H4sIAAAAAAAAE3VT0XLaMBD8lavbDiaNCUlbHhTItG_5h5AH2T5ArdBppDOEMv73SkbGJGnH47F9t3un3TsfMyO3mInsh2SW1Sa7zlZKo8_E0zHjg42pGAjxBPxp7cTvUHOMldLjv-IVGUbDoUw295VTlh-WZslqa8kxsLL2ACtHWxh175NffnQfAAGikSGxYQGfPEvGfPSIWtOH0TiBVo2pWJEBJtKhQp4YYzjG9JIdcuMM5IZqHMPioY8vOSA997zQoevf4a7heO7cdp34olRiTGr07OhwH1IdoO2A7dLMbwahZq6MbRhKZWqxk7rBxTGVbuHmhCgb5qDgmIx_J6XtHHukHTrYYqx_YpzYng8aO4RYayql7hU-1ZJl0akqHBE_D9KLolwL-Dibzc7iSln9XjtqTF1UpMkJ2EmXR-BgQErsN4pxIJKr0RVO1qrxAqaTO4fbc9bKulZm3cfDY3aZDgvFGErWjmzhN7KmfX5rXyDeX8Pt1mU-hXjdBOrtuB97vK4GPUtmJ41XcRMEGDLD8do0nJ73zhUhSlyRw0t3vPqD8cjfLs-axiFgNBrkUd9Ulp50c-GLxlXAVlJX-ffpZyiSn7H0eLCUySZQcQdXlxj4El0Yv_FZvIKElqqGTruVLhzu7VRKCh22_5toOyxsWqLwwzK-cCbYNdg-hy-p9D7sbiZWUnts_wLUOF3CJgQAAA==)):
33+
34+
```svelte
35+
<!--- file: App.svelte --->
36+
<script>
37+
import tippy from 'tippy.js';
38+
39+
let content = $state('Hello!');
40+
41+
/**
42+
* @param {string} content
43+
* @returns {import('svelte/attachments').Attachment}
44+
*/
45+
function tooltip(content) {
46+
return (element) => {
47+
const tooltip = tippy(element, { content });
48+
return tooltip.destroy;
49+
};
50+
}
51+
</script>
52+
53+
<input bind:value={content} />
54+
55+
<button {@attach tooltip(content)}>
56+
Hover me
57+
</button>
58+
```
59+
60+
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
61+
62+
## Inline attachments
63+
64+
Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71Wf3OaWBT9KoyTTnW3MS-I3dYmnWXVtnRAazRJzbozRSQEApiRhwKO333vuY8m225m_9yZGOT9OPfcc84D943UTfxGr_G7K6Xr3TVeNW7D2M8avT_3DVk-YAoDNF4vNB8e2tnWjyXGlm7mPzfurVPpp5JgGmeZtwkf5PtFupCxLzVvHa832rl2lElX-s2Xm2DZFNqp_hs-rZetd4v07ORpT3qmQHu7MF2td0BZp8k6z_xkvfXP902_pZ2_1_aYWEiqm0kN8I4r79qbdZ6umnq3q_2iNf22F4dE6qt2oimwdpim_uY6XMm7Fuo-IQT_iTD_CeGTHwZ38ieIJUFQRxirR1Xf39Dw0X5z0I72Af4tD61vvPNwWKQnqmfPTbduhsEd2J3vO_oBd3dc6fF2X7umNdWGf0vBRhSS6qoV7cCXfTXWfKmvWG61_si_vfU92Wz-E4RhsLhNIYinsox9QKGVd8-tuACCeKXRX12P-T_eKf7fhTq0Hvt-f3ailtSeoxJHRo1-58NoPe1UiBc1hkL8Yeh45y_vQ3mcuNl9T8s3cXPRWLnS7YWJG_gn2Tb4tUjid8jua-PVl08j_ab8I14mH8Llx0s5Tz5Err4ql52r_GYg0mVy1bEGZuD0ze64b5TWYFiM-16wSuJ4JT5vfVpDcztrcG_YkRU4s6HxufzDWF4XuVeJ1P10IbzBemt3Vp1V2e04ZXfrJd7Wicyd039brRIv_RIVu_nXi7X1cfL2sy66ztToUp1TO7qJ7NlwZ0f30pld5qNSVE5o6PbMojFHjgZB7oSicPpGteyLclQap7SvY0dXtM_LR1NT2JFHey3aaxa0VxCeYJ7RMHemoiCcgPZV9pR7o7kgcOjeGliYk9hjDZx8FAq6enwlTPSZj_vYPw9Il64dXdIY8ZmapzwfEd8-1ZyaxWhqkIZOibXUd-6Upqi1pD4uMicCV1GA_7zi73UN8BaF4sC8peJtMjfmjbHZBFwq5ov50qRaE0l96NZggnW4KqypYRAW-uhSz9ADvklwJF2J-5W0Z5fQPBhDX92R6I_0IFxRgDftge4l4dP-gH1hjD7uqU6fsOEZ9UNrCdPB-nys6uXgY6O3ZMd9sy5T9PghqrWHdjo4jB51CgLiKJaDYYA-7WgYONf1FbjkI-mE3EAfUY_rijfuJ_CVPaR50oe9JF7Q0pI8Dw3osxxYHdYPGbp2CnwHF8KvwJv2wEv0Z3ilQI6U9uwbZxbYJXvEmjjQjjCHkvNLvNg3yhzXQd1olamsT4IRrZmX0MUDpwL7R8zzHj7pSh9hPHFSHjLezKqAST51uC5zmtQ87skDUaneLokT5RbXkPWSYz53Abgjc8_o4KFGUZ-Hgv2Z1l5OTYM9D-HfUD0L-EwxH5wRnIG61gS-khfgY1bq7IAP_DA4l5xRuh9xlm8yGjutc8t-wHtkhWv3hc7aqGwiK5KzgvM5xRkZYn193uEln-su55j1GaIv7oM4iPrsVHiG0Dx7TR9-1lBfqFdwfvSd5LNL5xyZVp5NoHFZ57FkfiF6vKs4k5zvIfrX5xX6MXmt0gM5MTu8DjnhukrHHzTRd3jm0dma0_f_x5cxP9f4jBdqHvmbq2fUjzqcKh2Cp-yWj9ntcHanXmBXxhu7Q--eyjhfNFpaV7zgz4nWEUb7zUOhpevjjf_gu_KZ99pxFlZ-T3sttkmYqrco_26q35v0Ewzv5EZPbnL_8BfduWGMnyyN3q0bZ_7hb_7KG_L4CQAA)):
65+
66+
```svelte
67+
<!--- file: App.svelte --->
68+
<canvas
69+
width={32}
70+
height={32}
71+
{@attach (canvas) => {
72+
const context = canvas.getContext('2d');
73+
74+
$effect(() => {
75+
context.fillStyle = color;
76+
context.fillRect(0, 0, canvas.width, canvas.height);
77+
});
78+
}}
79+
></canvas>
80+
```
81+
82+
> [!NOTE]
83+
> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
84+
85+
## Passing attachments to components
86+
87+
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
88+
89+
This allows you to create _wrapper components_ that augment elements ([demo](/playground/untitled#H4sIAAAAAAAAE3VUS3ObMBD-KxvajnFqsJM2PhA7TXrKob31FjITAbKtRkiMtDhJPfz3LiAMdpxhGJvdb1_fPnaeYjn3Iu-WIbJ04028lZDcetHDzsO3olbVApI74F1RhHbLJdayhFl-Sp5qhVwhufEWNjWiwJtYxSjyQhsEFEXxBiujcxg1_8O_dnQ9APwsEbVyiHDafjrvDZCgkiO4MLCEzxYZcn90z6XUZ6OxA61KlaIgV6i1pFC-sxjDrlbHaDiWRoGvdMbHsLzp5DES0mJnRxGaRBvcBHb7yFUTCQeunEWYcYtGv12TqgFUDbCK1WLaM6IWQhUlQiJUFm2ZLPly51xXMG0Rjoyd69C7UqqG2nu95QZyXvtvLVpri2-SN4hoLXXCZFfhQ8aQBU1VgdEaH_vSgyBZR_BpPp_vi0tY-rw2ulRZkGqpTQRbZvwa2BPgFC8bgbw31CbjJjAsE6WNYBZeGp7vtQXLMqHWnZx-5kM1TR5ycpkZXQR2wzL94l8Ur1C_3-g168SfQf1MyfRi3LW9fs77emJEw5QV9SREoLTq06tcczq7d6xEUcJX2vAhO1b843XK34e5unZEMBr15ekuKEusluWAF8lXhE2ZTP2r2RcIHJ-163FPKerCgYJLOB9i4GvNwviI5-gAQiFFBk3tBTOU3HFXEk0R8o86WvUD64aINhv5K3oRmpJXkw8uxMG6Hh6JY9X7OwGSqfUy9tDG3sHNoEi0d_d_fv9qndxRU0VClFqo3KVo3U655Hnt1PXB3Qra2Y2QGdEwgTAMCxopsoxOe6SD0gD8movDhT0LAnhqlE8gVCpLWnRoV7OJCkFAwEXitrYL1W7p7pbiE_P7XH6E_rihODm5s52XtiH9Ekaw0VgI9exadWL1uoEYjPtg2672k5szsxbKyWB2fdT0w5Y_0hcT8oXOlRetmLS8-g-6TLXXQgYAAA==)):
90+
91+
```svelte
92+
<!--- file: Button.svelte --->
93+
<script>
94+
/** @type {import('svelte/elements').HTMLButtonAttributes} */
95+
let { children, ...props } = $props();
96+
</script>
97+
98+
<!-- `props` includes attachments -->
99+
<button {...props}>
100+
{@render children?.()}
101+
</button>
102+
```
103+
104+
```svelte
105+
<!--- file: App.svelte --->
106+
<script>
107+
import tippy from 'tippy.js';
108+
import Button from './Button.svelte';
109+
110+
let content = $state('Hello!');
111+
112+
/**
113+
* @param {string} content
114+
* @returns {import('svelte/attachments').Attachment}
115+
*/
116+
function tooltip(content) {
117+
return (element) => {
118+
const tooltip = tippy(element, { content });
119+
return tooltip.destroy;
120+
};
121+
}
122+
</script>
123+
124+
<input bind:value={content} />
125+
126+
<Button {@attach tooltip(content)}>
127+
Hover me
128+
</Button>
129+
```
130+
131+
## Controlling when attachments re-run
132+
133+
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
134+
135+
```js
136+
// @errors: 7006 2304 2552
137+
function foo(bar) {
138+
return (node) => {
139+
veryExpensiveSetupWork(node);
140+
update(node, bar);
141+
};
142+
}
143+
```
144+
145+
In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect:
146+
147+
```js
148+
// @errors: 7006 2304 2552
149+
function foo(+++getBar+++) {
150+
return (node) => {
151+
veryExpensiveSetupWork(node);
152+
153+
+++ $effect(() => {
154+
update(node, getBar());
155+
});+++
156+
}
157+
}
158+
```
159+
160+
## Creating attachments programmatically
161+
162+
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
163+
164+
## Converting actions to attachments
165+
166+
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.

0 commit comments

Comments
 (0)