diff --git a/.changeset/spicy-peas-vanish.md b/.changeset/spicy-peas-vanish.md
new file mode 100644
index 000000000000..0c377b61899b
--- /dev/null
+++ b/.changeset/spicy-peas-vanish.md
@@ -0,0 +1,5 @@
+---
+"svelte": patch
+---
+
+feat: add svelte/events package and export `on` function
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index bf6c66846658..5c891d52878d 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -81,6 +81,10 @@
"./transition": {
"types": "./types/index.d.ts",
"default": "./src/transition/index.js"
+ },
+ "./events": {
+ "types": "./types/index.d.ts",
+ "default": "./src/events/index.js"
}
},
"repository": {
diff --git a/packages/svelte/scripts/generate-types.js b/packages/svelte/scripts/generate-types.js
index c52693f5f342..bb195f5f8230 100644
--- a/packages/svelte/scripts/generate-types.js
+++ b/packages/svelte/scripts/generate-types.js
@@ -33,6 +33,7 @@ await createBundle({
[`${pkg.name}/server`]: `${dir}/src/server/index.js`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,
+ [`${pkg.name}/events`]: `${dir}/src/events/index.js`,
// TODO remove in Svelte 6
[`${pkg.name}/types/compiler/preprocess`]: `${dir}/src/compiler/preprocess/legacy-public.d.ts`,
[`${pkg.name}/types/compiler/interfaces`]: `${dir}/src/compiler/types/legacy-interfaces.d.ts`
diff --git a/packages/svelte/src/events/index.js b/packages/svelte/src/events/index.js
new file mode 100644
index 000000000000..ba5bd46c5240
--- /dev/null
+++ b/packages/svelte/src/events/index.js
@@ -0,0 +1 @@
+export { on } from '../internal/client/dom/elements/events.js';
diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js
index 480314f8ca8a..33cb2c3ca0e5 100644
--- a/packages/svelte/src/internal/client/dom/elements/events.js
+++ b/packages/svelte/src/internal/client/dom/elements/events.js
@@ -66,6 +66,24 @@ export function create_event(event_name, dom, handler, options) {
return target_handler;
}
+/**
+ * Attaches an event handler to an element and returns a function that removes the handler. Using this
+ * rather than `addEventListener` will preserve the correct order relative to handlers added declaratively
+ * (with attributes like `onclick`), which use event delegation for performance reasons
+ *
+ * @param {Element} element
+ * @param {string} type
+ * @param {EventListener} handler
+ * @param {AddEventListenerOptions} [options]
+ */
+export function on(element, type, handler, options = {}) {
+ var target_handler = create_event(type, element, handler, options);
+
+ return () => {
+ element.removeEventListener(type, target_handler, options);
+ };
+}
+
/**
* @param {string} event_name
* @param {Element} dom
diff --git a/packages/svelte/tests/runtime-runes/samples/event-on/_config.js b/packages/svelte/tests/runtime-runes/samples/event-on/_config.js
new file mode 100644
index 000000000000..23e33a3f4acb
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-on/_config.js
@@ -0,0 +1,17 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ mode: ['client'],
+
+ test({ assert, target, logs }) {
+ const [b1] = target.querySelectorAll('button');
+
+ b1?.click();
+ b1?.click();
+ b1?.click();
+ flushSync();
+ assert.htmlEqual(target.innerHTML, '');
+ assert.deepEqual(logs, []);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/event-on/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-on/main.svelte
new file mode 100644
index 000000000000..2ab365590889
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/event-on/main.svelte
@@ -0,0 +1,23 @@
+
+
+ console.log('logged from onclick')}>
+
+
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 48827bfc7e00..a8b8515dda75 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -2336,6 +2336,17 @@ declare module 'svelte/transition' {
}) => () => TransitionConfig];
}
+declare module 'svelte/events' {
+ /**
+ * Attaches an event handler to an element and returns a function that removes the handler. Using this
+ * rather than `addEventListener` will preserve the correct order relative to handlers added declaratively
+ * (with attributes like `onclick`), which use event delegation for performance reasons
+ *
+ *
+ */
+ export function on(element: Element, type: string, handler: EventListener, options?: AddEventListenerOptions | undefined): () => void;
+}
+
declare module 'svelte/types/compiler/preprocess' {
/** @deprecated import this from 'svelte/preprocess' instead */
export type MarkupPreprocessor = MarkupPreprocessor_1;
diff --git a/sites/svelte-5-preview/src/lib/autocomplete.js b/sites/svelte-5-preview/src/lib/autocomplete.js
index ff0c2f741059..7fe6b8e4c1a1 100644
--- a/sites/svelte-5-preview/src/lib/autocomplete.js
+++ b/sites/svelte-5-preview/src/lib/autocomplete.js
@@ -145,6 +145,7 @@ export function autocomplete(context, selected, files) {
'svelte',
'svelte/animate',
'svelte/easing',
+ 'svelte/events',
'svelte/legacy',
'svelte/motion',
'svelte/reactivity',
diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md
index 61a4f46d410a..de77e2914f12 100644
--- a/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md
+++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/05-imports.md
@@ -115,6 +115,30 @@ Svelte provides reactive `Map`, `Set`, `Date` and `URL` classes. These can be im
```
+## `svelte/events`
+
+Where possible, event handlers added with [attributes like `onclick`](/docs/event-handlers) use a technique called _event delegation_. It works by creating a single handler for each event type on the root DOM element, rather than creating a handler for each element, resulting in better performance and memory usage.
+
+Delegated event handlers run after other event handlers. In other words, a handler added programmatically with [`addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) will run _before_ a handler added declaratively with `onclick`, regardless of their relative position in the DOM ([demo](/#H4sIAAAAAAAAE41Sy2rDMBD8lUUXJxDiu-sYeugt_YK6h8RaN6LyykgrQzH6965shxJooQc_RhrNzA6aVW8sBlW9zYouA6pKPY-jOij-GjMIE1pGwcFF3-WVOnTejNy01LIZRucZZnD06iIxJOi9G6BYjxVPmZQfiwzaTBkL2ti73R5ODcwLiftIHRtHcLuQtuhlc9tpuSyBbyZAuLloNfhIELBzpO8E-Q_O4tG6j13hIqO_y0BvPOpiv0bhtJ1Y3pLoeNH6ZULiswmMJLZFZ033WRzuAvstdMseOXqCh9SriMfBTfgPnZxg-aYM6_KnS6pFCK6GdJVHPc0C01JyfY0slUnHi-JpfgjwSzUycdgmfOjFEP3RS1qdhJ8dYMDFt1yNmxxU0jRyCwanTW9Qq4p9xPSevgHI3m43QAIAAA==)). It also means that calling `event.stopPropagation()` inside a declarative handler _won't_ prevent the programmatic handler (created inside an action, for example) from running.
+
+To preserve the relative order, use `on` rather than `addEventListener` ([demo](/#H4sIAAAAAAAAE3VRy26DMBD8lZUvECkqdwpI_YB-QdJDgpfGqlkjex2pQv73rnmoStQeMB52dnZmmdVgLAZVn2ZFlxFVrd6mSR0Vf08ZhDtaRsHBRd_nL03ovZm4O9OZzTg5zzCDo3cXiSHB4N0IxdpWvD6RnuoV3pE4rLT8WGTQ5p6xoE20LA_QdjAvJB4i9WxE6nYhbdFLcaucuaqAbyZAuLloNfhIELB3pHeC3IOz-GLdZ1m4yOh3GRiMR10cViucto7l9MjRk9gvxdsRit6a_qs47q1rT8qvpvpdDjXChqshXWdT7SwwLVtrrpElnAguSu38EPCPEOItbF4eEhiifxKkdZLw8wQYcZlbrYO7bFTcdPJbR6fNYFCrmn3E9JF-AJZOg9MRAgAA)):
+
+```js
+// @filename: index.ts
+const element: Element = null as any;
+// ---cut---
+import { on } from 'svelte/events';
+
+const off = on(element, 'click', () => {
+ console.log('element was clicked');
+});
+
+// later, if we need to remove the event listener:
+off();
+```
+
+`on` also accepts an optional fourth argument which matches the options argument for `addEventListener`.
+
## `svelte/server`
### `render`