diff --git a/docs/devupdate.md b/docs/devupdate.md
index 4b3df850c..33dd781f4 100644
--- a/docs/devupdate.md
+++ b/docs/devupdate.md
@@ -28,3 +28,21 @@ The `moodle_page` class now includes `set_supplementary_content()` and `get_supp
For instance, `mod_forum` uses this new mechanism to display a "Go to all discussions" link in the sticky footer when viewing an individual discussion.
For more information, see the [Adding supplementary content to the sticky footer](./apis/plugintypes/format/linear_navigation.md#adding-supplementary-content-to-the-sticky-footer) section.
+
+## Modal dialogue titles are now `
` elements
+
+
+
+The title rendered by the `core/modal` template is now an `` instead of an ``, so that opening a dialogue no longer breaks the page's heading hierarchy for assistive technology users. The title's appearance is unchanged, because its size is now set by the `fs-5` utility class rather than by the element.
+
+If your plugin renders headings inside dialogue content, they must be nested beneath this ``, so the first level available to you is ``. Headings that previously nested beneath the old `` will now skip levels. This includes content that is rendered into a dialogue without being authored as part of one -- for example, the Markdown headings in an activity module's `modulename_help` string, which core has lowered from `######` to `####` for this reason.
+
+If your plugin renders its own modal header markup, or overrides the `header` block of the `core/modal` template, apply the same `` pattern.
+
+:::note[Backported]
+
+This change has also been backported to Moodle 4.5, 5.1, and 5.2. On Moodle 4.5 the title carries the Bootstrap 4 `h5` utility class rather than `fs-5`.
+
+:::
+
+For more information, see [Heading structure](./guides/javascript/modal/index.md#heading-structure) in the Modal Dialogues guide.
diff --git a/docs/guides/javascript/modal/index.md b/docs/guides/javascript/modal/index.md
index b79eaf999..30ee7b4e4 100644
--- a/docs/guides/javascript/modal/index.md
+++ b/docs/guides/javascript/modal/index.md
@@ -218,6 +218,94 @@ export const init = async () => {
+## Heading structure {/* #heading-structure */}
+
+The `core/modal` template renders the dialogue title as a level 2 heading:
+
+```mustache title="The title element in core/modal"
+Test title
+```
+
+The level is deliberately fixed at ``. A dialogue can be opened from anywhere in a page, so it cannot know what the surrounding heading structure is, and the page behind it already provides the single ``. Placing the title at level 2 keeps it directly beneath that `` wherever the dialogue is opened from.
+
+Its appearance comes from the `fs-5` [Bootstrap font size utility class](https://getbootstrap.com/docs/5.3/utilities/text/#font-size), not from the element. Never change the heading element to make a title look bigger or smaller -- change the utility class.
+
+:::info[Why `` and not ``?]
+
+[Bootstrap's own documentation](https://getbootstrap.com/docs/5.3/components/modal/) uses an `` for the modal title, on the basis that a dialogue is its own document context. Moodle uses an `` instead because the page behind the dialogue already has an ``, and accessibility auditing tools commonly report a second `` as a failure.
+
+:::
+
+### Headings within a dialogue {/* #headings-within-a-dialogue */}
+
+Because the title is an ``, the first heading level available to you inside the body or footer of a dialogue is ``:
+
+```mustache title="mod/example/templates/my_modal.mustache"
+{{< core/modal }}
+ {{$title}}{{#str}} pluginname, mod_example {{/str}}{{/title}}
+ {{$body}}
+ {{#str}} settings, mod_example {{/str}}
+ {{! ... }}
+ {{#str}} advancedsettings, mod_example {{/str}}
+ {{! ... }}
+ {{/body}}
+{{/ core/modal }}
+```
+
+Skipping a level -- going straight from the `` title to an ``, for example -- breaks the heading hierarchy that assistive technology users rely on to navigate the dialogue. See the [heading requirements](/general/development/process/peer-review/accessibility-checklist#page-headers-and-title) in the accessibility peer review checklist.
+
+This applies to content that is rendered *into* a dialogue as much as to the dialogue's own template. If a language string, filter, or renderer emits headings, and that output can be displayed in a dialogue, its heading levels must fit beneath the `` title too. The `modulename_help` strings shown by the activity chooser are one example: their Markdown headings start at `####` (``) so that they nest correctly under the activity name.
+
+:::tip[Sizing nested headings]
+
+If a nested heading needs to look smaller than its level implies, apply an `fs-*` utility class, or set the size in your theme's SCSS. Choose the heading element for its meaning and the class for its appearance.
+
+:::
+
+### Overriding the header {/* #overriding-the-header */}
+
+The `core/modal` template exposes a `header` block, which replaces the whole `modal-header` region including the title element. If you override it, you must render your own heading and it must keep the same level, id, and `modal-title` class, otherwise the dialogue loses the accessible name that `aria-labelledby` points at:
+
+```mustache title="Overriding the header block"
+{{< core/modal }}
+ {{$header}}
+
+ {{#str}} pluginname, mod_example {{/str}}
+
+ {{! Any additional header content. }}
+ {{/header}}
+{{/ core/modal }}
+```
+
+In most cases you should override the `title` block instead, and leave the heading itself to `core/modal`.
+
+:::note[Dialogues that are not built with `core/modal`]
+
+Some dialogue-like components render their own `modal-header` markup rather than extending `core/modal` -- `tool_usertours` tour steps are one example in core. These follow the same rule: the title is an ``, and content headings start at ``.
+
+:::
+
+### Testing the heading structure {/* #testing-the-heading-structure */}
+
+The `best-practice` axe ruleset checks heading order, so an `@accessibility` Behat scenario is the simplest way to guard the structure of a dialogue:
+
+```gherkin
+@accessibility
+Scenario: The example dialogue has a valid heading structure
+ Given I open the example dialogue
+ Then the "Example dialogue" "dialogue" should meet accessibility standards with "best-practice" extra tests
+```
+
+Assert on the semantics rather than the presentation. `"h2.modal-title" "css_element"` is a stable assertion; including the `fs-5` utility class in the selector is not, because the class that sets the title's size may change.
+
+See [Accessibility testing](/general/development/policies/accessibility/testing) for more on writing accessibility tests.
+
+:::note[Earlier releases]
+
+Before [MDL-75699](https://tracker.moodle.org/browse/MDL-75699) the dialogue title was an ``, and headings inside a dialogue were expected to nest beneath that. If you are writing code that must also run on releases from before that fix, be aware that the same markup produces a different heading hierarchy there.
+
+:::
+
## Creating a custom modal type {/* #creating-a-custom-modal-type */}
In some situations it is desirable to write a brand new modal.
diff --git a/versioned_docs/version-4.5/guides/javascript/modal/index.md b/versioned_docs/version-4.5/guides/javascript/modal/index.md
index b79eaf999..4ad4221d0 100644
--- a/versioned_docs/version-4.5/guides/javascript/modal/index.md
+++ b/versioned_docs/version-4.5/guides/javascript/modal/index.md
@@ -218,6 +218,94 @@ export const init = async () => {
+## Heading structure {/* #heading-structure */}
+
+The `core/modal` template renders the dialogue title as a level 2 heading:
+
+```mustache title="The title element in core/modal"
+Test title
+```
+
+The level is deliberately fixed at ``. A dialogue can be opened from anywhere in a page, so it cannot know what the surrounding heading structure is, and the page behind it already provides the single ``. Placing the title at level 2 keeps it directly beneath that `` wherever the dialogue is opened from.
+
+Its appearance comes from the `h5` [Bootstrap typography utility class](https://getbootstrap.com/docs/4.6/content/typography/), not from the element. Never change the heading element to make a title look bigger or smaller -- change the utility class.
+
+:::info[Why `` and not ``?]
+
+[Bootstrap 4's own example](https://getbootstrap.com/docs/4.6/components/modal/) uses an `` for the modal title, which is where Moodle's original markup came from -- it picks the element for its size rather than for its place in the document. Bootstrap 5 corrected this to an ``, on the basis that a dialogue is its own document context. Moodle uses an `` instead, because the page behind the dialogue already has an ``, and accessibility auditing tools commonly report a second `` as a failure.
+
+:::
+
+### Headings within a dialogue {/* #headings-within-a-dialogue */}
+
+Because the title is an ``, the first heading level available to you inside the body or footer of a dialogue is ``:
+
+```mustache title="mod/example/templates/my_modal.mustache"
+{{< core/modal }}
+ {{$title}}{{#str}} pluginname, mod_example {{/str}}{{/title}}
+ {{$body}}
+ {{#str}} settings, mod_example {{/str}}
+ {{! ... }}
+ {{#str}} advancedsettings, mod_example {{/str}}
+ {{! ... }}
+ {{/body}}
+{{/ core/modal }}
+```
+
+Skipping a level -- going straight from the `` title to an ``, for example -- breaks the heading hierarchy that assistive technology users rely on to navigate the dialogue. See the [heading requirements](/general/development/process/peer-review/accessibility-checklist#page-headers-and-title) in the accessibility peer review checklist.
+
+This applies to content that is rendered *into* a dialogue as much as to the dialogue's own template. If a language string, filter, or renderer emits headings, and that output can be displayed in a dialogue, its heading levels must fit beneath the `` title too.
+
+:::tip[Sizing nested headings]
+
+If a nested heading needs to look smaller than its level implies, apply a heading utility class such as `h6`, or set the size in your theme's SCSS. Choose the heading element for its meaning and the class for its appearance.
+
+:::
+
+### Overriding the header {/* #overriding-the-header */}
+
+The `core/modal` template exposes a `header` block, which replaces the whole `modal-header` region including the title element. If you override it, you must render your own heading and it must keep the same level, id, and `modal-title` class, otherwise the dialogue loses the accessible name that `aria-labelledby` points at:
+
+```mustache title="Overriding the header block"
+{{< core/modal }}
+ {{$header}}
+
+ {{#str}} pluginname, mod_example {{/str}}
+
+ {{! Any additional header content. }}
+ {{/header}}
+{{/ core/modal }}
+```
+
+In most cases you should override the `title` block instead, and leave the heading itself to `core/modal`.
+
+:::note[Dialogues that are not built with `core/modal`]
+
+Some dialogue-like components render their own `modal-header` markup rather than extending `core/modal` -- `tool_usertours` tour steps are one example in core. These follow the same rule: the title is an ``, and content headings start at ``.
+
+:::
+
+### Testing the heading structure {/* #testing-the-heading-structure */}
+
+The `best-practice` axe ruleset checks heading order, so an `@accessibility` Behat scenario is the simplest way to guard the structure of a dialogue:
+
+```gherkin
+@accessibility
+Scenario: The example dialogue has a valid heading structure
+ Given I open the example dialogue
+ Then the "Example dialogue" "dialogue" should meet accessibility standards with "best-practice" extra tests
+```
+
+Assert on the semantics rather than the presentation. `"h2.modal-title" "css_element"` is a stable assertion; including the `h5` utility class in the selector is not, because the class that sets the title's size may change.
+
+See [Accessibility testing](/general/development/policies/accessibility/testing) for more on writing accessibility tests.
+
+:::note[Earlier releases]
+
+Before [MDL-75699](https://tracker.moodle.org/browse/MDL-75699) the dialogue title was an ``, and headings inside a dialogue were expected to nest beneath that. If you are writing code that must also run on releases from before that fix, be aware that the same markup produces a different heading hierarchy there.
+
+:::
+
## Creating a custom modal type {/* #creating-a-custom-modal-type */}
In some situations it is desirable to write a brand new modal.
diff --git a/versioned_docs/version-5.1/guides/javascript/modal/index.md b/versioned_docs/version-5.1/guides/javascript/modal/index.md
index b79eaf999..433f4a1e6 100644
--- a/versioned_docs/version-5.1/guides/javascript/modal/index.md
+++ b/versioned_docs/version-5.1/guides/javascript/modal/index.md
@@ -218,6 +218,94 @@ export const init = async () => {
+## Heading structure {/* #heading-structure */}
+
+The `core/modal` template renders the dialogue title as a level 2 heading:
+
+```mustache title="The title element in core/modal"
+Test title
+```
+
+The level is deliberately fixed at ``. A dialogue can be opened from anywhere in a page, so it cannot know what the surrounding heading structure is, and the page behind it already provides the single ``. Placing the title at level 2 keeps it directly beneath that `` wherever the dialogue is opened from.
+
+Its appearance comes from the `fs-5` [Bootstrap font size utility class](https://getbootstrap.com/docs/5.3/utilities/text/#font-size), not from the element. Never change the heading element to make a title look bigger or smaller -- change the utility class.
+
+:::info[Why `` and not ``?]
+
+[Bootstrap's own documentation](https://getbootstrap.com/docs/5.3/components/modal/) uses an `` for the modal title, on the basis that a dialogue is its own document context. Moodle uses an `` instead because the page behind the dialogue already has an ``, and accessibility auditing tools commonly report a second `` as a failure.
+
+:::
+
+### Headings within a dialogue {/* #headings-within-a-dialogue */}
+
+Because the title is an ``, the first heading level available to you inside the body or footer of a dialogue is ``:
+
+```mustache title="mod/example/templates/my_modal.mustache"
+{{< core/modal }}
+ {{$title}}{{#str}} pluginname, mod_example {{/str}}{{/title}}
+ {{$body}}
+ {{#str}} settings, mod_example {{/str}}
+ {{! ... }}
+ {{#str}} advancedsettings, mod_example {{/str}}
+ {{! ... }}
+ {{/body}}
+{{/ core/modal }}
+```
+
+Skipping a level -- going straight from the `` title to an ``, for example -- breaks the heading hierarchy that assistive technology users rely on to navigate the dialogue. See the [heading requirements](/general/development/process/peer-review/accessibility-checklist#page-headers-and-title) in the accessibility peer review checklist.
+
+This applies to content that is rendered *into* a dialogue as much as to the dialogue's own template. If a language string, filter, or renderer emits headings, and that output can be displayed in a dialogue, its heading levels must fit beneath the `` title too.
+
+:::tip[Sizing nested headings]
+
+If a nested heading needs to look smaller than its level implies, apply an `fs-*` utility class, or set the size in your theme's SCSS. Choose the heading element for its meaning and the class for its appearance.
+
+:::
+
+### Overriding the header {/* #overriding-the-header */}
+
+The `core/modal` template exposes a `header` block, which replaces the whole `modal-header` region including the title element. If you override it, you must render your own heading and it must keep the same level, id, and `modal-title` class, otherwise the dialogue loses the accessible name that `aria-labelledby` points at:
+
+```mustache title="Overriding the header block"
+{{< core/modal }}
+ {{$header}}
+
+ {{#str}} pluginname, mod_example {{/str}}
+
+ {{! Any additional header content. }}
+ {{/header}}
+{{/ core/modal }}
+```
+
+In most cases you should override the `title` block instead, and leave the heading itself to `core/modal`.
+
+:::note[Dialogues that are not built with `core/modal`]
+
+Some dialogue-like components render their own `modal-header` markup rather than extending `core/modal` -- `tool_usertours` tour steps are one example in core. These follow the same rule: the title is an ``, and content headings start at ``.
+
+:::
+
+### Testing the heading structure {/* #testing-the-heading-structure */}
+
+The `best-practice` axe ruleset checks heading order, so an `@accessibility` Behat scenario is the simplest way to guard the structure of a dialogue:
+
+```gherkin
+@accessibility
+Scenario: The example dialogue has a valid heading structure
+ Given I open the example dialogue
+ Then the "Example dialogue" "dialogue" should meet accessibility standards with "best-practice" extra tests
+```
+
+Assert on the semantics rather than the presentation. `"h2.modal-title" "css_element"` is a stable assertion; including the `fs-5` utility class in the selector is not, because the class that sets the title's size may change.
+
+See [Accessibility testing](/general/development/policies/accessibility/testing) for more on writing accessibility tests.
+
+:::note[Earlier releases]
+
+Before [MDL-75699](https://tracker.moodle.org/browse/MDL-75699) the dialogue title was an ``, and headings inside a dialogue were expected to nest beneath that. If you are writing code that must also run on releases from before that fix, be aware that the same markup produces a different heading hierarchy there.
+
+:::
+
## Creating a custom modal type {/* #creating-a-custom-modal-type */}
In some situations it is desirable to write a brand new modal.
diff --git a/versioned_docs/version-5.2/guides/javascript/modal/index.md b/versioned_docs/version-5.2/guides/javascript/modal/index.md
index b79eaf999..30ee7b4e4 100644
--- a/versioned_docs/version-5.2/guides/javascript/modal/index.md
+++ b/versioned_docs/version-5.2/guides/javascript/modal/index.md
@@ -218,6 +218,94 @@ export const init = async () => {
+## Heading structure {/* #heading-structure */}
+
+The `core/modal` template renders the dialogue title as a level 2 heading:
+
+```mustache title="The title element in core/modal"
+Test title
+```
+
+The level is deliberately fixed at ``. A dialogue can be opened from anywhere in a page, so it cannot know what the surrounding heading structure is, and the page behind it already provides the single ``. Placing the title at level 2 keeps it directly beneath that `` wherever the dialogue is opened from.
+
+Its appearance comes from the `fs-5` [Bootstrap font size utility class](https://getbootstrap.com/docs/5.3/utilities/text/#font-size), not from the element. Never change the heading element to make a title look bigger or smaller -- change the utility class.
+
+:::info[Why `` and not ``?]
+
+[Bootstrap's own documentation](https://getbootstrap.com/docs/5.3/components/modal/) uses an `` for the modal title, on the basis that a dialogue is its own document context. Moodle uses an `` instead because the page behind the dialogue already has an ``, and accessibility auditing tools commonly report a second `` as a failure.
+
+:::
+
+### Headings within a dialogue {/* #headings-within-a-dialogue */}
+
+Because the title is an ``, the first heading level available to you inside the body or footer of a dialogue is ``:
+
+```mustache title="mod/example/templates/my_modal.mustache"
+{{< core/modal }}
+ {{$title}}{{#str}} pluginname, mod_example {{/str}}{{/title}}
+ {{$body}}
+ {{#str}} settings, mod_example {{/str}}
+ {{! ... }}
+ {{#str}} advancedsettings, mod_example {{/str}}
+ {{! ... }}
+ {{/body}}
+{{/ core/modal }}
+```
+
+Skipping a level -- going straight from the `` title to an ``, for example -- breaks the heading hierarchy that assistive technology users rely on to navigate the dialogue. See the [heading requirements](/general/development/process/peer-review/accessibility-checklist#page-headers-and-title) in the accessibility peer review checklist.
+
+This applies to content that is rendered *into* a dialogue as much as to the dialogue's own template. If a language string, filter, or renderer emits headings, and that output can be displayed in a dialogue, its heading levels must fit beneath the `` title too. The `modulename_help` strings shown by the activity chooser are one example: their Markdown headings start at `####` (``) so that they nest correctly under the activity name.
+
+:::tip[Sizing nested headings]
+
+If a nested heading needs to look smaller than its level implies, apply an `fs-*` utility class, or set the size in your theme's SCSS. Choose the heading element for its meaning and the class for its appearance.
+
+:::
+
+### Overriding the header {/* #overriding-the-header */}
+
+The `core/modal` template exposes a `header` block, which replaces the whole `modal-header` region including the title element. If you override it, you must render your own heading and it must keep the same level, id, and `modal-title` class, otherwise the dialogue loses the accessible name that `aria-labelledby` points at:
+
+```mustache title="Overriding the header block"
+{{< core/modal }}
+ {{$header}}
+
+ {{#str}} pluginname, mod_example {{/str}}
+
+ {{! Any additional header content. }}
+ {{/header}}
+{{/ core/modal }}
+```
+
+In most cases you should override the `title` block instead, and leave the heading itself to `core/modal`.
+
+:::note[Dialogues that are not built with `core/modal`]
+
+Some dialogue-like components render their own `modal-header` markup rather than extending `core/modal` -- `tool_usertours` tour steps are one example in core. These follow the same rule: the title is an ``, and content headings start at ``.
+
+:::
+
+### Testing the heading structure {/* #testing-the-heading-structure */}
+
+The `best-practice` axe ruleset checks heading order, so an `@accessibility` Behat scenario is the simplest way to guard the structure of a dialogue:
+
+```gherkin
+@accessibility
+Scenario: The example dialogue has a valid heading structure
+ Given I open the example dialogue
+ Then the "Example dialogue" "dialogue" should meet accessibility standards with "best-practice" extra tests
+```
+
+Assert on the semantics rather than the presentation. `"h2.modal-title" "css_element"` is a stable assertion; including the `fs-5` utility class in the selector is not, because the class that sets the title's size may change.
+
+See [Accessibility testing](/general/development/policies/accessibility/testing) for more on writing accessibility tests.
+
+:::note[Earlier releases]
+
+Before [MDL-75699](https://tracker.moodle.org/browse/MDL-75699) the dialogue title was an ``, and headings inside a dialogue were expected to nest beneath that. If you are writing code that must also run on releases from before that fix, be aware that the same markup produces a different heading hierarchy there.
+
+:::
+
## Creating a custom modal type {/* #creating-a-custom-modal-type */}
In some situations it is desirable to write a brand new modal.