Headings headings headings!

General accessibility

Heading markup (e.g. native hN elements) is utilized by Assistive Technologies (AT) to construct a table of contents for a document. This provides an accessible method of in-page navigation.

AT will also be able to provide an appropriate alternate visual display that can be identified by heading markup. Screen readers use the headings to jump quickly around the page and understand its content.

You can quickly review a page’s heading hierarchy with the a11y-outline bookmarklet at https://xi.github.io/a11y-outline/ . Just drag the link that is on that page to your Bookmarks bar, and then click it to see a list of Headings, Landmarks, or Links.  Note that it doesn’t work with iframes, though, and learning unit content is in an iframe in the Learning MFE.

Nesting headings properly ensures that a screen reader can pick up on the hierarchy of the content and generate an ordered list for the user to browse.

Generally headings should be used for chunking related information. There are some occasions when we use headings for other purposes, but there are exceptions to the rule / usability compromises.  Start with the assumption that headings should be chunking hierarchical info, and check with the Accessibility team if you think there is a good exception.

When to use which hN tag

Native heading elements are NOT intended to be used as a way to style or resize text. See below section “hN tags as CSS classes” for Paragon classes that DO resize text to match heading levels.

Avoid skipping heading levels (h1 followed by h2, h2 followed by h3,...):

  • Levels should only be skipped when closing subsections (i.e. an h4 tag followed by an h2 tag to start a new section)

  • If a heading level is skipped, this could provide a negative experience to users as they may feel compelled to go back and look for the “missing” heading

  • An exception to this rule is headings for fixed content (e.g. sidebars) where the rankings should not change depending on the ranks of the content area. Consistency across pages is more important

There should ONE h1 per page/view and it should describe the overall purpose of the content.

  • If there is no h1 in a mockup, you can add one with className="sr-only" so only screen readers “see” it.

    • Example: <h1 className="sr-only">Hello world</h1>

    • Will be hidden from the view but AT will still pick up on it

  • This doesn’t have to be the first heading element in the document but it usually is (see this example)

  • The page title attribute (in the <head> element) should generally reflect or be a superset of the h1 content.  Example: <h1>Notes</h1> has <title>Notes | edx.org</title>

    • Change the <title> whenever the H1 changes.

    • If possible, also push a new route onto the Back stack when the h1 changes

Aria attributes


Defines the hierarchical level of an element within a structure; when combined with role="heading" will tell AT that the element is a heading. The “heading” role requires use of the aria-level attribute to indicate its position in the hierarchy.

  • role="heading" without an aria-level attribute will default to an h2, but you should include the aria-level attribute for clarity anyway.

  • This can even be done to hN tags if necessary

  • Example:

    • <div role="heading" aria-level="3">Hello World</div> is the same as using an h3

  • This also enables devs to level headings beyond what is provided natively (h1 - h6)

    • We could theoretically create an h7 this way using aria-level

All this being said, you should still prioritize using native hN elements over aria-level.

When this might be useful:

  • The main header for dialogs is, by convention, an h2. If there is a request to use a different tag as the top-level in the dialog, then add role="heading" and aria-level=N attributes to all headers as appropriate to start from level 2


Can be used to associate headings with their page region

  1. This can be useful when there is more than one of the same sectioning element on a page (e.g. <nav> <main> <header> <footer> <article>, full list here)

  2. The id of the hN tag will need match the value of the parent element’s aria-labelledby attribute, and must be unique to the page.

  3. See this example of aria-labelledby being used to differentiate two different <nav> elements

  4. You should generally use aria-labelledby to name all <section> and <nav> elements in this way when appropriate visible DOM elements are present (i.e. headings). If they're not present, then you can either use aria-labelledby with sr-only elements, or use aria-label (which takes a human-readable string, not a DOM ID)

  5. Dialogs (div role="dialog" or <dialog>`) need an accessible name, which generally comes from an aria-label attribute or aria-labelledby (pointing to the main header in the dialog).

hN tags as CSS classes

Paragon provides a CSS class for each hN element (.h1, .h2, .h3,...). These do not provide any information for screen readers or accessibility. They are intended to change font-size and styling.

Because of this, you may encounter instances such as an <h3> tag being styled with an .h4 class:

<h3 className="h4">Hello world</h3>

There is also a .heading-label class. Worth noting that this doesn’t effect accessibility (has no relation to aria-label) and is primarily for styling purposes.

See Paragon https://paragon-openedx.netlify.app/foundations/typography for more information on which properties are provided in each class and how they can be implemented.