diff --git a/.changeset/wise-tigers-happen.md b/.changeset/wise-tigers-happen.md
new file mode 100644
index 0000000000..53cc671859
--- /dev/null
+++ b/.changeset/wise-tigers-happen.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: attachments `fromAction` utility
diff --git a/documentation/docs/03-template-syntax/09-@attach.md b/documentation/docs/03-template-syntax/09-@attach.md
index 3644cbc8e8..b25fbb32a6 100644
--- a/documentation/docs/03-template-syntax/09-@attach.md
+++ b/documentation/docs/03-template-syntax/09-@attach.md
@@ -160,3 +160,7 @@ function foo(+++getBar+++) {
## Creating attachments programmatically
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
+
+## Converting actions to attachments
+
+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.
diff --git a/packages/svelte/src/attachments/index.js b/packages/svelte/src/attachments/index.js
index 948a19f4dd..dc2f3e93d7 100644
--- a/packages/svelte/src/attachments/index.js
+++ b/packages/svelte/src/attachments/index.js
@@ -1,4 +1,9 @@
+/** @import { Action, ActionReturn } from '../action/public' */
+/** @import { Attachment } from './public' */
+import { noop, render_effect } from 'svelte/internal/client';
import { ATTACHMENT_KEY } from '../constants.js';
+import { untrack } from 'svelte';
+import { teardown } from '../internal/client/reactivity/effects.js';
/**
* Creates an object key that will be recognised as an attachment when the object is spread onto an element,
@@ -25,3 +30,84 @@ import { ATTACHMENT_KEY } from '../constants.js';
export function createAttachmentKey() {
return Symbol(ATTACHMENT_KEY);
}
+
+/**
+ * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
+ * It's useful if you want to start using attachments on components but you have actions provided by a library.
+ *
+ * Note that the second argument, if provided, must be a function that _returns_ the argument to the
+ * action function, not the argument itself.
+ *
+ * ```svelte
+ *
+ *
...
+ *
+ *
+ *
bar)}>...
+ * ```
+ * @template {EventTarget} E
+ * @template {unknown} T
+ * @overload
+ * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function
+ * @param {() => T} fn A function that returns the argument for the action
+ * @returns {Attachment}
+ */
+/**
+ * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
+ * It's useful if you want to start using attachments on components but you have actions provided by a library.
+ *
+ * Note that the second argument, if provided, must be a function that _returns_ the argument to the
+ * action function, not the argument itself.
+ *
+ * ```svelte
+ *
+ *
...
+ *
+ *
+ *
bar)}>...
+ * ```
+ * @template {EventTarget} E
+ * @overload
+ * @param {Action | ((element: E) => void | ActionReturn)} action The action function
+ * @returns {Attachment}
+ */
+/**
+ * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
+ * It's useful if you want to start using attachments on components but you have actions provided by a library.
+ *
+ * Note that the second argument, if provided, must be a function that _returns_ the argument to the
+ * action function, not the argument itself.
+ *
+ * ```svelte
+ *
+ *
...
+ *
+ *
+ *
bar)}>...
+ * ```
+ *
+ * @template {EventTarget} E
+ * @template {unknown} T
+ * @param {Action | ((element: E, arg: T) => void | ActionReturn)} action The action function
+ * @param {() => T} fn A function that returns the argument for the action
+ * @returns {Attachment}
+ * @since 5.32
+ */
+export function fromAction(action, fn = /** @type {() => T} */ (noop)) {
+ return (element) => {
+ const { update, destroy } = untrack(() => action(element, fn()) ?? {});
+
+ if (update) {
+ var ran = false;
+ render_effect(() => {
+ const arg = fn();
+ if (ran) update(arg);
+ });
+ ran = true;
+ }
+
+ if (destroy) {
+ teardown(destroy);
+ }
+ };
+}
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js
new file mode 100644
index 0000000000..2e53f0d29d
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/_config.js
@@ -0,0 +1,116 @@
+import { ok, test } from '../../test';
+import { flushSync } from 'svelte';
+
+export default test({
+ async test({ assert, target, logs }) {
+ const [btn, btn2, btn3] = target.querySelectorAll('button');
+
+ // both logs on creation it will not log on change
+ assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment']);
+
+ // clicking the first button logs the right value
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0]);
+
+ // clicking the second button logs the right value
+ flushSync(() => {
+ btn2?.click();
+ });
+ assert.deepEqual(logs, ['create', 0, 'action', 'create', 0, 'attachment', 0, 0]);
+
+ // updating the arguments logs the update function for both
+ flushSync(() => {
+ btn3?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment'
+ ]);
+
+ // clicking the first button again shows the right value
+ flushSync(() => {
+ btn?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment',
+ 1
+ ]);
+
+ // clicking the second button again shows the right value
+ flushSync(() => {
+ btn2?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment',
+ 1,
+ 1
+ ]);
+
+ // unmounting logs the destroy function for both
+ flushSync(() => {
+ btn3?.click();
+ });
+ assert.deepEqual(logs, [
+ 'create',
+ 0,
+ 'action',
+ 'create',
+ 0,
+ 'attachment',
+ 0,
+ 0,
+ 'update',
+ 1,
+ 'action',
+ 'update',
+ 1,
+ 'attachment',
+ 1,
+ 1,
+ 'destroy',
+ 'action',
+ 'destroy',
+ 'attachment'
+ ]);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte
new file mode 100644
index 0000000000..35079aa15e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-from-action/main.svelte
@@ -0,0 +1,37 @@
+
+
+{#if count < 2}
+
+
+{/if}
+
+
\ No newline at end of file
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 1fda9a36b8..fcc1ec315c 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -658,6 +658,107 @@ declare module 'svelte/attachments' {
* @since 5.29
*/
export function createAttachmentKey(): symbol;
+ /**
+ * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
+ * It's useful if you want to start using attachments on components but you have actions provided by a library.
+ *
+ * Note that the second argument, if provided, must be a function that _returns_ the argument to the
+ * action function, not the argument itself.
+ *
+ * ```svelte
+ *
+ *
...
+ *
+ *
+ *
bar)}>...
+ * ```
+ * */
+ export function fromAction(action: Action | ((element: E, arg: T) => void | ActionReturn), fn: () => T): Attachment;
+ /**
+ * Converts an [action](https://svelte.dev/docs/svelte/use) into an [attachment](https://svelte.dev/docs/svelte/@attach) keeping the same behavior.
+ * It's useful if you want to start using attachments on components but you have actions provided by a library.
+ *
+ * Note that the second argument, if provided, must be a function that _returns_ the argument to the
+ * action function, not the argument itself.
+ *
+ * ```svelte
+ *
+ *
...
+ *
+ *
+ *
bar)}>...
+ * ```
+ * */
+ export function fromAction(action: Action | ((element: E) => void | ActionReturn)): Attachment;
+ /**
+ * Actions can return an object containing the two properties defined in this interface. Both are optional.
+ * - update: An action can have a parameter. This method will be called whenever that parameter changes,
+ * immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn` both
+ * mean that the action accepts no parameters.
+ * - destroy: Method that is called after the element is unmounted
+ *
+ * Additionally, you can specify which additional attributes and events the action enables on the applied element.
+ * This applies to TypeScript typings only and has no effect at runtime.
+ *
+ * Example usage:
+ * ```ts
+ * interface Attributes {
+ * newprop?: string;
+ * 'on:event': (e: CustomEvent) => void;
+ * }
+ *
+ * export function myAction(node: HTMLElement, parameter: Parameter): ActionReturn {
+ * // ...
+ * return {
+ * update: (updatedParameter) => {...},
+ * destroy: () => {...}
+ * };
+ * }
+ * ```
+ */
+ interface ActionReturn<
+ Parameter = undefined,
+ Attributes extends Record = Record
+ > {
+ update?: (parameter: Parameter) => void;
+ destroy?: () => void;
+ /**
+ * ### DO NOT USE THIS
+ * This exists solely for type-checking and has no effect at runtime.
+ * Set this through the `Attributes` generic instead.
+ */
+ $$_attributes?: Attributes;
+ }
+
+ /**
+ * Actions are functions that are called when an element is created.
+ * You can use this interface to type such actions.
+ * The following example defines an action that only works on `
` elements
+ * and optionally accepts a parameter which it has a default value for:
+ * ```ts
+ * export const myAction: Action = (node, param = { someProperty: true }) => {
+ * // ...
+ * }
+ * ```
+ * `Action` and `Action` both signal that the action accepts no parameters.
+ *
+ * You can return an object with methods `update` and `destroy` from the function and type which additional attributes and events it has.
+ * See interface `ActionReturn` for more details.
+ */
+ interface Action<
+ Element = HTMLElement,
+ Parameter = undefined,
+ Attributes extends Record = Record
+ > {
+ (
+ ...args: undefined extends Parameter
+ ? [node: Node, parameter?: Parameter]
+ : [node: Node, parameter: Parameter]
+ ): void | ActionReturn;
+ }
+
+ // Implementation notes:
+ // - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode
export {};
}