Repository: sveltejs/eslint-plugin-svelte
Pull request #1197 (2025-04-17 08:10:16)

feat: add `no-add-event-listener` rule
this helped me learn about the events helpers, so figured i may as well throw it together.

let me know if there's anything i missed, as i don't think i've added a new rule in this repo before.

one thing to note is that it has a suggestion, but that relies on the user later making sure `on` has been imported correctly. im not sure we can do that automatically since each suggestion runs individually and should probably only affect the subset of code you're reporting

close: https://github.com/sveltejs/eslint-plugin-svelte/issues/1194

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: a11babd4221f502257c75cb269865639075bfbef

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/43081j/eslint-plugin-svelte/new/listeners-and-such?filename=.changeset/angry-windows-lie.md&value=---%0A%22eslint-plugin-svelte%22%3A%20patch%0A---%0A%0Afeat%3A%20add%20%60no-add-event-listener%60%20rule%0A)


github-actions[bot]: <!-- posted by pkg.pr.new-comment.mjs -->

## Try the Instant Preview in Online Playground

[ESLint Online Playground](https://eslint-online-playground.netlify.app/?overrideDeps=%7B%22eslint-plugin-svelte%22%3A%22https%3A%2F%2Fpkg.pr.new%2Feslint-plugin-svelte%40a11babd%22%7D#eslint-plugin-svelte%20with%20typescript)

## Install the Instant Preview to Your Local

```
npm i https://pkg.pr.new/eslint-plugin-svelte@a11babd
```

## Published Instant Preview Packages:

- eslint-plugin-svelte: https://pkg.pr.new/eslint-plugin-svelte@a11babd

[View Commit](https://github.com/sveltejs/eslint-plugin-svelte/commit/a11babd4221f502257c75cb269865639075bfbef)

Patch:
diff --git a/.changeset/sad-points-strive.md b/.changeset/sad-points-strive.md
new file mode 100644
index 0000000..2b53faf
--- /dev/null
+++ b/.changeset/sad-points-strive.md
@@ -0,0 +1,5 @@
+---
+'eslint-plugin-svelte': minor
+---
+
+Added `no-add-event-listener` rule to disallow usages of `addEventListener`
diff --git a/.editorconfig b/.editorconfig
index d4479be..9364f19 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -5,3 +5,9 @@ end_of_line = lf
 indent_size = 2
 indent_style = tab
 trim_trailing_whitespace = true
+
+[docs/rules/no-trailing-spaces.md]
+trim_trailing_whitespace = false
+
+[packages/eslint-plugin-svelte/tests/fixtures/**/*-input.svelte]
+trim_trailing_whitespace = false
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index ce9bdc7..e0e7fc3 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -68,13 +68,13 @@ body:
     attributes:
       label: Link to **GitHub Repo** with Minimal Reproducible Example
       description: |
-        Create a minimal reproduction of the problem. **A minimal reproduction is required** so that others can help debug your issue. If a report is vague (e.g. just a generic error message) and has no reproduction, it may be closed.  
-        [Why Reproductions are Required](https://antfu.me/posts/why-reproductions-are-required)  
+        Create a minimal reproduction of the problem. **A minimal reproduction is required** so that others can help debug your issue. If a report is vague (e.g. just a generic error message) and has no reproduction, it may be closed.\
+        [Why Reproductions are Required](https://antfu.me/posts/why-reproductions-are-required)\
 
-        Be sure to share the repo on GitHub. GitHub's repo is ready to debug using Codespace.  
-        Please DON'T USE non-GitHub repos such as GitLab as repro.  
-        It takes me a long time to prepare my local PC for debugging.  
-        Please DON'T USE stackblitz as a repro.  
+        Be sure to share the repo on GitHub. GitHub's repo is ready to debug using Codespace.\
+        Please DON'T USE non-GitHub repos such as GitLab as repro.\
+        It takes me a long time to prepare my local PC for debugging.\
+        Please DON'T USE stackblitz as a repro.\
         We can't debug eslint with stackblitz.
       placeholder: |
         https://github.com/[your]/[repo]
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 7a81ca7..88dc427 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -7,7 +7,7 @@ body:
     attributes:
       label: Description
       description: |
-        A clear and concise description of the new feature.  
+        A clear and concise description of the new feature.\
         Also give a few code examples.
     validations:
       required: true
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1f1d954..fcddde4 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -34,7 +34,7 @@ Rule tests typically use fixtures. For example, for the `indent` rule, the test
 - The `invalid` directory contains test cases where the rule should report errors.
 - The `valid` directory contains test cases where no errors are reported.
 
-Fixture input files should be named `*-input.svelte` and are automatically collected.  
+Fixture input files should be named `*-input.svelte` and are automatically collected.\
 If configuration is needed, include a JSON file:
 
 - For a specific test file (e.g., `my-test-input.svelte`), add `my-test-config.json`.
@@ -52,7 +52,7 @@ pnpm run test -- -g indent
 
 Refer to [this Stack Overflow post](https://stackoverflow.com/questions/10832031/how-to-run-a-single-test-with-mocha) for details.
 
-To test a single file (e.g., `my-test-input.svelte`), add a `my-test-config.json` with `{"only": true}`.  
+To test a single file (e.g., `my-test-input.svelte`), add a `my-test-config.json` with `{"only": true}`.
 (Remember to remove `{"only": true}` before submitting a pull request.)
 
 ## Preview Docs
diff --git a/README.md b/README.md
index edc472b..8bc75ad 100644
--- a/README.md
+++ b/README.md
@@ -30,8 +30,8 @@
 
 ## Introduction
 
-`eslint-plugin-svelte` is the official [ESLint](https://eslint.org/) plugin for [Svelte](https://svelte.dev/).  
-It leverages the AST generated by [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser) to provide custom linting for Svelte.  
+`eslint-plugin-svelte` is the official [ESLint](https://eslint.org/) plugin for [Svelte](https://svelte.dev/).\
+It leverages the AST generated by [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser) to provide custom linting for Svelte.\
 Note that `eslint-plugin-svelte` and `svelte-eslint-parser` cannot be used alongside [eslint-plugin-svelte3](https://github.com/sveltejs/eslint-plugin-svelte3).
 
 <!--USAGE_SECTION_START-->
@@ -219,8 +219,8 @@ export default [
 
 ## Editor Integrations
 
-**Visual Studio Code**  
-Install [dbaeumer.vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint).  
+**Visual Studio Code**\
+Install [dbaeumer.vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint).\
 Configure `.svelte` files in `.vscode/settings.json`:
 
 ```json
@@ -247,8 +247,8 @@ This project follows [Semantic Versioning](https://semver.org/). Unlike [ESLint
 <!-- prettier-ignore-start -->
 <!--RULES_SECTION_START-->
 
-:wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the reported problems.  
-:bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).  
+:wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the reported problems.\
+:bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).\
 :star: Indicates that the rule is included in the `plugin:svelte/recommended` config.
 
 <!--RULES_TABLE_START-->
@@ -294,6 +294,7 @@ These rules relate to better ways of doing things to help you avoid problems:
 |:--------|:------------|:---|
 | [svelte/block-lang](https://sveltejs.github.io/eslint-plugin-svelte/rules/block-lang/) | disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks. | :bulb: |
 | [svelte/button-has-type](https://sveltejs.github.io/eslint-plugin-svelte/rules/button-has-type/) | disallow usage of button without an explicit type attribute |  |
+| [svelte/no-add-event-listener](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-add-event-listener/) | Warns against the use of `addEventListener` | :bulb: |
 | [svelte/no-at-debug-tags](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/) | disallow the use of `{@debug}` | :star::bulb: |
 | [svelte/no-ignored-unsubscribe](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-ignored-unsubscribe/) | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores. |  |
 | [svelte/no-immutable-reactive-statements](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-immutable-reactive-statements/) | disallow reactive statements that don't reference reactive values. | :star: |
@@ -400,7 +401,7 @@ These rules relate to this plugin works:
 
 ## Contributing
 
-Contributions are welcome! Please open an issue or submit a PR. For more details, see [CONTRIBUTING.md](./CONTRIBUTING.md).  
+Contributions are welcome! Please open an issue or submit a PR. For more details, see [CONTRIBUTING.md](./CONTRIBUTING.md).\
 Refer to [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser) for AST details.
 
 <!--DOCS_IGNORE_END-->
diff --git a/docs-svelte-kit/package.json b/docs-svelte-kit/package.json
index 6ea86fb..6137c39 100644
--- a/docs-svelte-kit/package.json
+++ b/docs-svelte-kit/package.json
@@ -25,7 +25,7 @@
     "@types/babel__core": "^7.20.5",
     "@types/cross-spawn": "^6.0.6",
     "@types/escape-html": "^1.0.4",
-    "@types/eslint-scope": "^3.7.7",
+    "@types/eslint-scope": "^8.0.0",
     "@types/eslint-visitor-keys": "^3.3.2",
     "@types/less": "^3.0.7",
     "@types/markdown-it": "^14.1.2",
diff --git a/docs/README.md b/docs/README.md
index 9f765d5..0e49ffd 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -4,8 +4,8 @@ title: 'eslint-plugin-svelte'
 
 ## Introduction
 
-`eslint-plugin-svelte` is the official [ESLint](https://eslint.org/) plugin for [Svelte](https://svelte.dev/).  
-It leverages the AST generated by [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser) to provide custom linting for Svelte.  
+`eslint-plugin-svelte` is the official [ESLint](https://eslint.org/) plugin for [Svelte](https://svelte.dev/).\
+It leverages the AST generated by [svelte-eslint-parser](https://github.com/sveltejs/svelte-eslint-parser) to provide custom linting for Svelte.\
 Note that `eslint-plugin-svelte` and `svelte-eslint-parser` cannot be used alongside [eslint-plugin-svelte3](https://github.com/sveltejs/eslint-plugin-svelte3).
 
 See [User Guide](./user-guide.md).
diff --git a/docs/migration.md b/docs/migration.md
index 0acc972..2f9796f 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -65,7 +65,7 @@ Therefore, you need to replace the package name, and the presets, rules, and set
 
 ## From `eslint-plugin-svelte` v2 To v3
 
-This section explains the necessary changes when upgrading from `eslint-plugin-svelte` v2 to v3.  
+This section explains the necessary changes when upgrading from `eslint-plugin-svelte` v2 to v3.\
 v3 includes **support for ESLint Flat Config only**, **changes to the recommended rule set**, and other breaking changes.
 
 ---
diff --git a/docs/rules.md b/docs/rules.md
index 75dd5ed..1fdd627 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -4,8 +4,8 @@ sidebarDepth: 0
 
 # Available Rules
 
-:wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the reported problems.  
-:bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).  
+:wrench: Indicates that the rule is fixable, and using `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the reported problems.\
+:bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).\
 :star: Indicates that the rule is included in the `plugin:svelte/recommended` config.
 
 <!-- This file is automatically generated in tools/update-docs-rules-index.js, do not change! -->
@@ -51,6 +51,7 @@ These rules relate to better ways of doing things to help you avoid problems:
 | :--------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :------------- |
 | [svelte/block-lang](./rules/block-lang.md)                                               | disallows the use of languages other than those specified in the configuration for the lang attribute of `<script>` and `<style>` blocks. | :bulb:         |
 | [svelte/button-has-type](./rules/button-has-type.md)                                     | disallow usage of button without an explicit type attribute                                                                               |                |
+| [svelte/no-add-event-listener](./rules/no-add-event-listener.md)                         | Warns against the use of `addEventListener`                                                                                               | :bulb:         |
 | [svelte/no-at-debug-tags](./rules/no-at-debug-tags.md)                                   | disallow the use of `{@debug}`                                                                                                            | :star::bulb:   |
 | [svelte/no-ignored-unsubscribe](./rules/no-ignored-unsubscribe.md)                       | disallow ignoring the unsubscribe method returned by the `subscribe()` on Svelte stores.                                                  |                |
 | [svelte/no-immutable-reactive-statements](./rules/no-immutable-reactive-statements.md)   | disallow reactive statements that don't reference reactive values.                                                                        | :star:         |
diff --git a/docs/rules/no-add-event-listener.md b/docs/rules/no-add-event-listener.md
new file mode 100644
index 0000000..aa4869e
--- /dev/null
+++ b/docs/rules/no-add-event-listener.md
@@ -0,0 +1,50 @@
+---
+pageClass: 'rule-details'
+sidebarDepth: 0
+title: 'svelte/no-add-event-listener'
+description: 'Warns against the use of `addEventListener`'
+---
+
+# svelte/no-add-event-listener
+
+> Warns against the use of `addEventListener`
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+
+## :book: Rule Details
+
+Svelte relies on event delegation for performance and predictable handler order. Calling `addEventListener` inside a component skips this mechanism. This rule reports any call to `addEventListener` suggests converting to the `on()` helper from `svelte/events`.
+
+<!--eslint-skip-->
+
+```svelte
+<!-- ✓ GOOD -->
+<script>
+  /* eslint svelte/no-add-event-listener: "error" */
+  on(window, 'resize', handler);
+</script>
+```
+
+<!--eslint-skip-->
+
+```svelte
+<!-- ✗ BAD -->
+<script>
+  /* eslint svelte/no-add-event-listener: "error" */
+  window.addEventListener('resize', handler);
+</script>
+```
+
+## :books: Further reading
+
+- [svelte - event delegation]
+- [svelte/events `on` documentation]
+
+[svelte - event delegation]: https://svelte.dev/docs/svelte/basic-markup#Events-Event-delegation
+[svelte/events `on` documentation]: https://svelte.dev/docs/svelte/svelte-events#on
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/src/rules/no-add-event-listener.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/packages/eslint-plugin-svelte/tests/src/rules/no-add-event-listener.ts)
diff --git a/docs/rules/no-dynamic-slot-name.md b/docs/rules/no-dynamic-slot-name.md
index 805e5e9..8ad69b9 100644
--- a/docs/rules/no-dynamic-slot-name.md
+++ b/docs/rules/no-dynamic-slot-name.md
@@ -15,7 +15,7 @@ since: 'v0.14.0'
 
 ## :book: Rule Details
 
-This rule reports the dynamically specified `<slot>` name.  
+This rule reports the dynamically specified `<slot>` name.\
 Dynamic `<slot>` names are not allowed in Svelte, so you must use static names.
 
 The auto-fix of this rule can be replaced with a static `<slot>` name if the expression given to the `<slot>` name is static and resolvable.
diff --git a/docs/rules/no-extra-reactive-curlies.md b/docs/rules/no-extra-reactive-curlies.md
index 6d8a9f6..bc0a6ea 100644
--- a/docs/rules/no-extra-reactive-curlies.md
+++ b/docs/rules/no-extra-reactive-curlies.md
@@ -38,7 +38,7 @@ Nothing.
 
 ## :heart: Compatibility
 
-This rule was taken from [@tivac/eslint-plugin-svelte].  
+This rule was taken from [@tivac/eslint-plugin-svelte].\
 This rule is compatible with `@tivac/svelte/reactive-curlies` rule.
 
 [@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/
diff --git a/docs/rules/no-inner-declarations.md b/docs/rules/no-inner-declarations.md
index 50cff3e..aa35c9b 100644
--- a/docs/rules/no-inner-declarations.md
+++ b/docs/rules/no-inner-declarations.md
@@ -16,7 +16,7 @@ since: 'v0.0.8'
 
 This rule requires that function declarations and, optionally, variable declarations be in the root of a program or the body of a function.
 
-This rule extends the base ESLint's [no-inner-declarations] rule. The AST generated by [svelte-eslint-parser] will false positives in [no-inner-declarations] rule because the root node of the script is not the `Program`.  
+This rule extends the base ESLint's [no-inner-declarations] rule. The AST generated by [svelte-eslint-parser] will false positives in [no-inner-declarations] rule because the root node of the script is not the `Program`.\
 This rule supports [svelte-eslint-parser]'s AST.
 
 [svelte-eslint-parser]: https://github.com/sveltejs/svelte-eslint-parser
diff --git a/docs/rules/no-not-function-handler.md b/docs/rules/no-not-function-handler.md
index c1736a0..ae00551 100644
--- a/docs/rules/no-not-function-handler.md
+++ b/docs/rules/no-not-function-handler.md
@@ -14,7 +14,7 @@ since: 'v0.5.0'
 
 ## :book: Rule Details
 
-This rule reports where you used not function value in event handlers.  
+This rule reports where you used not function value in event handlers.\
 If you use a non-function value for the event handler, it event handler will not be called. It's almost always a mistake. You may have written a lot of unnecessary curly braces.
 
 <!--eslint-skip-->
diff --git a/docs/rules/no-object-in-text-mustaches.md b/docs/rules/no-object-in-text-mustaches.md
index c6f2a35..ed70788 100644
--- a/docs/rules/no-object-in-text-mustaches.md
+++ b/docs/rules/no-object-in-text-mustaches.md
@@ -14,7 +14,7 @@ since: 'v0.5.0'
 
 ## :book: Rule Details
 
-This rule disallows the use of objects in text mustache interpolation.  
+This rule disallows the use of objects in text mustache interpolation.\
 When you use an object for text interpolation, it is drawn as `[object Object]`. It's almost always a mistake. You may have written a lot of unnecessary curly braces.
 
 <!--eslint-skip-->
diff --git a/docs/rules/no-reactive-functions.md b/docs/rules/no-reactive-functions.md
index d81f186..5b5c904 100644
--- a/docs/rules/no-reactive-functions.md
+++ b/docs/rules/no-reactive-functions.md
@@ -47,7 +47,7 @@ Nothing
 
 ## :heart: Compatibility
 
-This rule was taken from [@tivac/eslint-plugin-svelte].  
+This rule was taken from [@tivac/eslint-plugin-svelte].\
 This rule is compatible with `@tivac/svelte/reactive-functions` rule.
 
 [@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/
diff --git a/docs/rules/no-reactive-literals.md b/docs/rules/no-reactive-literals.md
index d0633ae..6fb6e44 100644
--- a/docs/rules/no-reactive-literals.md
+++ b/docs/rules/no-reactive-literals.md
@@ -36,7 +36,7 @@ Nothing.
 
 ## :heart: Compatibility
 
-This rule was taken from [@tivac/eslint-plugin-svelte].  
+This rule was taken from [@tivac/eslint-plugin-svelte].\
 This rule is compatible with `@tivac/svelte/reactive-literals` rule.
 
 [@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/
diff --git a/docs/rules/no-unused-props.md b/docs/rules/no-unused-props.md
index 33f1f08..d87e29b 100644
--- a/docs/rules/no-unused-props.md
+++ b/docs/rules/no-unused-props.md
@@ -14,7 +14,7 @@ since: 'v3.2.0'
 
 ## :book: Rule Details
 
-This rule reports properties that are defined in Props but never used in the component code.  
+This rule reports properties that are defined in Props but never used in the component code.\
 It helps to detect dead code and improve component clarity by ensuring that every declared prop is utilized.
 
 This rule checks various usage patterns of props:
diff --git a/docs/rules/no-useless-mustaches.md b/docs/rules/no-useless-mustaches.md
index 5ded185..781793c 100644
--- a/docs/rules/no-useless-mustaches.md
+++ b/docs/rules/no-useless-mustaches.md
@@ -15,7 +15,7 @@ since: 'v0.0.4'
 
 ## :book: Rule Details
 
-This rule reports mustache interpolation with a string literal value.  
+This rule reports mustache interpolation with a string literal value.\
 The mustache interpolation with a string literal value can be changed to a static contents.
 
 <!--eslint-skip-->
diff --git a/docs/rules/prefer-destructured-store-props.md b/docs/rules/prefer-destructured-store-props.md
index ce9890b..6d7e2db 100644
--- a/docs/rules/prefer-destructured-store-props.md
+++ b/docs/rules/prefer-destructured-store-props.md
@@ -43,7 +43,7 @@ Nothing
 
 ## :heart: Compatibility
 
-This rule was taken from [@tivac/eslint-plugin-svelte].  
+This rule was taken from [@tivac/eslint-plugin-svelte].\
 This rule is compatible with `@tivac/svelte/store-prop-destructuring` rule.
 
 [@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/
diff --git a/docs/rules/require-optimized-style-attribute.md b/docs/rules/require-optimized-style-attribute.md
index dc526df..60380ed 100644
--- a/docs/rules/require-optimized-style-attribute.md
+++ b/docs/rules/require-optimized-style-attribute.md
@@ -14,7 +14,7 @@ since: 'v0.32.0'
 
 This rule reports `style` attributes written in a format that cannot be optimized.
 
-Svelte parses the content written in the style attribute and tries to optimize it. (See [https://github.com/sveltejs/svelte/pull/810](https://github.com/sveltejs/svelte/pull/810))  
+Svelte parses the content written in the style attribute and tries to optimize it. (See [https://github.com/sveltejs/svelte/pull/810](https://github.com/sveltejs/svelte/pull/810))\
 If Svelte can be successfully optimized, Svelte can minimize the number of re-renders.
 
 e.g.
diff --git a/docs/rules/require-store-reactive-access.md b/docs/rules/require-store-reactive-access.md
index b8d917c..b8013f3 100644
--- a/docs/rules/require-store-reactive-access.md
+++ b/docs/rules/require-store-reactive-access.md
@@ -15,7 +15,7 @@ since: 'v2.12.0'
 
 ## :book: Rule Details
 
-This rule disallow to use of the store itself as an operand.  
+This rule disallow to use of the store itself as an operand.\
 You should access the store value using the `$` prefix or the `get` function.
 
 <!--eslint-skip-->
@@ -58,7 +58,7 @@ You should access the store value using the `$` prefix or the `get` function.
 <MyComponent {...storeValue} />
 ```
 
-This rule checks the usage of store variables only if the store can be determined within a single file.  
+This rule checks the usage of store variables only if the store can be determined within a single file.
 However, when using `@typescript-eslint/parser` and full type information, this rule uses the type information to determine if the expression is a store.
 
 <!--eslint-skip-->
diff --git a/docs/rules/require-stores-init.md b/docs/rules/require-stores-init.md
index 13dcc99..78222fb 100644
--- a/docs/rules/require-stores-init.md
+++ b/docs/rules/require-stores-init.md
@@ -38,7 +38,7 @@ Nothing.
 
 ## :heart: Compatibility
 
-This rule was taken from [@tivac/eslint-plugin-svelte].  
+This rule was taken from [@tivac/eslint-plugin-svelte].\
 This rule is compatible with `@tivac/svelte/stores-initial-value` rule.
 
 [@tivac/eslint-plugin-svelte]: https://github.com/tivac/eslint-plugin-svelte/
diff --git a/docs/rules/sort-attributes.md b/docs/rules/sort-attributes.md
index 4f45af1..5d427d5 100644
--- a/docs/rules/sort-attributes.md
+++ b/docs/rules/sort-attributes.md
@@ -14,7 +14,7 @@ since: 'v2.4.0'
 
 ## :book: Rule Details
 
-This rule aims to enforce ordering of attributes.  
+This rule aims to enforce ordering of attributes.\
 The default order is:
 
 - `this` property.
@@ -175,8 +175,8 @@ If there is a spread attribute between the attributes, it will not be reported a
       - `"alphabetical"` ... Sorts the attributes of the same group in alphabetical order.
       - `"ignore"` ... Attributes in the same group are not sorted.
 
-Note that the behavior may change depending on how you specify the `order` setting.  
-For example, `bind:value` and `on:input={() => console.log(value)}` behave differently depending on the order. See <https://svelte.dev/docs#template-syntax-element-directives-bind-property> for details.  
+Note that the behavior may change depending on how you specify the `order` setting.\
+For example, `bind:value` and `on:input={() => console.log(value)}` behave differently depending on the order. See <https://svelte.dev/docs#template-syntax-element-directives-bind-property> for details.\
 By default it is designed to be sorted safely.
 
 You can use the following formats for names or patterns:
diff --git a/docs/user-guide.md b/docs/user-guide.md
index babec87..14c0613 100644
--- a/docs/user-guide.md
+++ b/docs/user-guide.md
@@ -184,8 +184,8 @@ export default [
 
 ## Editor Integrations
 
-**Visual Studio Code**  
-Install [dbaeumer.vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint).  
+**Visual Studio Code**\
+Install [dbaeumer.vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint).\
 Configure `.svelte` files in `.vscode/settings.json`:
 
 ```json
diff --git a/package.json b/package.json
index 8d92ed0..1b4433b 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
     "@typescript-eslint/parser": "^8.16.0",
     "c8": "^10.1.2",
     "env-cmd": "^10.1.0",
-    "eslint": "~9.25.0",
+    "eslint": "~9.26.0",
     "eslint-config-prettier": "^10.0.0",
     "eslint-formatter-friendly": "^7.0.0",
     "eslint-plugin-eslint-plugin": "^6.3.2",
@@ -34,11 +34,11 @@
     "eslint-plugin-markdown": "^5.1.0",
     "eslint-plugin-mdx": "^3.1.5",
     "eslint-plugin-n": "^17.14.0",
-    "eslint-plugin-node-dependencies": "^0.13.0",
+    "eslint-plugin-node-dependencies": "^1.0.0",
     "eslint-plugin-prettier": "^5.2.1",
     "eslint-plugin-regexp": "^2.7.0",
     "eslint-plugin-yml": "^1.15.0",
-    "npm-run-all2": "^7.0.1",
+    "npm-run-all2": "^8.0.0",
     "prettier": "^3.4.1",
     "prettier-plugin-svelte": "^3.3.2",
     "rimraf": "^6.0.1",
diff --git a/packages/eslint-plugin-svelte/src/rule-types.ts b/packages/eslint-plugin-svelte/src/rule-types.ts
index ac9f2a9..8c56fd8 100644
--- a/packages/eslint-plugin-svelte/src/rule-types.ts
+++ b/packages/eslint-plugin-svelte/src/rule-types.ts
@@ -99,6 +99,11 @@ export interface RuleOptions {
    * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/mustache-spacing/
    */
   'svelte/mustache-spacing'?: Linter.RuleEntry<SvelteMustacheSpacing>
+  /**
+   * Warns against the use of `addEventListener`
+   * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-add-event-listener/
+   */
+  'svelte/no-add-event-listener'?: Linter.RuleEntry<[]>
   /**
    * disallow the use of `{@debug}`
    * @see https://sveltejs.github.io/eslint-plugin-svelte/rules/no-at-debug-tags/
diff --git a/packages/eslint-plugin-svelte/src/rules/no-add-event-listener.ts b/packages/eslint-plugin-svelte/src/rules/no-add-event-listener.ts
new file mode 100644
index 0000000..9ba5583
--- /dev/null
+++ b/packages/eslint-plugin-svelte/src/rules/no-add-event-listener.ts
@@ -0,0 +1,69 @@
+import type { TSESTree } from '@typescript-eslint/types';
+
+import { createRule } from '../utils/index.js';
+import type { SuggestionReportDescriptor } from '../types.js';
+
+export default createRule('no-add-event-listener', {
+	meta: {
+		docs: {
+			description: 'Warns against the use of `addEventListener`',
+			category: 'Best Practices',
+			recommended: false
+		},
+		hasSuggestions: true,
+		schema: [],
+		messages: {
+			unexpected:
+				'Do not use `addEventListener`. Use the `on` function from `svelte/events` instead.'
+		},
+		type: 'suggestion',
+		conditions: [
+			{
+				svelteVersions: ['5']
+			}
+		]
+	},
+	create(context) {
+		return {
+			CallExpression(node: TSESTree.CallExpression) {
+				const { callee } = node;
+				let target: string | null = null;
+
+				if (
+					callee.type === 'MemberExpression' &&
+					callee.property.type === 'Identifier' &&
+					callee.property.name === 'addEventListener'
+				) {
+					target = context.sourceCode.getText(callee.object);
+				} else if (callee.type === 'Identifier' && callee.name === 'addEventListener') {
+					target = 'window';
+				}
+
+				if (target === null) {
+					return;
+				}
+
+				const openParen = context.sourceCode.getTokenAfter(callee);
+				const suggest: SuggestionReportDescriptor[] = [];
+
+				if (openParen !== null) {
+					suggest.push({
+						desc: 'Use `on` from `svelte/events` instead',
+						fix(fixer) {
+							return [
+								fixer.replaceText(callee, 'on'),
+								fixer.insertTextAfter(openParen, `${target}, `)
+							];
+						}
+					});
+				}
+
+				context.report({
+					node,
+					messageId: 'unexpected',
+					suggest
+				});
+			}
+		};
+	}
+});
diff --git a/packages/eslint-plugin-svelte/src/utils/rules.ts b/packages/eslint-plugin-svelte/src/utils/rules.ts
index e79cd96..446c49b 100644
--- a/packages/eslint-plugin-svelte/src/utils/rules.ts
+++ b/packages/eslint-plugin-svelte/src/utils/rules.ts
@@ -19,6 +19,7 @@ import indent from '../rules/indent.js';
 import infiniteReactiveLoop from '../rules/infinite-reactive-loop.js';
 import maxAttributesPerLine from '../rules/max-attributes-per-line.js';
 import mustacheSpacing from '../rules/mustache-spacing.js';
+import noAddEventListener from '../rules/no-add-event-listener.js';
 import noAtDebugTags from '../rules/no-at-debug-tags.js';
 import noAtHtmlTags from '../rules/no-at-html-tags.js';
 import noDomManipulating from '../rules/no-dom-manipulating.js';
@@ -96,6 +97,7 @@ export const rules = [
 	infiniteReactiveLoop,
 	maxAttributesPerLine,
 	mustacheSpacing,
+	noAddEventListener,
 	noAtDebugTags,
 	noAtHtmlTags,
 	noDomManipulating,
diff --git a/packages/eslint-plugin-svelte/tools/update-docs-rules-index.ts b/packages/eslint-plugin-svelte/tools/update-docs-rules-index.ts
index e0c9a42..ae38b30 100644
--- a/packages/eslint-plugin-svelte/tools/update-docs-rules-index.ts
+++ b/packages/eslint-plugin-svelte/tools/update-docs-rules-index.ts
@@ -14,8 +14,8 @@ sidebarDepth: 0
 
 # Available Rules
 
-:wrench: Indicates that the rule is fixable, and using \`--fix\` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the reported problems.  
-:bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).  
+:wrench: Indicates that the rule is fixable, and using \`--fix\` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the reported problems.\\
+:bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).\\
 :star: Indicates that the rule is included in the \`plugin:svelte/recommended\` config.
 
 <!-- This file is automatically generated in tools/update-docs-rules-index.js, do not change! -->


Test patch:
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/_requirements.json
new file mode 100644
index 0000000..0192b10
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/_requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0-0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-errors.yaml
new file mode 100644
index 0000000..db04267
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-errors.yaml
@@ -0,0 +1,396 @@
+- message: Do not use `addEventListener`. Use the `on` function from
+    `svelte/events` instead.
+  line: 17
+  column: 3
+  suggestions:
+    - desc: Use `on` from `svelte/events` instead
+      output: |
+        <script>
+        	const handler = (ev) => {
+        		console.log(ev);
+        	};
+
+        	function onClick(event) {
+        		const target = event.currentTarget;
+
+        		const deepObj = {
+        			deep: {
+        				obj: {
+        					target
+        				}
+        			}
+        		};
+
+        		on(target, 'focus', handler);
+        		deepObj.deep.obj.target.addEventListener('focus', handler);
+        	}
+
+        	addEventListener('message', handler);
+        	window.addEventListener('message', handler);
+        	document.addEventListener('visibilitychange', handler);
+
+        	// with a load of whitespace
+        	window.addEventListener    ('message', handler);
+        	// with a comment
+        	window.addEventListener/* foo */('message', handler);
+
+        	// with options
+        	window.addEventListener('message', handler, { once: true });
+
+        	// using spread
+        	window.addEventListener(...params);
+        </script>
+
+        <button onclick={onClick}>Hello</button>
+- message: Do not use `addEventListener`. Use the `on` function from
+    `svelte/events` instead.
+  line: 18
+  column: 3
+  suggestions:
+    - desc: Use `on` from `svelte/events` instead
+      output: |
+        <script>
+        	const handler = (ev) => {
+        		console.log(ev);
+        	};
+
+        	function onClick(event) {
+        		const target = event.currentTarget;
+
+        		const deepObj = {
+        			deep: {
+        				obj: {
+        					target
+        				}
+        			}
+        		};
+
+        		target.addEventListener('focus', handler);
+        		on(deepObj.deep.obj.target, 'focus', handler);
+        	}
+
+        	addEventListener('message', handler);
+        	window.addEventListener('message', handler);
+        	document.addEventListener('visibilitychange', handler);
+
+        	// with a load of whitespace
+        	window.addEventListener    ('message', handler);
+        	// with a comment
+        	window.addEventListener/* foo */('message', handler);
+
+        	// with options
+        	window.addEventListener('message', handler, { once: true });
+
+        	// using spread
+        	window.addEventListener(...params);
+        </script>
+
+        <button onclick={onClick}>Hello</button>
+- message: Do not use `addEventListener`. Use the `on` function from
+    `svelte/events` instead.
+  line: 21
+  column: 2
+  suggestions:
+    - desc: Use `on` from `svelte/events` instead
+      output: |
+        <script>
+        	const handler = (ev) => {
+        		console.log(ev);
+        	};
+
+        	function onClick(event) {
+        		const target = event.currentTarget;
+
+        		const deepObj = {
+        			deep: {
+        				obj: {
+        					target
+        				}
+        			}
+        		};
+
+        		target.addEventListener('focus', handler);
+        		deepObj.deep.obj.target.addEventListener('focus', handler);
+        	}
+
+        	on(window, 'message', handler);
+        	window.addEventListener('message', handler);
+        	document.addEventListener('visibilitychange', handler);
+
+        	// with a load of whitespace
+        	window.addEventListener    ('message', handler);
+        	// with a comment
+        	window.addEventListener/* foo */('message', handler);
+
+        	// with options
+        	window.addEventListener('message', handler, { once: true });
+
+        	// using spread
+        	window.addEventListener(...params);
+        </script>
+
+        <button onclick={onClick}>Hello</button>
+- message: Do not use `addEventListener`. Use the `on` function from
+    `svelte/events` instead.
+  line: 22
+  column: 2
+  suggestions:
+    - desc: Use `on` from `svelte/events` instead
+      output: |
+        <script>
+        	const handler = (ev) => {
+        		console.log(ev);
+        	};
+
+        	function onClick(event) {
+        		const target = event.currentTarget;
+
+        		const deepObj = {
+        			deep: {
+        				obj: {
+        					target
+        				}
+        			}
+        		};
+
+        		target.addEventListener('focus', handler);
+        		deepObj.deep.obj.target.addEventListener('focus', handler);
+        	}
+
+        	addEventListener('message', handler);
+        	on(window, 'message', handler);
+        	document.addEventListener('visibilitychange', handler);
+
+        	// with a load of whitespace
+        	window.addEventListener    ('message', handler);
+        	// with a comment
+        	window.addEventListener/* foo */('message', handler);
+
+        	// with options
+        	window.addEventListener('message', handler, { once: true });
+
+        	// using spread
+        	window.addEventListener(...params);
+        </script>
+
+        <button onclick={onClick}>Hello</button>
+- message: Do not use `addEventListener`. Use the `on` function from
+    `svelte/events` instead.
+  line: 23
+  column: 2
+  suggestions:
+    - desc: Use `on` from `svelte/events` instead
+      output: |
+        <script>
+        	const handler = (ev) => {
+        		console.log(ev);
+        	};
+
+        	function onClick(event) {
+        		const target = event.currentTarget;
+
+        		const deepObj = {
+        			deep: {
+        				obj: {
+        					target
+        				}
+        			}
+        		};
+
+        		target.addEventListener('focus', handler);
+        		deepObj.deep.obj.target.addEventListener('focus', handler);
+        	}
+
+        	addEventListener('message', handler);
+        	window.addEventListener('message', handler);
+        	on(document, 'visibilitychange', handler);
+
+        	// with a load of whitespace
+        	window.addEventListener    ('message', handler);
+        	// with a comment
+        	window.addEventListener/* foo */('message', handler);
+
+        	// with options
+        	window.addEventListener('message', handler, { once: true });
+
+        	// using spread
+        	window.addEventListener(...params);
+        </script>
+
+        <button onclick={onClick}>Hello</button>
+- message: Do not use `addEventListener`. Use the `on` function from
+    `svelte/events` instead.
+  line: 26
+  column: 2
+  suggestions:
+    - desc: Use `on` from `svelte/events` instead
+      output: |
+        <script>
+        	const handler = (ev) => {
+        		console.log(ev);
+        	};
+
+        	function onClick(event) {
+        		const target = event.currentTarget;
+
+        		const deepObj = {
+        			deep: {
+        				obj: {
+        					target
+        				}
+        			}
+        		};
+
+        		target.addEventListener('focus', handler);
+        		deepObj.deep.obj.target.addEventListener('focus', handler);
+        	}
+
+        	addEventListener('message', handler);
+        	window.addEventListener('message', handler);
+        	document.addEventListener('visibilitychange', handler);
+
+        	// with a load of whitespace
+        	on    (window, 'message', handler);
+        	// with a comment
+        	window.addEventListener/* foo */('message', handler);
+
+        	// with options
+        	window.addEventListener('message', handler, { once: true });
+
+        	// using spread
+        	window.addEventListener(...params);
+        </script>
+
+        <button onclick={onClick}>Hello</button>
+- message: Do not use `addEventListener`. Use the `on` function from
+    `svelte/events` instead.
+  line: 28
+  column: 2
+  suggestions:
+    - desc: Use `on` from `svelte/events` instead
+      output: |
+        <script>
+        	const handler = (ev) => {
+        		console.log(ev);
+        	};
+
+        	function onClick(event) {
+        		const target = event.currentTarget;
+
+        		const deepObj = {
+        			deep: {
+        				obj: {
+        					target
+        				}
+        			}
+        		};
+
+        		target.addEventListener('focus', handler);
+        		deepObj.deep.obj.target.addEventListener('focus', handler);
+        	}
+
+        	addEventListener('message', handler);
+        	window.addEventListener('message', handler);
+        	document.addEventListener('visibilitychange', handler);
+
+        	// with a load of whitespace
+        	window.addEventListener    ('message', handler);
+        	// with a comment
+        	on/* foo */(window, 'message', handler);
+
+        	// with options
+        	window.addEventListener('message', handler, { once: true });
+
+        	// using spread
+        	window.addEventListener(...params);
+        </script>
+
+        <button onclick={onClick}>Hello</button>
+- message: Do not use `addEventListener`. Use the `on` function from
+    `svelte/events` instead.
+  line: 31
+  column: 2
+  suggestions:
+    - desc: Use `on` from `svelte/events` instead
+      output: |
+        <script>
+        	const handler = (ev) => {
+        		console.log(ev);
+        	};
+
+        	function onClick(event) {
+        		const target = event.currentTarget;
+
+        		const deepObj = {
+        			deep: {
+        				obj: {
+        					target
+        				}
+        			}
+        		};
+
+        		target.addEventListener('focus', handler);
+        		deepObj.deep.obj.target.addEventListener('focus', handler);
+        	}
+
+        	addEventListener('message', handler);
+        	window.addEventListener('message', handler);
+        	document.addEventListener('visibilitychange', handler);
+
+        	// with a load of whitespace
+        	window.addEventListener    ('message', handler);
+        	// with a comment
+        	window.addEventListener/* foo */('message', handler);
+
+        	// with options
+        	on(window, 'message', handler, { once: true });
+
+        	// using spread
+        	window.addEventListener(...params);
+        </script>
+
+        <button onclick={onClick}>Hello</button>
+- message: Do not use `addEventListener`. Use the `on` function from
+    `svelte/events` instead.
+  line: 34
+  column: 2
+  suggestions:
+    - desc: Use `on` from `svelte/events` instead
+      output: |
+        <script>
+        	const handler = (ev) => {
+        		console.log(ev);
+        	};
+
+        	function onClick(event) {
+        		const target = event.currentTarget;
+
+        		const deepObj = {
+        			deep: {
+        				obj: {
+        					target
+        				}
+        			}
+        		};
+
+        		target.addEventListener('focus', handler);
+        		deepObj.deep.obj.target.addEventListener('focus', handler);
+        	}
+
+        	addEventListener('message', handler);
+        	window.addEventListener('message', handler);
+        	document.addEventListener('visibilitychange', handler);
+
+        	// with a load of whitespace
+        	window.addEventListener    ('message', handler);
+        	// with a comment
+        	window.addEventListener/* foo */('message', handler);
+
+        	// with options
+        	window.addEventListener('message', handler, { once: true });
+
+        	// using spread
+        	on(window, ...params);
+        </script>
+
+        <button onclick={onClick}>Hello</button>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-input.svelte
new file mode 100644
index 0000000..5f8a27c
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/test01-input.svelte
@@ -0,0 +1,37 @@
+<script>
+	const handler = (ev) => {
+		console.log(ev);
+	};
+
+	function onClick(event) {
+		const target = event.currentTarget;
+
+		const deepObj = {
+			deep: {
+				obj: {
+					target
+				}
+			}
+		};
+
+		target.addEventListener('focus', handler);
+		deepObj.deep.obj.target.addEventListener('focus', handler);
+	}
+
+	addEventListener('message', handler);
+	window.addEventListener('message', handler);
+	document.addEventListener('visibilitychange', handler);
+
+	// with a load of whitespace
+	window.addEventListener    ('message', handler);
+	// with a comment
+	window.addEventListener/* foo */('message', handler);
+
+	// with options
+	window.addEventListener('message', handler, { once: true });
+
+	// using spread
+	window.addEventListener(...params);
+</script>
+
+<button onclick={onClick}>Hello</button>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-errors.yaml b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-errors.yaml
new file mode 100644
index 0000000..b1c8fc4
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-errors.yaml
@@ -0,0 +1,17 @@
+- message: Do not use `addEventListener`. Use the `on` function from
+    `svelte/events` instead.
+  line: 6
+  column: 2
+  suggestions:
+    - desc: Use `on` from `svelte/events` instead
+      output: |
+        <script lang="ts">
+        	const handler = (ev: Event) => {
+        		console.log(ev);
+        	};
+
+        	on(window as any, 'message', handler);
+        	(window.addEventListener as any)('message', handler);
+        </script>
+
+        <div>Hello</div>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-input.svelte
new file mode 100644
index 0000000..180366a
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/invalid/typescript01-input.svelte
@@ -0,0 +1,10 @@
+<script lang="ts">
+	const handler = (ev: Event) => {
+		console.log(ev);
+	};
+
+	(window as any).addEventListener('message', handler);
+	(window.addEventListener as any)('message', handler);
+</script>
+
+<div>Hello</div>
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/_requirements.json b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/_requirements.json
new file mode 100644
index 0000000..0192b10
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/_requirements.json
@@ -0,0 +1,3 @@
+{
+	"svelte": ">=5.0.0-0"
+}
diff --git a/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/test01-input.svelte b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/test01-input.svelte
new file mode 100644
index 0000000..0b48a7d
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/fixtures/rules/no-add-event-listener/valid/test01-input.svelte
@@ -0,0 +1,19 @@
+<script>
+	import { on } from 'svelte/events';
+
+	const handler = (ev) => {
+		console.log(ev);
+	};
+
+	function onClick(event) {
+		const target = event.currentTarget;
+
+		on(target, 'focus', handler);
+	}
+
+	on(window, 'message', handler);
+	on(document, 'visibilitychange', handler);
+
+</script>
+
+<button onclick={onClick}>Hello</button>
diff --git a/packages/eslint-plugin-svelte/tests/src/rules/no-add-event-listener.ts b/packages/eslint-plugin-svelte/tests/src/rules/no-add-event-listener.ts
new file mode 100644
index 0000000..a2b5928
--- /dev/null
+++ b/packages/eslint-plugin-svelte/tests/src/rules/no-add-event-listener.ts
@@ -0,0 +1,12 @@
+import { RuleTester } from '../../utils/eslint-compat.js';
+import rule from '../../../src/rules/no-add-event-listener.js';
+import { loadTestCases } from '../../utils/utils.js';
+
+const tester = new RuleTester({
+	languageOptions: {
+		ecmaVersion: 2020,
+		sourceType: 'module'
+	}
+});
+
+tester.run('no-add-event-listener', rule as any, loadTestCases('no-add-event-listener'));