diff --git a/.changeset/wise-flies-lay.md b/.changeset/wise-flies-lay.md
new file mode 100644
index 000000000..a5360b7b7
--- /dev/null
+++ b/.changeset/wise-flies-lay.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-svelte": minor
+---
+
+feat: add `no-restricted-html-elements` rule
diff --git a/README.md b/README.md
index a5661af1c..f512560a2 100644
--- a/README.md
+++ b/README.md
@@ -367,6 +367,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
| [svelte/max-attributes-per-line](https://sveltejs.github.io/eslint-plugin-svelte/rules/max-attributes-per-line/) | enforce the maximum number of attributes per line | :wrench: |
| [svelte/mustache-spacing](https://sveltejs.github.io/eslint-plugin-svelte/rules/mustache-spacing/) | enforce unified spacing in mustache | :wrench: |
| [svelte/no-extra-reactive-curlies](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-extra-reactive-curlies/) | disallow wrapping single reactive statements in curly braces | :bulb: |
+| [svelte/no-restricted-html-elements](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-restricted-html-elements/) | disallow specific HTML elements | |
| [svelte/no-spaces-around-equal-signs-in-attribute](https://sveltejs.github.io/eslint-plugin-svelte/rules/no-spaces-around-equal-signs-in-attribute/) | disallow spaces around equal signs in attribute | :wrench: |
| [svelte/prefer-class-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-class-directive/) | require class directives instead of ternary expressions | :wrench: |
| [svelte/prefer-style-directive](https://sveltejs.github.io/eslint-plugin-svelte/rules/prefer-style-directive/) | require style directives instead of style attribute | :wrench: |
diff --git a/docs/rules.md b/docs/rules.md
index ef85cb467..644183301 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -80,6 +80,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
| [svelte/max-attributes-per-line](./rules/max-attributes-per-line.md) | enforce the maximum number of attributes per line | :wrench: |
| [svelte/mustache-spacing](./rules/mustache-spacing.md) | enforce unified spacing in mustache | :wrench: |
| [svelte/no-extra-reactive-curlies](./rules/no-extra-reactive-curlies.md) | disallow wrapping single reactive statements in curly braces | :bulb: |
+| [svelte/no-restricted-html-elements](./rules/no-restricted-html-elements.md) | disallow specific HTML elements | |
| [svelte/no-spaces-around-equal-signs-in-attribute](./rules/no-spaces-around-equal-signs-in-attribute.md) | disallow spaces around equal signs in attribute | :wrench: |
| [svelte/prefer-class-directive](./rules/prefer-class-directive.md) | require class directives instead of ternary expressions | :wrench: |
| [svelte/prefer-style-directive](./rules/prefer-style-directive.md) | require style directives instead of style attribute | :wrench: |
diff --git a/docs/rules/no-restricted-html-elements.md b/docs/rules/no-restricted-html-elements.md
new file mode 100644
index 000000000..b7cac878b
--- /dev/null
+++ b/docs/rules/no-restricted-html-elements.md
@@ -0,0 +1,107 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/no-restricted-html-elements"
+description: "disallow specific HTML elements"
+---
+
+# svelte/no-restricted-html-elements
+
+> disallow specific HTML elements
+
+- :exclamation: **_This rule has not been released yet._**
+
+## :book: Rule Details
+
+This rule reports to usage of resticted HTML elements.
+
+
+
+
+
+```svelte
+
+
+
+
+
+
+foo
+
+
+
bar
+
+```
+
+
+
+---
+
+
+
+
+
+```svelte
+
+
+
+
+
+
+
+
+
+
+
+```
+
+
+
+## :wrench: Options
+
+This rule takes a list of strings, where each string is an HTML element name to be restricted:
+
+```json
+{
+ "svelte/no-restricted-html-elements": [
+ "error",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6"
+ ]
+}
+```
+
+Alternatively, the rule also accepts objects.
+
+```json
+{
+ "svelte/no-restricted-html-elements": [
+ "error",
+ {
+ "elements": ["h1", "h2", "h3", "h4", "h5", "h6"],
+ "message": "Prefer use of our custom component"
+ },
+ {
+ "elements": ["marquee"],
+ "message": "Do not use deprecated HTML tags"
+ }
+ ]
+}
+```
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/src/rules/no-restricted-html-elements.ts)
+- [Test source](https://github.com/sveltejs/eslint-plugin-svelte/blob/main/tests/src/rules/no-restricted-html-elements.ts)
diff --git a/src/rules/no-restricted-html-elements.ts b/src/rules/no-restricted-html-elements.ts
new file mode 100644
index 000000000..979333012
--- /dev/null
+++ b/src/rules/no-restricted-html-elements.ts
@@ -0,0 +1,63 @@
+import { createRule } from "../utils"
+
+export default createRule("no-restricted-html-elements", {
+ meta: {
+ docs: {
+ description: "disallow specific HTML elements",
+ category: "Stylistic Issues",
+ recommended: false,
+ conflictWithPrettier: false,
+ },
+ schema: {
+ type: "array",
+ items: {
+ oneOf: [
+ { type: "string" },
+ {
+ type: "object",
+ properties: {
+ elements: {
+ type: "array",
+ items: {
+ type: ["string"],
+ },
+ uniqueItems: true,
+ minItems: 1,
+ },
+ message: { type: "string", minLength: 1 },
+ },
+ additionalProperties: false,
+ minItems: 1,
+ },
+ ],
+ },
+ uniqueItems: true,
+ minItems: 1,
+ },
+ messages: {},
+ type: "suggestion",
+ },
+ create(context) {
+ return {
+ SvelteElement(node) {
+ if (node.kind !== "html") return
+ const { name } = node
+ if (name.type !== "SvelteName") return
+ for (const option of context.options) {
+ const message =
+ option.message ||
+ `Unexpected use of forbidden HTML element ${name.name}.`
+ const elements = option.elements || [option]
+ for (const element of elements) {
+ if (element === name.name) {
+ context.report({
+ message,
+ node: node.startTag,
+ })
+ }
+ }
+ }
+ },
+ }
+ },
+})
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index 7dac11750..3e4141f65 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -34,6 +34,7 @@ import noObjectInTextMustaches from "../rules/no-object-in-text-mustaches"
import noReactiveFunctions from "../rules/no-reactive-functions"
import noReactiveLiterals from "../rules/no-reactive-literals"
import noReactiveReassign from "../rules/no-reactive-reassign"
+import noRestrictedHtmlElements from "../rules/no-restricted-html-elements"
import noShorthandStylePropertyOverrides from "../rules/no-shorthand-style-property-overrides"
import noSpacesAroundEqualSignsInAttribute from "../rules/no-spaces-around-equal-signs-in-attribute"
import noStoreAsync from "../rules/no-store-async"
@@ -93,6 +94,7 @@ export const rules = [
noReactiveFunctions,
noReactiveLiterals,
noReactiveReassign,
+ noRestrictedHtmlElements,
noShorthandStylePropertyOverrides,
noSpacesAroundEqualSignsInAttribute,
noStoreAsync,
diff --git a/tests/fixtures/rules/no-restricted-html-elements/invalid/array-config.json b/tests/fixtures/rules/no-restricted-html-elements/invalid/array-config.json
new file mode 100644
index 000000000..e5e624770
--- /dev/null
+++ b/tests/fixtures/rules/no-restricted-html-elements/invalid/array-config.json
@@ -0,0 +1,3 @@
+{
+ "options": ["h1", "h2", "h3", "h4", "h5", "h6"]
+}
diff --git a/tests/fixtures/rules/no-restricted-html-elements/invalid/array-errors.yaml b/tests/fixtures/rules/no-restricted-html-elements/invalid/array-errors.yaml
new file mode 100644
index 000000000..4242fd1f1
--- /dev/null
+++ b/tests/fixtures/rules/no-restricted-html-elements/invalid/array-errors.yaml
@@ -0,0 +1,24 @@
+- message: Unexpected use of forbidden HTML element h1.
+ line: 1
+ column: 1
+ suggestions: null
+- message: Unexpected use of forbidden HTML element h2.
+ line: 2
+ column: 1
+ suggestions: null
+- message: Unexpected use of forbidden HTML element h3.
+ line: 3
+ column: 1
+ suggestions: null
+- message: Unexpected use of forbidden HTML element h4.
+ line: 4
+ column: 1
+ suggestions: null
+- message: Unexpected use of forbidden HTML element h5.
+ line: 5
+ column: 1
+ suggestions: null
+- message: Unexpected use of forbidden HTML element h6.
+ line: 6
+ column: 1
+ suggestions: null
diff --git a/tests/fixtures/rules/no-restricted-html-elements/invalid/array-input.svelte b/tests/fixtures/rules/no-restricted-html-elements/invalid/array-input.svelte
new file mode 100644
index 000000000..2caff83b1
--- /dev/null
+++ b/tests/fixtures/rules/no-restricted-html-elements/invalid/array-input.svelte
@@ -0,0 +1,6 @@
+Main Title - H1
+Subtitle - H2
+Subsection Title - H3
+Sub-Subsection Title - H4
+Minor Title - H5
+Minor Subtitle - H6
diff --git a/tests/fixtures/rules/no-restricted-html-elements/invalid/object-config.json b/tests/fixtures/rules/no-restricted-html-elements/invalid/object-config.json
new file mode 100644
index 000000000..9f76c45b7
--- /dev/null
+++ b/tests/fixtures/rules/no-restricted-html-elements/invalid/object-config.json
@@ -0,0 +1,12 @@
+{
+ "options": [
+ {
+ "elements": ["h1", "h2", "h3", "h4", "h5", "h6"],
+ "message": "Prefer use of our custom component"
+ },
+ {
+ "elements": ["marquee"],
+ "message": "Do not use deprecated HTML tags"
+ }
+ ]
+}
diff --git a/tests/fixtures/rules/no-restricted-html-elements/invalid/object-errors.yaml b/tests/fixtures/rules/no-restricted-html-elements/invalid/object-errors.yaml
new file mode 100644
index 000000000..418419380
--- /dev/null
+++ b/tests/fixtures/rules/no-restricted-html-elements/invalid/object-errors.yaml
@@ -0,0 +1,48 @@
+- message: Prefer use of our custom component
+ line: 1
+ column: 1
+ suggestions: null
+- message: Prefer use of our custom component
+ line: 2
+ column: 1
+ suggestions: null
+- message: Prefer use of our custom component
+ line: 3
+ column: 1
+ suggestions: null
+- message: Prefer use of our custom component
+ line: 4
+ column: 1
+ suggestions: null
+- message: Prefer use of our custom component
+ line: 5
+ column: 1
+ suggestions: null
+- message: Prefer use of our custom component
+ line: 6
+ column: 1
+ suggestions: null
+- message: Do not use deprecated HTML tags
+ line: 8
+ column: 1
+ suggestions: null
+- message: Do not use deprecated HTML tags
+ line: 10
+ column: 1
+ suggestions: null
+- message: Do not use deprecated HTML tags
+ line: 12
+ column: 1
+ suggestions: null
+- message: Do not use deprecated HTML tags
+ line: 19
+ column: 3
+ suggestions: null
+- message: Do not use deprecated HTML tags
+ line: 23
+ column: 3
+ suggestions: null
+- message: Prefer use of our custom component
+ line: 27
+ column: 3
+ suggestions: null
diff --git a/tests/fixtures/rules/no-restricted-html-elements/invalid/object-input.svelte b/tests/fixtures/rules/no-restricted-html-elements/invalid/object-input.svelte
new file mode 100644
index 000000000..414fc87b0
--- /dev/null
+++ b/tests/fixtures/rules/no-restricted-html-elements/invalid/object-input.svelte
@@ -0,0 +1,28 @@
+Main Title - H1
+Subtitle - H2
+Subsection Title - H3
+Sub-Subsection Title - H4
+Minor Title - H5
+Minor Subtitle - H6
+
+
+
+
+
+
+
+
+ This text will scroll from right to left
+
+
+
+
Minor Subtitle - H6
+
diff --git a/tests/fixtures/rules/no-restricted-html-elements/valid/array-config.json b/tests/fixtures/rules/no-restricted-html-elements/valid/array-config.json
new file mode 100644
index 000000000..e5e624770
--- /dev/null
+++ b/tests/fixtures/rules/no-restricted-html-elements/valid/array-config.json
@@ -0,0 +1,3 @@
+{
+ "options": ["h1", "h2", "h3", "h4", "h5", "h6"]
+}
diff --git a/tests/fixtures/rules/no-restricted-html-elements/valid/array-input.svelte b/tests/fixtures/rules/no-restricted-html-elements/valid/array-input.svelte
new file mode 100644
index 000000000..29d43d502
--- /dev/null
+++ b/tests/fixtures/rules/no-restricted-html-elements/valid/array-input.svelte
@@ -0,0 +1,23 @@
+This is a title
+This is a subtitle
+This is a paragraph. Some sample text goes here.
+
+ - This is
+ - a list item
+
+
+
+ Table Cell 1 |
+ Table Cell 2 |
+
+
+ Table Cell 3 |
+ Table Cell 4 |
+
+
+This is a hyperlink to example.com
+
diff --git a/tests/fixtures/rules/no-restricted-html-elements/valid/object-config.json b/tests/fixtures/rules/no-restricted-html-elements/valid/object-config.json
new file mode 100644
index 000000000..9f76c45b7
--- /dev/null
+++ b/tests/fixtures/rules/no-restricted-html-elements/valid/object-config.json
@@ -0,0 +1,12 @@
+{
+ "options": [
+ {
+ "elements": ["h1", "h2", "h3", "h4", "h5", "h6"],
+ "message": "Prefer use of our custom component"
+ },
+ {
+ "elements": ["marquee"],
+ "message": "Do not use deprecated HTML tags"
+ }
+ ]
+}
diff --git a/tests/fixtures/rules/no-restricted-html-elements/valid/object-input.svelte b/tests/fixtures/rules/no-restricted-html-elements/valid/object-input.svelte
new file mode 100644
index 000000000..29d43d502
--- /dev/null
+++ b/tests/fixtures/rules/no-restricted-html-elements/valid/object-input.svelte
@@ -0,0 +1,23 @@
+This is a title
+This is a subtitle
+This is a paragraph. Some sample text goes here.
+
+ - This is
+ - a list item
+
+
+
+ Table Cell 1 |
+ Table Cell 2 |
+
+
+ Table Cell 3 |
+ Table Cell 4 |
+
+
+This is a hyperlink to example.com
+
diff --git a/tests/src/rules/no-restricted-html-elements.ts b/tests/src/rules/no-restricted-html-elements.ts
new file mode 100644
index 000000000..d27517d22
--- /dev/null
+++ b/tests/src/rules/no-restricted-html-elements.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/no-restricted-html-elements"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run(
+ "no-restricted-html-elements",
+ rule as any,
+ loadTestCases("no-restricted-html-elements"),
+)