DS2 top nav

No notes defined.

{#
  Structure: header > top row (logo, search form, search icon, menu button) + nav with nested lists.
  An item with children is a <button> followed by its panel, which holds a back button and the child list.
  idPrefix keeps the search form's and the nav's ids unique, so two headers can share a page.
  The panels have no ids here: the JS pairs each button with the panel that follows it and adds the
  ids, aria-controls and aria-expanded itself, so there is nothing to number in the markup.
  Styles: _ds2-top-nav.scss. Behaviour: ds2-top-nav.module.js. Aiming at WCAG 2.2 AA.
#}
{% set idPrefix = idPrefix or 'ds2-top-nav' %}
{# only written when hover is turned off, so leaving the option out keeps hover on #}
<header class="ds2-top-nav">
  <div class="ds2-top-nav__bar">

    {# crest and wordmark are separate svgs so the wordmark can be hidden on smaller screens #}
    {# the svgs are decorative and hidden, the link is named by the text below them (WCAG 1.1.1, 2.4.4) #}
    <a class="ds2-top-nav__logo" href="{{ logo.url }}">
      <svg class="ds2-top-nav__logo-crest" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261 261.2" aria-hidden="true" focusable="false">
        <polygon points="142,168.4 146.6,168.4 146.6,126.8 156.8,126.8 156.8,119.2 142,119.2"></polygon>
        <polygon points="162.4,168.4 167,168.4 167,126.8 177.2,126.8 177.2,119.2 162.4,119.2"></polygon>
        <polygon points="169,83.4 173.2,83.4 173.2,79.6 166.4,79.6 166.4,91 169,91"></polygon>
        <polygon points="157,83.4 161.2,83.4 161.2,79.6 154.4,79.6 154.4,91 157,91"></polygon>
        <polygon points="182.8,168.4 187.4,168.4 187.4,126.8 197.6,126.8 197.6,119.2 182.8,119.2"></polygon>
        <path d="M184.2,206.3c0-8.4-6.6-14.2-14.4-14.2s-14.4,5.8-14.4,14.2c0,8.4,6.6,14.2,14.4,14.2
          S184.2,214.7,184.2,206.3z M160.8,206.3c0-5.4,4.2-9,9-9s9,3.6,9,9c0,5.4-4.2,9-9,9S160.8,211.7,160.8,206.3z"></path>
        <polygon points="137.2,112.2 202.4,112.2 205.2,108 134.4,108"></polygon>
        <path d="M0,0.1v261h114.2V192H142v69.1h4.6v-77.7H193v77.7h4.6V192h27.8v69.1H261V0.1H0z M169.6,30.1h0.4l26.8,16.4
          h-54L169.6,30.1z M142.4,49.7h54.8l2.8,19.8h-60.4L142.4,49.7z M219.6,187.4h-22v-8.6H142v8.6h-22v-19.2h3.8v-62.8h6V72.8h80v32.6
          h6v62.8h3.8V187.4z"></path>
        <polygon points="181,83.4 185.2,83.4 185.2,79.6 178.4,79.6 178.4,91 181,91"></polygon>
      </svg>
      {# viewBox starts at the crest's right edge so the original gap is kept #}
      <svg class="ds2-top-nav__logo-wordmark" xmlns="http://www.w3.org/2000/svg" viewBox="261 0 1662 261.2" aria-hidden="true" focusable="false">
        <path d="M1851.2,174c7.6,5,18.4,9.8,33.4,9.8c22,0,38.4-12.2,38.4-31.6c0-33-45.2-30.6-45.2-48.6
          c0-6.2,6.2-9.6,13.8-9.6c8.8,0,16.2,3,25,7.8l0.4-19c-6-3.6-15.4-6.4-26.2-6.4c-23.2,0-35.6,14-35.6,30c0,8,2.4,14,6.4,18.6
          c13,14.4,38.4,16.8,38.4,30.6c0,8-8.6,10.6-16.4,10.6c-9.4,0-17.4-3.8-24-8L1851.2,174z M1769.6,164.2V95.8h13.4
          c21.4,0,34.6,13.2,34.6,34.2c0,20.6-13.2,34.2-34.6,34.2H1769.6z M1748,182.4h34.6c34,0,57.6-21.6,57.6-53
          c0-30.8-21.6-51.6-57.2-51.6h-35V182.4z M1666.4,182.4h60.6v-18h-39V138h33.6v-18.2H1688V95.6h38.8V77.8h-60.4V182.4z
          M1584.8,182.4h60.6v-18h-39V138h33.4v-18.2h-33.4V95.6h38.6V77.8h-60.2V182.4z M1509.4,182.4h58.4v-18h-36.6V77.8h-21.8V182.4z
          M1401,182.4h21.8v-44h32.4v-18h-32.4V95.6h38V77.8H1401V182.4z M1292.6,130c0-21.6,14.2-35.8,33-35.8s33.2,14.2,33.2,35.8
          c0,21.8-14.4,35.8-33.2,35.8S1292.6,151.8,1292.6,130 M1269.8,130c0,31,24,53.8,55.8,53.8c32,0,55.8-22.8,55.8-53.8
          s-23.8-53.8-55.8-53.8C1293.8,76.2,1269.8,99,1269.8,130 M1142.8,77.8l36.4,63.2v41.4h21.8v-41.2l36-63.4h-22.6l-16,29.4
          c-2.4,4.4-5.4,10-7.6,14.8h-0.2c-2.4-4.8-5.2-10.6-7.8-15l-16-29.2H1142.8z M1047.8,95.6h31.4v86.8h21.6V95.6h31.4V77.8h-84.4V95.6
          z M1007.6,182.4h21.6V77.8h-21.6V182.4z M915.2,174c7.4,5,18.2,9.8,33.2,9.8c22.4,0,38.4-11.6,38.4-31.6c0-32.4-45.2-31-45.2-48.6
          c0-6.2,6.2-9.6,14-9.6c8.6,0,16,3,25,7.8l0.2-19c-5.8-3.6-15.2-6.4-26.2-6.4c-23.2,0-35.6,14-35.6,30c0,8,2.4,14,6.6,18.6
          c12.8,14.4,38.2,16.8,38.2,30.6c0,8-8.4,10.6-16.4,10.6c-9.4,0-17.2-3.8-24-8L915.2,174z M847.4,126V95.6h9.2
          c10.4,0,16.2,6.2,16.2,14.4c0,9-7,16-16.2,16H847.4z M825.6,182.4h21.8V142h8.2c3.6,1.4,10,13.2,26,40.4h24.8
          c-15.8-26.8-25.8-44.4-31.2-47.4v-0.2c9.6-4.4,19-13.4,19-27c0-16.2-10.2-30-38-30h-30.6V182.4z M744,182.4h60.8v-18h-39V138h33.4
          v-18.2h-33.4V95.6h38.6V77.8H744V182.4z M624,77.8l43.4,105.4h17.4l44.6-105.4h-22.2l-22.8,55.8c-2.8,6.6-5,13.2-7.2,19.4h-0.4
          c-2-6.2-4.4-12.6-7-19.4l-22-55.8H624z M587.6,182.4h21.6V77.8h-21.6V182.4z M465,182.4h21.4V125c0-4.8,0-11.2-0.4-14.6h0.4
          c3,4.6,6,8.6,10,13.8l45.4,58.2H562V77.8h-21.4V131c0,4.4,0,11.4,0.2,16.4h-0.2c-3-4.4-5.8-8.6-10-13.8l-44-55.8H465V182.4z
          M351.2,141.6c0,27.6,15.4,42.2,44.6,42.2c28,0,45.6-14.2,45.6-42.6V77.8h-21.8V141c0,16.2-8,24.6-23.4,24.6
          c-14.6,0-23.2-8.8-23.2-24.6V77.8h-21.8V141.6z"></path>
      </svg>
      <span class="ds2-top-nav__visually-hidden">{{ logo.label }}</span>
    </a>

    {# the arrow and the box travel together, so on desktop they share one flex box rather than
       sitting in separate grid columns. It's display: contents below the desktop breakpoint,
       so the bar's own flex layout is unchanged there #}
    <div class="ds2-top-nav__search-group">
    {# closes the search box. Only shown while the box is open, so it's the one visible way out on a touch screen #}
    {# the same arrow as the menu's back buttons, named for what it does rather than where it goes #}
    <button class="ds2-top-nav__search-close" type="button">
      <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
      <span class="ds2-top-nav__visually-hidden">{{ search.closeLabel }}</span>
    </button>

    {# a real form in a search landmark, so it's announced as search and Enter submits (WCAG 1.3.1) #}
    {# the input has a proper label, not just a placeholder, and the label starts with the visible word "Search" (3.3.2, 2.5.3) #}
    <form class="ds2-top-nav__search" id="{{ idPrefix }}-search" role="search" action="{{ search.action }}" method="get">
      <label class="ds2-top-nav__search-label ds2-top-nav__visually-hidden" for="{{ idPrefix }}-search-input">{{ search.label }}</label>
      <input class="ds2-top-nav__search-input" id="{{ idPrefix }}-search-input" name="{{ search.inputName }}" type="search" placeholder="{{ search.placeholder }}">
      {% for field in search.hiddenFields %}
        <input type="hidden" name="{{ field.name }}" value="{{ field.value }}">
      {% endfor %}
      <button class="ds2-top-nav__search-submit" type="submit">
        <svg class="ds2-top-nav__search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
          <path d="M20.67,18.67H19.61l-.37-.36a8.66,8.66,0,1,0-.93.93l.36.37v1.06l6.66,6.65,2-2Zm-8,0a6,6,0,1,1,6-6A6,6,0,0,1,12.67,18.67Z"></path>
        </svg>
        <span class="ds2-top-nav__visually-hidden">{{ search.submitLabel }}</span>
      </button>
    </form>
    </div>

    {# shows the search box and focuses the input, on every screen size. It never opens the menu #}
    <button class="ds2-top-nav__search-toggle" type="button" aria-controls="{{ idPrefix }}-search">
      <svg class="ds2-top-nav__search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
        <path d="M20.67,18.67H19.61l-.37-.36a8.66,8.66,0,1,0-.93.93l.36.37v1.06l6.66,6.65,2-2Zm-8,0a6,6,0,1,1,6-6A6,6,0,0,1,12.67,18.67Z"></path>
      </svg>
      <span class="ds2-top-nav__visually-hidden">{{ search.openLabel }}</span>
    </button>

    {# comes before the nav in the source so focus order matches the visual order on mobile and tablet (WCAG 2.4.3) #}
    {# the old component put the burger after the nav, so tabbing from it left the header entirely #}
    {# aria-expanded on this and the other disclosure buttons is added by the JS, so without JS nothing claims to be collapsed while its links are showing #}
    <button class="ds2-top-nav__menu-toggle" type="button" aria-controls="{{ idPrefix }}-menu">
      {# staggered lines taken from the mobile design reference, currentColor keeps it visible in forced colours #}
      <svg class="ds2-top-nav__menu-icon ds2-top-nav__menu-icon--burger" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 12" aria-hidden="true" focusable="false">
        <rect x="0" y="0" width="14" height="2"></rect>
        <rect x="9" y="5" width="15" height="2"></rect>
        <rect x="1" y="10" width="16" height="2"></rect>
      </svg>
      {# shown instead of the burger while the menu is open #}
      <svg class="ds2-top-nav__menu-icon ds2-top-nav__menu-icon--close" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
        <path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
      </svg>
      <span class="ds2-top-nav__visually-hidden">{{ menuLabel }}</span>
    </button>
  </div>

  {# a named navigation landmark, with the levels as nested lists so their structure is announced (WCAG 1.3.1) #}
  <nav class="ds2-top-nav__nav" id="{{ idPrefix }}-menu" aria-label="{{ navLabel }}">
    <ul class="ds2-top-nav__list ds2-top-nav__list--level-1">
      {% for level1Option in level1Options %}
        {% if level1Option.level2Options %}
          {# items with children are buttons, not links: they open a panel rather than going anywhere (WCAG 4.1.2, 2.1.1) #}
          {# the arrow is decorative, so it's hidden and the button is named by its text alone (1.1.1) #}
          <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
            <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
              <span class="ds2-top-nav__link-text">{{ level1Option.title }}</span>
              <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
            </button>

            {# the panel is the button's next sibling, and that's how the JS finds it. It gives the
               panel an id and the button an aria-controls on init, along with aria-expanded, so
               there are no ids to number here. Don't wrap either of them in anything #}
            <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">
              {# the back button shows only an arrow, so hidden text says where it goes (WCAG 1.1.1, 2.4.6) #}
              <button class="ds2-top-nav__back" type="button">
                <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
              </button>
              <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">
                {% for level2Option in level1Option.level2Options %}
                  {% if level2Option.level3Options %}
                    <li class="ds2-top-nav__item ds2-top-nav__item--level-2 ds2-top-nav__item--has-children">
                      <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                        <span class="ds2-top-nav__link-text">{{ level2Option.title }}</span>
                        <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                      </button>

                      <div class="ds2-top-nav__panel ds2-top-nav__panel--level-3">
                        {# named after its destination, e.g. "Back to Study", so it makes sense out of context (WCAG 2.4.6) #}
                        <button class="ds2-top-nav__back" type="button">
                          <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                          <span class="ds2-top-nav__visually-hidden">Back to {{ level1Option.title }}</span>
                        </button>
                        <ul class="ds2-top-nav__list ds2-top-nav__list--level-3">
                          {% for level3Option in level2Option.level3Options %}
                            <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                              <a class="ds2-top-nav__link" href="{{ level3Option.url }}">{{ level3Option.title }}</a>
                            </li>
                          {% endfor %}
                        </ul>
                      </div>
                    </li>
                  {% else %}
                    <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                      <a class="ds2-top-nav__link" href="{{ level2Option.url }}">{{ level2Option.title }}</a>
                    </li>
                  {% endif %}
                {% endfor %}
              </ul>
            </div>
          </li>
        {% else %}
          <li class="ds2-top-nav__item ds2-top-nav__item--level-1">
            <a class="ds2-top-nav__link" href="{{ level1Option.url }}">{{ level1Option.title }}</a>
          </li>
        {% endif %}
      {% endfor %}
    </ul>
  </nav>
</header>
<header class="ds2-top-nav">
    <div class="ds2-top-nav__bar">

        <a class="ds2-top-nav__logo" href="https://www.leeds.ac.uk/">
            <svg class="ds2-top-nav__logo-crest" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261 261.2" aria-hidden="true" focusable="false">
                <polygon points="142,168.4 146.6,168.4 146.6,126.8 156.8,126.8 156.8,119.2 142,119.2"></polygon>
                <polygon points="162.4,168.4 167,168.4 167,126.8 177.2,126.8 177.2,119.2 162.4,119.2"></polygon>
                <polygon points="169,83.4 173.2,83.4 173.2,79.6 166.4,79.6 166.4,91 169,91"></polygon>
                <polygon points="157,83.4 161.2,83.4 161.2,79.6 154.4,79.6 154.4,91 157,91"></polygon>
                <polygon points="182.8,168.4 187.4,168.4 187.4,126.8 197.6,126.8 197.6,119.2 182.8,119.2"></polygon>
                <path d="M184.2,206.3c0-8.4-6.6-14.2-14.4-14.2s-14.4,5.8-14.4,14.2c0,8.4,6.6,14.2,14.4,14.2
          S184.2,214.7,184.2,206.3z M160.8,206.3c0-5.4,4.2-9,9-9s9,3.6,9,9c0,5.4-4.2,9-9,9S160.8,211.7,160.8,206.3z"></path>
                <polygon points="137.2,112.2 202.4,112.2 205.2,108 134.4,108"></polygon>
                <path d="M0,0.1v261h114.2V192H142v69.1h4.6v-77.7H193v77.7h4.6V192h27.8v69.1H261V0.1H0z M169.6,30.1h0.4l26.8,16.4
          h-54L169.6,30.1z M142.4,49.7h54.8l2.8,19.8h-60.4L142.4,49.7z M219.6,187.4h-22v-8.6H142v8.6h-22v-19.2h3.8v-62.8h6V72.8h80v32.6
          h6v62.8h3.8V187.4z"></path>
                <polygon points="181,83.4 185.2,83.4 185.2,79.6 178.4,79.6 178.4,91 181,91"></polygon>
            </svg>

            <svg class="ds2-top-nav__logo-wordmark" xmlns="http://www.w3.org/2000/svg" viewBox="261 0 1662 261.2" aria-hidden="true" focusable="false">
                <path d="M1851.2,174c7.6,5,18.4,9.8,33.4,9.8c22,0,38.4-12.2,38.4-31.6c0-33-45.2-30.6-45.2-48.6
          c0-6.2,6.2-9.6,13.8-9.6c8.8,0,16.2,3,25,7.8l0.4-19c-6-3.6-15.4-6.4-26.2-6.4c-23.2,0-35.6,14-35.6,30c0,8,2.4,14,6.4,18.6
          c13,14.4,38.4,16.8,38.4,30.6c0,8-8.6,10.6-16.4,10.6c-9.4,0-17.4-3.8-24-8L1851.2,174z M1769.6,164.2V95.8h13.4
          c21.4,0,34.6,13.2,34.6,34.2c0,20.6-13.2,34.2-34.6,34.2H1769.6z M1748,182.4h34.6c34,0,57.6-21.6,57.6-53
          c0-30.8-21.6-51.6-57.2-51.6h-35V182.4z M1666.4,182.4h60.6v-18h-39V138h33.6v-18.2H1688V95.6h38.8V77.8h-60.4V182.4z
          M1584.8,182.4h60.6v-18h-39V138h33.4v-18.2h-33.4V95.6h38.6V77.8h-60.2V182.4z M1509.4,182.4h58.4v-18h-36.6V77.8h-21.8V182.4z
          M1401,182.4h21.8v-44h32.4v-18h-32.4V95.6h38V77.8H1401V182.4z M1292.6,130c0-21.6,14.2-35.8,33-35.8s33.2,14.2,33.2,35.8
          c0,21.8-14.4,35.8-33.2,35.8S1292.6,151.8,1292.6,130 M1269.8,130c0,31,24,53.8,55.8,53.8c32,0,55.8-22.8,55.8-53.8
          s-23.8-53.8-55.8-53.8C1293.8,76.2,1269.8,99,1269.8,130 M1142.8,77.8l36.4,63.2v41.4h21.8v-41.2l36-63.4h-22.6l-16,29.4
          c-2.4,4.4-5.4,10-7.6,14.8h-0.2c-2.4-4.8-5.2-10.6-7.8-15l-16-29.2H1142.8z M1047.8,95.6h31.4v86.8h21.6V95.6h31.4V77.8h-84.4V95.6
          z M1007.6,182.4h21.6V77.8h-21.6V182.4z M915.2,174c7.4,5,18.2,9.8,33.2,9.8c22.4,0,38.4-11.6,38.4-31.6c0-32.4-45.2-31-45.2-48.6
          c0-6.2,6.2-9.6,14-9.6c8.6,0,16,3,25,7.8l0.2-19c-5.8-3.6-15.2-6.4-26.2-6.4c-23.2,0-35.6,14-35.6,30c0,8,2.4,14,6.6,18.6
          c12.8,14.4,38.2,16.8,38.2,30.6c0,8-8.4,10.6-16.4,10.6c-9.4,0-17.2-3.8-24-8L915.2,174z M847.4,126V95.6h9.2
          c10.4,0,16.2,6.2,16.2,14.4c0,9-7,16-16.2,16H847.4z M825.6,182.4h21.8V142h8.2c3.6,1.4,10,13.2,26,40.4h24.8
          c-15.8-26.8-25.8-44.4-31.2-47.4v-0.2c9.6-4.4,19-13.4,19-27c0-16.2-10.2-30-38-30h-30.6V182.4z M744,182.4h60.8v-18h-39V138h33.4
          v-18.2h-33.4V95.6h38.6V77.8H744V182.4z M624,77.8l43.4,105.4h17.4l44.6-105.4h-22.2l-22.8,55.8c-2.8,6.6-5,13.2-7.2,19.4h-0.4
          c-2-6.2-4.4-12.6-7-19.4l-22-55.8H624z M587.6,182.4h21.6V77.8h-21.6V182.4z M465,182.4h21.4V125c0-4.8,0-11.2-0.4-14.6h0.4
          c3,4.6,6,8.6,10,13.8l45.4,58.2H562V77.8h-21.4V131c0,4.4,0,11.4,0.2,16.4h-0.2c-3-4.4-5.8-8.6-10-13.8l-44-55.8H465V182.4z
          M351.2,141.6c0,27.6,15.4,42.2,44.6,42.2c28,0,45.6-14.2,45.6-42.6V77.8h-21.8V141c0,16.2-8,24.6-23.4,24.6
          c-14.6,0-23.2-8.8-23.2-24.6V77.8h-21.8V141.6z"></path>
            </svg>
            <span class="ds2-top-nav__visually-hidden">University of Leeds homepage</span>
        </a>

        <div class="ds2-top-nav__search-group">

            <button class="ds2-top-nav__search-close" type="button">
                <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                <span class="ds2-top-nav__visually-hidden">Close search</span>
            </button>

            <form class="ds2-top-nav__search" id="ds2-top-nav-search" role="search" action="https://www.leeds.ac.uk/search" method="get">
                <label class="ds2-top-nav__search-label ds2-top-nav__visually-hidden" for="ds2-top-nav-search-input">Search leeds.ac.uk</label>
                <input class="ds2-top-nav__search-input" id="ds2-top-nav-search-input" name="q" type="search" placeholder="Search">

                <input type="hidden" name="searchOption" value="searchSite">

                <button class="ds2-top-nav__search-submit" type="submit">
                    <svg class="ds2-top-nav__search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
                        <path d="M20.67,18.67H19.61l-.37-.36a8.66,8.66,0,1,0-.93.93l.36.37v1.06l6.66,6.65,2-2Zm-8,0a6,6,0,1,1,6-6A6,6,0,0,1,12.67,18.67Z"></path>
                    </svg>
                    <span class="ds2-top-nav__visually-hidden">Search</span>
                </button>
            </form>
        </div>

        <button class="ds2-top-nav__search-toggle" type="button" aria-controls="ds2-top-nav-search">
            <svg class="ds2-top-nav__search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
                <path d="M20.67,18.67H19.61l-.37-.36a8.66,8.66,0,1,0-.93.93l.36.37v1.06l6.66,6.65,2-2Zm-8,0a6,6,0,1,1,6-6A6,6,0,0,1,12.67,18.67Z"></path>
            </svg>
            <span class="ds2-top-nav__visually-hidden">Search</span>
        </button>

        <button class="ds2-top-nav__menu-toggle" type="button" aria-controls="ds2-top-nav-menu">

            <svg class="ds2-top-nav__menu-icon ds2-top-nav__menu-icon--burger" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 12" aria-hidden="true" focusable="false">
                <rect x="0" y="0" width="14" height="2"></rect>
                <rect x="9" y="5" width="15" height="2"></rect>
                <rect x="1" y="10" width="16" height="2"></rect>
            </svg>

            <svg class="ds2-top-nav__menu-icon ds2-top-nav__menu-icon--close" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
            </svg>
            <span class="ds2-top-nav__visually-hidden">Menu</span>
        </button>
    </div>

    <nav class="ds2-top-nav__nav" id="ds2-top-nav-menu" aria-label="Main">
        <ul class="ds2-top-nav__list ds2-top-nav__list--level-1">

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
                <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                    <span class="ds2-top-nav__link-text">Study</span>
                    <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                </button>

                <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">

                    <button class="ds2-top-nav__back" type="button">
                        <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                        <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
                    </button>
                    <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://courses.leeds.ac.uk/">Course search</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2 ds2-top-nav__item--has-children">
                            <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                                <span class="ds2-top-nav__link-text">Undergraduate</span>
                                <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                            </button>

                            <div class="ds2-top-nav__panel ds2-top-nav__panel--level-3">

                                <button class="ds2-top-nav__back" type="button">
                                    <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                                    <span class="ds2-top-nav__visually-hidden">Back to Study</span>
                                </button>
                                <ul class="ds2-top-nav__list ds2-top-nav__list--level-3">

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/undergraduate">Undergraduate home page</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/clearing">Clearing 2026</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://courses.leeds.ac.uk/course-search/undergraduate-courses">Undergraduate course search</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/undergraduate-fees/doc/fees-undergraduate-fees">Fees and funding</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/undergraduate-student-life/doc/students-union">Student life</a>
                                    </li>

                                </ul>
                            </div>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2 ds2-top-nav__item--has-children">
                            <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                                <span class="ds2-top-nav__link-text">Online courses</span>
                                <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                            </button>

                            <div class="ds2-top-nav__panel ds2-top-nav__panel--level-3">

                                <button class="ds2-top-nav__back" type="button">
                                    <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                                    <span class="ds2-top-nav__visually-hidden">Back to Study</span>
                                </button>
                                <ul class="ds2-top-nav__list ds2-top-nav__list--level-3">

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/online-courses">Overview</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/online-masters-study">Online Masters study</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/short-online-courses">Short online courses</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/online-courses/doc/phd-distance-learning">PhD by distance learning</a>
                                    </li>

                                </ul>
                            </div>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2 ds2-top-nav__item--has-children">
                            <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                                <span class="ds2-top-nav__link-text">Masters</span>
                                <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                            </button>

                            <div class="ds2-top-nav__panel ds2-top-nav__panel--level-3">

                                <button class="ds2-top-nav__back" type="button">
                                    <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                                    <span class="ds2-top-nav__visually-hidden">Back to Study</span>
                                </button>
                                <ul class="ds2-top-nav__list ds2-top-nav__list--level-3">

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/masters">Masters home page</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/postgraduate-events">Postgraduate open day</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://courses.leeds.ac.uk/course-search/masters-courses?type=PGT&amp;query=">Masters course search</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/masters-applying/doc/apply-masters-courses">Applying</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/masters-offer/doc/masters-fees-funding">Fees and funding</a>
                                    </li>

                                </ul>
                            </div>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-degrees">Research degrees</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/international-students">International students</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/foundation-courses">Foundation courses</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/mature-students">Mature students</a>
                        </li>

                    </ul>
                </div>
            </li>

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
                <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                    <span class="ds2-top-nav__link-text">Faculties</span>
                    <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                </button>

                <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">

                    <button class="ds2-top-nav__back" type="button">
                        <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                        <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
                    </button>
                    <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://ahc.leeds.ac.uk/">Arts, Humanities and Cultures</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://biologicalsciences.leeds.ac.uk/">Biological Sciences</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://business.leeds.ac.uk/">Business School</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://eps.leeds.ac.uk/">Engineering and Physical Sciences</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://environment.leeds.ac.uk/">Environment</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://medicinehealth.leeds.ac.uk/">Medicine and Health</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://essl.leeds.ac.uk/">Social Sciences</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/about/doc/faculties-contacts">Faculty contacts</a>
                        </li>

                    </ul>
                </div>
            </li>

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
                <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                    <span class="ds2-top-nav__link-text">Research</span>
                    <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                </button>

                <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">

                    <button class="ds2-top-nav__back" type="button">
                        <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                        <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
                    </button>
                    <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-and-innovation">Research overview</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-impact">Research excellence and impact</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-strategy-environment">Strategy and environment</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/researcher-staff-development">Researcher and staff development</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-public-engagement">Public engagement</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-and-innovation/doc/research-innovation-contacts">Research and innovation contacts</a>
                        </li>

                    </ul>
                </div>
            </li>

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
                <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                    <span class="ds2-top-nav__link-text">Business</span>
                    <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                </button>

                <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">

                    <button class="ds2-top-nav__back" type="button">
                        <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                        <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
                    </button>
                    <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships">Overview</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships-access-expertise/doc/work-with-academics">Access our expertise</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/training-development/doc/cpd-professional-learning-businesses-employers">Training and development</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships/doc/recruit-students">Recruit our students and graduates</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships/doc/use-facilities">Use our facilities</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships/doc/support-small-businesses">Support for small businesses</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships/doc/business-partners">Our business partners</a>
                        </li>

                    </ul>
                </div>
            </li>

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
                <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                    <span class="ds2-top-nav__link-text">About</span>
                    <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                </button>

                <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">

                    <button class="ds2-top-nav__back" type="button">
                        <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                        <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
                    </button>
                    <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/campusmap">Campus map</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/about/doc/about-history">Our history</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/our-faculties">Faculties</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/structure-governance/doc/governance">Governance</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/structure-governance/doc/reports-accounts-policies">Reports, accounts and policies</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/about/doc/strategies-values">Strategies and values</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://equality.leeds.ac.uk/">Equality</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/university-executive">Executive team</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/university-news">News</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/events/">Events</a>
                        </li>

                    </ul>
                </div>
            </li>

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1">
                <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/alumni">Alumni</a>
            </li>

        </ul>
    </nav>
</header>
  • Content:
    // Desktop layout: single row with JS-driven mega-menu dropdowns.
    @import 'ds2-top-nav-vars';
    
    .ds2-top-nav {
    
    @media (min-width: $desktopBreakpoint) {
      &--js {
        // Desktop bar uses a centred three-column grid.
        $contentInset: unquote("max(#{$desktopPaddingInline}, calc((100% - #{$desktopMaxWidth}) / 2))");
    
        // Desktop bar uses a three-column grid; search open pushes links toward the logo.
        display: grid;
        // Equal side columns keep the nav centred instead of skewing toward the search icon.
        grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
        column-gap: $desktopColumnGap;
        align-items: center;
        min-height: $desktopBarHeight;
        padding-inline: $contentInset;
    
        .ds2-top-nav__bar {
          display: contents;
        }
    
        .ds2-top-nav__logo {
          grid-column: 1;
          grid-row: 1;
          justify-self: start;
          margin: 0;
          padding: 0;
        }
    
        .ds2-top-nav__logo-crest {
          width: $desktopLogoHeight;
          height: $desktopLogoHeight;
        }
    
        .ds2-top-nav__logo-wordmark {
          height: $desktopLogoHeight;
        }
    
        .ds2-top-nav__menu-toggle {
          display: none;
        }
    
        // Search trigger expands into the search field.
        .ds2-top-nav__search-toggle {
          grid-column: 3;
          grid-row: 1;
          justify-self: end;
        }
    
        .ds2-top-nav__search {
          display: none;
          grid-column: 3;
          grid-row: 1;
          justify-self: end;
          width: 250px;
          max-width: none;
          height: 44px;
          margin: 0;
        }
    
        // Top-level nav sits in its own centred column.
        .ds2-top-nav__nav {
          display: block;
          position: static;
          grid-column: 2;
          grid-row: 1;
          justify-self: center;
          min-height: 0;
          padding: 0;
          background: none;
          overflow: visible;
        }
    
        .ds2-top-nav__list--level-1 {
          flex-direction: row;
          gap: 0;
          max-width: none;
          padding: 0;
    
          > .ds2-top-nav__item > .ds2-top-nav__link {
            width: auto;
            // Full bar height keeps the whole row as the hover target.
            min-height: $desktopBarHeight;
            padding: 0 $desktopLinkPaddingFluid;
    
            &[aria-expanded="true"] {
              color: $highlightColour;
            }
    
            // Arrow rotates with the open state.
            > .ds2-top-nav__arrow {
              transform: rotate(90deg);
              transition: transform 0.2s ease;
    
              @media (prefers-reduced-motion: reduce) {
                transition: none;
              }
            }
    
            &[aria-expanded="true"] > .ds2-top-nav__arrow {
              transform: rotate(-90deg);
            }
          }
        }
    
        // Desktop has no back buttons; level 2 and 3 sit side by side.
        .ds2-top-nav__back {
          display: none;
        }
    
        // Level 2 dropdown sits full width beneath the header bar.
        .ds2-top-nav__panel--level-2 {
          display: none;
          position: absolute;
          z-index: $menuZIndex;
          top: 100%;
          left: 0;
          right: 0;
          width: auto;
          height: auto;
          min-height: 0;
          overflow: visible;
          padding: $spacing-3 $contentInset $spacing-7;
          transform: none;
          visibility: visible;
          transition: none;
    
          // Keep the dropdown edge visible in forced colours.
          @media (forced-colors: active) {
            border-bottom: 1px solid CanvasText;
          }
        }
    
        // Level 3 panel sits beside the level 2 list.
        .ds2-top-nav__panel--level-3 {
          display: none;
          position: absolute;
          top: 0;
          left: 100%;
          width: 100%;
          height: auto;
          min-height: 0;
          overflow: visible;
          padding: 0;
          background: none;
          transform: none;
          visibility: visible;
          transition: none;
        }
    
        .ds2-top-nav__panel--open {
          display: block;
        }
    
        .ds2-top-nav__list--level-2 {
          position: relative;
          gap: 0;
          width: $desktopLevel2Width;
          max-width: none;
          padding: 0;
        }
    
        .ds2-top-nav__list--level-3 {
          gap: 0;
          max-width: none;
          padding: 0;
        }
    
        .ds2-top-nav__panel .ds2-top-nav__link {
          min-height: $desktopRowHeight;
          padding: 0;
    
          // The dropdown has enough room for the focus ring to sit outside the row.
          &:focus-visible {
            outline-offset: $focusOutlineOffset;
          }
    
          &[aria-expanded="true"] {
            color: $highlightColour;
          }
        }
    
        // Level 2 items with children grow an arrow when open or hovered.
        .ds2-top-nav__list--level-2 > .ds2-top-nav__item > .ds2-top-nav__link {
          padding-right: $spacing-5;
    
          .ds2-top-nav__link-text {
            flex: none;
          }
    
          > .ds2-top-nav__arrow {
            flex: 0 1 20px;
            color: $highlightColour;
            transition: flex-grow 0.4s ease;
    
            @media (prefers-reduced-motion: reduce) {
              transition: none;
            }
          }
    
          &[aria-expanded="true"] > .ds2-top-nav__arrow {
            flex-grow: 1;
          }
        }
    
      }
    
      // Desktop open search fills the space left by the hidden nav links.
      &--search-open,
      &--search-closing {
        // Search-open layout: logo plus one shared flex row for the arrow and field.
        grid-template-columns: auto minmax(0, 1fr);
    
        .ds2-top-nav__search-toggle {
          display: none;
        }
    
        // Desktop keeps the logo visible while nav links are hidden.
        .ds2-top-nav__logo {
          display: inline-flex;
        }
    
        // Hide nav links from the tab order when the search box expands.
        .ds2-top-nav__nav {
          display: none;
        }
    
        // Arrow and search box share one flex row so they open leftward together.
        .ds2-top-nav__search-group {
          display: flex;
          align-items: center;
          justify-content: flex-end;
          gap: $desktopColumnGap;
          grid-column: 2;
          grid-row: 1;
        }
    
        .ds2-top-nav__search-close {
          // It's a flex item here, not a grid one.
          grid-column: auto;
          justify-self: auto;
        }
    
        // Search field takes the space the links vacate, capped at its max width.
        .ds2-top-nav__search {
          display: flex;
          flex: 1;
          width: auto;
          max-width: $desktopSearchMaxWidth;
          justify-self: auto;
          margin-inline: 0;
        }
      }
    
      &--search-open .ds2-top-nav__search {
        animation: ds2-top-nav-desktop-search-grow $searchSlideDuration ease;
    
        @media (prefers-reduced-motion: reduce) {
          animation: none;
        }
      }
    
      &--search-closing .ds2-top-nav__search {
        animation: ds2-top-nav-desktop-search-shrink $searchSlideDuration ease forwards;
      }
    
      // Desktop search doesn't use the mobile cross-fade because the bar is display: contents.
      &--search-opening,
      &--search-closing {
        .ds2-top-nav__logo {
          position: static;
          inset: auto;
          margin-block: 0;
          animation: none;
          pointer-events: auto;
        }
      }
    
      // Closing state brings the search icon back into place and restores focus there.
      &--search-closing {
        .ds2-top-nav__search-toggle {
          display: inline-flex;
          grid-column: 2;
          grid-row: 1;
          justify-self: end;
        }
      }
    
      // Opening state hides the icon while the box expands.
      &--search-opening {
        .ds2-top-nav__search-toggle {
          display: none;
        }
      }
    }
    }
    
  • URL: /components/raw/ds2-top-nav/_ds2-top-nav-desktop.scss
  • Filesystem Path: src/library/ds2/02-components/ds2-top-nav/_ds2-top-nav-desktop.scss
  • Size: 7.1 KB
  • Content:
    // DS2 top nav variables
    $bgColour: $color-black;
    $fgColour: $color-white;
    $highlightColour: $color-brand-vivid-yellow;
    $searchBorderColour: $color-black--light;
    $desktopBreakpoint: 80rem;
    
    $barPaddingTop: 32px;
    $barPaddingBottom: 24px;
    $barPaddingInline: 24px;
    $logoPadding: 24px;
    $crestWidth: 43.49px;
    $crestHeight: 42.74px;
    $barHeight: $barPaddingTop + $logoPadding * 2 + $crestHeight + $barPaddingBottom;
    
    $iconButtonSize: 40px;
    $menuButtonSize: 48px;
    $searchIconSize: 20px;
    $closeIconSize: 26px;
    $burgerWidth: 24px;
    $burgerHeight: 12px;
    
    $searchHeight: 60px;
    $searchGapInline: 12px;
    $searchRadius: 4px;
    $linkFontSize: 1rem;
    $linkLineHeight: 1.5;
    $rowHeight: 60px;
    $rowGap: $spacing-1;
    $listPaddingBottom: $spacing-6;
    
    $childRowHeight: 56px;
    $backHeight: 60px;
    $slideDuration: 0.3s;
    $searchMaxWidth: 480px;
    $listMaxWidth: 480px;
    
    $searchSlideDuration: 0.4s;
    $searchSlideEndWidth: unquote("min(530px, calc(100vw - #{$barPaddingInline * 2 + $iconButtonSize + $searchGapInline * 2 + $menuButtonSize}))");
    
    // Arrows use borders so they stay visible in forced colours.
    $arrowWidth: 13px;
    // Arrow spacing is tuned to the design measurements.
    $arrowGap: $spacing-4;
    $arrowStroke: 2px;
    $arrowHeadSize: 8px;
    
    // Desktop values match the legacy DS2 header styling.
    $desktopBarHeight: 80px;
    $desktopMaxWidth: 1400px;
    $desktopPaddingInline: $spacing-4;
    $desktopLogoHeight: 34px;
    // Desktop nav spacing narrows toward 1280px so it still fits beside the open search box.
    $desktopLinkPaddingFluid: unquote("clamp(10px, calc(2.5vw - 20px), #{$spacing-4})");
    // Desktop spacing between logo, nav, and search.
    $desktopColumnGap: $spacing-4;
    $desktopRowHeight: 52px;
    $desktopLevel2Width: 33%;
    // Open desktop search fills the space left by the nav links.
    $desktopSearchMaxWidth: 800px;
    
    $focusOutlineWidth: 2px;
    $focusOutlineOffset: 2px;
    $menuZIndex: 100;
    $seamOverlap: 1px;
    $noJsIndent: 24px;
    
  • URL: /components/raw/ds2-top-nav/_ds2-top-nav-vars.scss
  • Filesystem Path: src/library/ds2/02-components/ds2-top-nav/_ds2-top-nav-vars.scss
  • Size: 1.9 KB
  • Content:
    @import 'ds2-top-nav-vars';
    
    .ds2-top-nav {
      position: relative;
      font-family: uolInter, Arial, Helvetica, sans-serif;
      background: $bgColour;
      color: $fgColour;
    
      // Ensure box sizing stays consistent across full-width rows.
      &,
      *,
      *::before,
      *::after {
        box-sizing: border-box;
      }
    
      // Single focus style for keyboard users; search input is excluded because the whole box gets the ring.
      :where(a, button):focus-visible {
        outline: $focusOutlineWidth solid $highlightColour;
        outline-offset: $focusOutlineOffset;
      }
    
      // top row
    
      &__bar {
        display: flex;
        align-items: center;
        // Keep the bar height stable while the logo fades out during search open.
        position: relative;
        min-height: $barHeight;
        padding: $barPaddingTop $barPaddingInline $barPaddingBottom;
      }
    
      &__logo {
        display: inline-flex;
        align-items: center;
        margin-right: auto;
        padding: $logoPadding;
        color: inherit;
        fill: currentColor;
      }
    
      &__logo-crest {
        width: $crestWidth;
        height: $crestHeight;
      }
    
      // Wordmark only appears on desktop; mobile uses the crest alone.
      &__logo-wordmark {
        display: none;
        height: $crestHeight;
    
        @media (min-width: $desktopBreakpoint) {
          display: block;
        }
      }
    
      // Shared button reset.
      &__search-toggle,
      &__search-close,
      &__menu-toggle,
      &__search-submit {
        align-items: center;
        justify-content: center;
        flex: none;
        width: $iconButtonSize;
        height: $iconButtonSize;
        padding: 0;
        border: 0;
        background: none;
        color: inherit;
        cursor: pointer;
    
        // hover colour for mouse users only, so it doesn't stick after a tap on a touch screen
        @media (hover: hover) {
          &:hover {
            color: $highlightColour;
          }
        }
      }
    
      &__menu-toggle {
        width: $menuButtonSize;
        height: $menuButtonSize;
      }
    
      // Only shown once JS is active.
      &__search-toggle,
      &__menu-toggle {
        display: none;
      }
    
      // Visible only while the search box is open; it lets touch users close it.
      &__search-close {
        display: none;
    
        .ds2-top-nav__back-arrow {
          // This arrow sits as the full button instead of after a label.
          margin: 0;
        }
      }
    
      &__search-icon {
        width: $searchIconSize;
        height: $searchIconSize;
        fill: currentColor;
      }
    
      &__menu-icon {
        fill: currentColor;
    
        &--burger {
          width: $burgerWidth;
          height: $burgerHeight;
        }
    
        &--close {
          display: none;
          width: $closeIconSize;
          height: $closeIconSize;
        }
      }
    
      // Icon state follows the button state so the visual and announced state stay aligned.
      &__menu-toggle[aria-expanded="true"] {
        .ds2-top-nav__menu-icon--burger {
          display: none;
        }
    
        .ds2-top-nav__menu-icon--close {
          display: block;
        }
      }
    
      // Desktop arrow and search box move together; elsewhere they sit directly in the bar.
      &__search-group {
        display: contents;
      }
    
      // Search box appears in the open menu or when JS is unavailable.
      &__search {
        display: flex;
        align-items: center;
        flex: 1;
        min-width: 0;
        max-width: $searchMaxWidth;
        height: $searchHeight;
        margin-inline: $searchGapInline;
        border: 1px solid $searchBorderColour;
        border-radius: $searchRadius;
    
        // Keep focus ring around the whole search box; fallback is still acceptable if :has() is unavailable.
        &:has(.ds2-top-nav__search-input:focus-visible) {
          outline: $focusOutlineWidth solid $highlightColour;
          outline-offset: $focusOutlineOffset;
        }
      }
    
      &__search-input {
        flex: 1;
        min-width: 0;
        height: 100%;
        padding: 0 $spacing-3 0 $spacing-1;
        border: 0;
        background: transparent;
        color: inherit;
        font: inherit;
        // Keep iOS from zooming in on focus; 1rem is 16px unless the user has chosen a larger size.
        font-size: $linkFontSize;
    
        &::placeholder {
          color: inherit;
          opacity: 1;
        }
    
        &:focus {
          outline: none;
        }
      }
    
      // Submit button is announced after the input but laid out on the left to match the design.
      &__search-submit {
        display: inline-flex;
        order: -1;
        margin-left: $spacing-2;
      }
    
      // Menu list.
      &__list {
        display: flex;
        flex-direction: column;
        gap: $rowGap;
        max-width: $listMaxWidth;
        margin: 0;
        padding: 0;
        list-style: none;
      }
    
      &__list--level-1 {
        padding-block: $spacing-7 $listPaddingBottom;
      }
    
      &__link {
        display: flex;
        align-items: center;
        width: 100%;
        // Use min-height so rows can expand without clipping the text or tap target.
        min-height: $rowHeight;
        padding: 0 36px;
        border: 0;
        background: none;
        color: inherit;
        font: inherit;
        font-size: $linkFontSize;
        line-height: $linkLineHeight;
        text-align: left;
        text-decoration: none;
        cursor: pointer;
    
        // The full-width row keeps the focus ring inside the hit area.
        &:focus-visible {
          outline-offset: -$focusOutlineWidth * 2;
        }
    
        @media (hover: hover) {
          &:hover {
            color: $highlightColour;
          }
        }
      }
    
      // Child panel rows are slightly shorter than top-level rows.
      &__panel &__link {
        min-height: $childRowHeight;
      }
    
      &__list--level-2,
      &__list--level-3 {
        padding-bottom: $listPaddingBottom;
      }
    
      // Back buttons are JS-only.
      &__back {
        display: none;
        align-items: center;
        height: $backHeight;
        padding: 0 $spacing-5;
        border: 0;
        background: none;
        color: $highlightColour;
        cursor: pointer;
    
        // Keep the focus ring inside the back button at the screen edge.
        &:focus-visible {
          outline-offset: -$focusOutlineWidth * 2;
        }
      }
    
      // Arrows use borders so they remain visible in high-contrast mode.
      &__arrow,
      &__back-arrow {
        position: relative;
        flex: none;
        width: $arrowWidth;
        border-top: $arrowStroke solid currentColor;
    
        // The arrow head is a rotated square with two borders.
        &::after {
          content: '';
          position: absolute;
          top: -($arrowStroke / 2) - ($arrowHeadSize / 2);
          right: $arrowStroke / 2;
          width: $arrowHeadSize;
          height: $arrowHeadSize;
          border-top: $arrowStroke solid currentColor;
          border-right: $arrowStroke solid currentColor;
          transform: rotate(45deg);
        }
      }
    
      &__arrow {
        margin-left: $arrowGap;
      }
    
      // Back arrow points left.
      &__back-arrow {
        transform: scaleX(-1);
      }
    
      // Without JS, show the full list flat and hide arrows to avoid misleading drill-down cues.
      &:not(.ds2-top-nav--js) {
        .ds2-top-nav__arrow {
          display: none;
        }
    
        .ds2-top-nav__list--level-2,
        .ds2-top-nav__list--level-3 {
          padding-left: $noJsIndent;
        }
      }
    
      // With JS enabled, menu and search stay hidden behind the icon buttons until opened.
      &--js {
        .ds2-top-nav__search-toggle,
        .ds2-top-nav__menu-toggle {
          display: inline-flex;
        }
    
        .ds2-top-nav__search,
        .ds2-top-nav__nav {
          display: none;
        }
    
        // Menu overlays the page below the header; a 1px overlap keeps the seam clean.
        .ds2-top-nav__nav {
          position: absolute;
          top: calc(100% - #{$seamOverlap});
          left: 0;
          right: 0;
          z-index: $menuZIndex;
          min-height: calc(100vh - #{$barHeight - $seamOverlap});
          min-height: calc(100dvh - #{$barHeight - $seamOverlap});
          padding-top: $seamOverlap;
          background: $bgColour;
          overflow-x: clip;
        }
    
        .ds2-top-nav__back {
          display: inline-flex;
        }
    
        // Panels slide in from the right for drill-down navigation.
        .ds2-top-nav__panel {
          position: absolute;
          // Keep arrow layers below the panel so they don't paint over it.
          z-index: 1;
          top: 0;
          left: 0;
          width: 100%;
          // Closed panels are clipped to the menu height so they don't add scroll space.
          height: 100%;
          overflow: hidden;
          padding-top: $spacing-5;
          background: $bgColour;
          transform: translateX(100%);
          // Hidden panels are removed from the focus and read-out order.
          visibility: hidden;
          transition: transform $slideDuration ease, visibility 0s linear $slideDuration;
        }
    
        .ds2-top-nav__panel--open {
          height: auto;
          min-height: 100%;
          overflow: visible;
          transform: none;
          visibility: visible;
          transition: transform $slideDuration ease;
        }
    
        // Reduced motion disables panel sliding.
        @media (prefers-reduced-motion: reduce) {
          .ds2-top-nav__panel,
          .ds2-top-nav__panel--open {
            transition: none;
          }
        }
      }
    
      // Mobile and tablet open menu state.
      &--menu-open {
        .ds2-top-nav__nav {
          display: block;
        }
      }
    
      // Search box belongs to the search control, not the menu; the burger stays visible while opened.
      &--search-open,
      &--search-closing {
    
        // The close arrow stays attached to the box and keeps the burger at the right edge.
        .ds2-top-nav__search-close {
          display: inline-flex;
          margin-left: auto;
        }
    
    
        .ds2-top-nav__search {
          display: flex;
          max-width: $searchSlideEndWidth;
          // Keep the arrow and box together while the field grows from the middle.
          margin-inline: $searchGapInline auto;
          overflow: hidden;
        }
      }
    
      // Search-open state removes the logo and icon from the tab order.
      &--search-open {
        .ds2-top-nav__logo,
        .ds2-top-nav__search-toggle {
          display: none;
        }
    
        .ds2-top-nav__search {
          animation: ds2-top-nav-search-grow $searchSlideDuration ease;
    
          @media (prefers-reduced-motion: reduce) {
            animation: none;
          }
        }
      }
    
      // Cross-fade logo and search icon during open/close transitions.
      &--search-opening,
      &--search-closing {
        .ds2-top-nav__logo,
        .ds2-top-nav__search-toggle {
          display: inline-flex;
          // Position the fades precisely so the icon and logo don't jump while switching states.
          position: absolute;
          top: $barPaddingTop;
          bottom: $barPaddingBottom;
          margin-block: auto;
          // they're on their way out or not properly back yet, so they take no clicks
          pointer-events: none;
        }
    
        .ds2-top-nav__logo {
          left: $barPaddingInline;
        }
    
        // Keep the search icon aligned with the burger edge.
        .ds2-top-nav__search-toggle {
          right: $barPaddingInline + $menuButtonSize;
        }
      }
    
      &--search-opening {
        .ds2-top-nav__logo,
        .ds2-top-nav__search-toggle {
          animation: ds2-top-nav-fade-out $searchSlideDuration ease forwards;
        }
    
        .ds2-top-nav__search-close {
          animation: ds2-top-nav-fade-in $searchSlideDuration ease;
        }
      }
    
      // Closing state hides interactive elements while the search box collapses.
      &--search-closing {
        .ds2-top-nav__logo,
        .ds2-top-nav__search-toggle {
          animation: ds2-top-nav-fade-in $searchSlideDuration ease;
        }
    
        .ds2-top-nav__search-close {
          pointer-events: none;
          animation: ds2-top-nav-fade-out $searchSlideDuration ease forwards;
        }
    
        .ds2-top-nav__search {
          pointer-events: none;
          // Use a separate shrink animation so it can restart cleanly.
          animation: ds2-top-nav-search-shrink $searchSlideDuration ease forwards;
        }
      }
    }
      @import 'ds2-top-nav-desktop';
    
      .ds2-top-nav {
      // Search box motion uses distinct keyframes so the animation can restart correctly.
      @keyframes ds2-top-nav-fade-out {
        from {
          opacity: 1;
        }
    
        to {
          opacity: 0;
        }
      }
    
      @keyframes ds2-top-nav-fade-in {
        from {
          opacity: 0;
        }
    
        to {
          opacity: 1;
        }
      }
    
      // Shrink animation is separate so it can restart cleanly.
      @keyframes ds2-top-nav-search-shrink {
        from {
          max-width: $searchSlideEndWidth;
          opacity: 1;
        }
    
        67% {
          opacity: 1;
        }
    
        to {
          max-width: $iconButtonSize;
          opacity: 0;
        }
      }
    
      // Search box grows from the centre of the bar with a brief fade-in.
      @keyframes ds2-top-nav-search-grow {
        from {
          max-width: $iconButtonSize;
          opacity: 0;
        }
    
        33% {
          opacity: 1;
        }
    
        to {
          max-width: $searchSlideEndWidth;
          opacity: 1;
        }
      }
    
      // Desktop search width animates directly; the open box grows by max-width.
      @keyframes ds2-top-nav-desktop-search-grow {
        from {
          max-width: $iconButtonSize;
        }
    
        to {
          max-width: $desktopSearchMaxWidth;
        }
      }
    
      @keyframes ds2-top-nav-desktop-search-shrink {
        from {
          max-width: $desktopSearchMaxWidth;
        }
    
        to {
          max-width: $iconButtonSize;
        }
      }
    
      // Screen-reader text labels controls without adding a global dependency.
      &__visually-hidden {
        position: absolute !important;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
      }
      }
    
  • URL: /components/raw/ds2-top-nav/_ds2-top-nav.scss
  • Filesystem Path: src/library/ds2/02-components/ds2-top-nav/_ds2-top-nav.scss
  • Size: 12.8 KB
  • Content:
    // Burger menu with drill-down panels up to 80rem, mega-menu above it.
    // This file holds the state; how each state looks is in _ds2-top-nav.scss.
    // The buttons are disclosures with aria-expanded, not ARIA menus, and hover is
    // always an extra on top of click and keyboard. Aiming at WCAG 2.2 AA.
    
    // must match $desktopBreakpoint in the SCSS
    // in rem so it moves with the reader's text size: 80rem is 1280px at the usual 16px text
    const desktopMediaQuery = '(min-width: 80rem)';
    
    const openMenuClass = 'ds2-top-nav--menu-open';
    // the search box shows only in this state, on every screen size
    const openSearchClass = 'ds2-top-nav--search-open';
    const openPanelClass = 'ds2-top-nav__panel--open';
    // held for the length of the closing animation, because a closed search box is display: none
    // and so has nothing to animate out of
    const closingSearchClass = 'ds2-top-nav--search-closing';
    // its opposite number: held while the box opens, so the logo and the search icon can fade out
    // instead of blinking away
    const openingSearchClass = 'ds2-top-nav--search-opening';
    
    // must match $searchSlideDuration in the SCSS
    const searchSlideDuration = 400;
    
    export const ds2Header = () => {
      const desktopQuery = window.matchMedia(desktopMediaQuery);
    
      document.querySelectorAll('.ds2-top-nav').forEach((header) => {
        // don't set up the same header twice if the init function is called again
        if (header.classList.contains('ds2-top-nav--js')) return;
    
        initTopNav(header, desktopQuery);
      });
    };
    
    const initTopNav = (header, desktopQuery) => {
      const nav = header.querySelector('.ds2-top-nav__nav');
      if (!nav) return;
    
      const menuToggle = header.querySelector('.ds2-top-nav__menu-toggle');
      const searchToggle = header.querySelector('.ds2-top-nav__search-toggle');
      const searchClose = header.querySelector('.ds2-top-nav__search-close');
    
      // where the nav sits in the markup, so it can be put back exactly as it was below the breakpoint.
      // it's a child of the header, while the logo and the search sit inside the bar, so moving it
      // between them means moving it into the bar
      const bar = header.querySelector('.ds2-top-nav__bar');
      const navHome = nav.nextElementSibling;
      const searchGroup = header.querySelector('.ds2-top-nav__search-group');
      const searchForm = header.querySelector('.ds2-top-nav__search');
      const searchInput = header.querySelector('.ds2-top-nav__search-input');
    
      // each disclosure button in the nav controls one panel: the panel is the button's next sibling.
      // pairing them here, rather than in the markup, keeps every id and aria-controls out of the
      // template, so an implementation in another templating language has only the structure to
      // reproduce, with nothing to number. The markup carries no ARIA for these buttons at all,
      // which is also the honest state without JS: nothing can open, so nothing claims it can
      const panelFor = new Map();
      const buttonFor = new Map();
    
      // ids have to be unique on the page. idPrefix in the config is meant to keep two headers apart,
      // but two left on the default would share every id, which breaks aria-controls and the label,
      // and is a duplicate-id failure in its own right. Rather than rely on remembering, anything
      // already taken is given a suffix here.
      //
      // which of the two keeps the markup's id isn't fixed: whichever is set up first sees the other's
      // copy and moves off it. That's fine, because nothing outside the component depends on these ids,
      // and it only happens at all when they were already clashing
      const freeId = (wanted) => {
        if (!document.getElementById(wanted)) return wanted;
    
        let n = 2;
        while (document.getElementById(`${wanted}-${n}`)) n += 1;
    
        return `${wanted}-${n}`;
      };
    
      // an id the markup set, which another element on the page has already claimed
      const isTaken = (el) => el && el.id
        && Array.from(document.querySelectorAll(`[id="${el.id}"]`)).some((other) => other !== el);
    
      // rename it and mend whatever pointed at it
      const rename = (el, pointers) => {
        if (!isTaken(el)) return;
    
        const to = freeId(el.id);
        el.id = to;
        pointers.forEach(({ element, attribute }) => {
          if (element) element.setAttribute(attribute, to);
        });
      };
    
      rename(nav, [{ element: menuToggle, attribute: 'aria-controls' }]);
      rename(searchForm, [{ element: searchToggle, attribute: 'aria-controls' }]);
      rename(searchInput, [{ element: header.querySelector('.ds2-top-nav__search-label'), attribute: 'for' }]);
    
      // the panels are numbered from the nav's id, so this reads it after any renaming above
      const idBase = nav.id || `${header.id || 'ds2-top-nav'}-menu`;
      let panelNumber = 0;
    
      Array.from(nav.querySelectorAll('.ds2-top-nav__item > button')).forEach((button) => {
        const panel = button.nextElementSibling;
    
        if (panel && panel.classList.contains('ds2-top-nav__panel')) {
          panelFor.set(button, panel);
          buttonFor.set(panel, button);
    
          panelNumber += 1;
          if (!panel.id) panel.id = freeId(`${idBase}-panel-${panelNumber}`);
          button.setAttribute('aria-controls', panel.id);
    
          // the state lives on the button, not the panel (WCAG 4.1.2)
          // it isn't in the markup, so without JS nothing is announced as collapsed while its links show
          button.setAttribute('aria-expanded', 'false');
        }
      });
    
      const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
      let searchClosingTimer;
    
      const isDesktop = () => desktopQuery.matches;
      const isMenuOpen = () => header.classList.contains(openMenuClass);
      const isSearchOpen = () => header.classList.contains(openSearchClass);
      const isExpanded = (button) => button.getAttribute('aria-expanded') === 'true';
      const isTopLevel = (button) => !button.closest('.ds2-top-nav__panel');
      const openButtons = () => Array.from(panelFor.keys()).filter(isExpanded);
    
      // the disclosure button that belongs directly to an item, not one in a nested panel
      const itemButton = (item) => {
        const button = item.querySelector(':scope > button');
        return button && panelFor.has(button) ? button : null;
      };
    
      // on mobile and tablet a panel slides over the list it came from, so what it covers is made inert:
      // the other items, the button that opened it, and the back button of the panel above.
      // Otherwise keyboard and screen reader users reach items hidden behind the panel (WCAG 2.4.11, 4.1.2).
      // inert goes on the siblings because a descendant can't undo it.
      // inert is the one modern feature here with an accessibility consequence rather than a cosmetic
      // one: without it, a keyboard or screen reader user can reach the links behind an open panel.
      // Firefox only got it in April 2023, so it's the thing to check if that ever starts happening
      const setCoveredInert = (button, covered) => {
        const item = button.closest('.ds2-top-nav__item');
        if (!item || !item.parentElement) return;
    
        Array.from(item.parentElement.children).forEach((sibling) => {
          if (sibling !== item) sibling.inert = covered;
        });
        button.inert = covered;
    
        const parentPanel = item.closest('.ds2-top-nav__panel');
        const parentBack = parentPanel && parentPanel.querySelector(':scope > .ds2-top-nav__back');
        if (parentBack) parentBack.inert = covered;
      };
    
      const closePanel = (button) => {
        const panel = panelFor.get(button);
    
        // close nested panels first so nothing is left open inside a closed panel
        Array.from(panel.querySelectorAll('button[aria-expanded="true"]')).forEach((nestedButton) => {
          if (panelFor.has(nestedButton)) closePanel(nestedButton);
        });
    
        button.setAttribute('aria-expanded', 'false');
        panel.classList.remove(openPanelClass);
        setCoveredInert(button, false);
      };
    
      const closeAllPanels = () => {
        openButtons().forEach(closePanel);
      };
    
      const openPanel = (button) => {
        const panel = panelFor.get(button);
        const item = button.closest('.ds2-top-nav__item');
    
        // only one panel open per list
        if (item && item.parentElement) {
          Array.from(item.parentElement.children).forEach((sibling) => {
            const siblingButton = itemButton(sibling);
    
            if (sibling !== item && siblingButton && isExpanded(siblingButton)) {
              closePanel(siblingButton);
            }
          });
        }
    
        button.setAttribute('aria-expanded', 'true');
        panel.classList.add(openPanelClass);
    
        // the panel slides over the button, so focus moves into it (WCAG 2.4.3)
        // leaving focus on the covered button would strand keyboard users behind the panel
        if (!isDesktop()) {
          setCoveredInert(button, true);
    
          // fall back to the back button, so focus never lands on the button just made inert
          const firstItem = panel.querySelector(':scope > .ds2-top-nav__list a[href], :scope > .ds2-top-nav__list button')
            || panel.querySelector(':scope > .ds2-top-nav__back');
    
          if (firstItem) firstItem.focus();
        }
      };
    
      // the icon's aria-expanded says whether the box is showing (WCAG 4.1.2)
      const syncSearchToggle = () => {
        if (searchToggle) searchToggle.setAttribute('aria-expanded', String(isSearchOpen()));
      };
    
      // the icon expands into the search box
      // on desktop the box takes the place of the nav links, so any open dropdown goes with them
      const setSearchOpen = (open) => {
        if (open && isDesktop()) closeAllPanels();
    
        const wasOpen = isSearchOpen();
    
        header.classList.toggle(openSearchClass, open);
        syncSearchToggle();
    
        // play the opening animation backwards on the way out, then let the box go back to display: none.
        // reduced motion skips it, so the box doesn't linger for no reason
        clearTimeout(searchClosingTimer);
    
        header.classList.remove(openingSearchClass);
    
        if (!open && wasOpen && !reducedMotion.matches) {
          header.classList.add(closingSearchClass);
          // it's on its way out, so nothing in it should take focus in the meantime
          if (searchForm) searchForm.inert = true;
    
          searchClosingTimer = setTimeout(() => {
            header.classList.remove(closingSearchClass);
            if (searchForm) searchForm.inert = false;
          }, searchSlideDuration);
        } else if (open && !wasOpen && !reducedMotion.matches) {
          header.classList.remove(closingSearchClass);
          if (searchForm) searchForm.inert = false;
    
          header.classList.add(openingSearchClass);
          searchClosingTimer = setTimeout(() => {
            header.classList.remove(openingSearchClass);
          }, searchSlideDuration);
        } else {
          header.classList.remove(closingSearchClass);
          if (searchForm) searchForm.inert = false;
        }
      };
    
      // the menu and the search box are independent: the burger opens and closes the menu and nothing else.
      // closing the menu used to put the search box away too, which was left over from when the search icon
      // opened the menu as well. It hasn't since 2026-09-14
      const setMenuOpen = (open) => {
        header.classList.toggle(openMenuClass, open);
        if (menuToggle) menuToggle.setAttribute('aria-expanded', String(open));
    
        if (!open) closeAllPanels();
      };
    
      // clicking or tabbing away only closes search if nothing has been typed, so text isn't hidden
      const closeSearchIfEmpty = () => {
        if (isSearchOpen() && !(searchInput && searchInput.value)) setSearchOpen(false);
      };
    
      // close everything, used for clicking outside and focus leaving the header
      const closeAll = () => {
        closeAllPanels();
        setMenuOpen(false);
        closeSearchIfEmpty();
      };
    
      panelFor.forEach((panel, button) => {
        button.addEventListener('click', () => {
          if (isExpanded(button)) {
            closePanel(button);
          } else {
            openPanel(button);
          }
        });
      });
    
      // back buttons return to the panel above and focus the button that opened this one,
      // so people carry on from where they were rather than at the top of the menu (WCAG 2.4.3)
      Array.from(nav.querySelectorAll('.ds2-top-nav__back')).forEach((backButton) => {
        const panel = backButton.closest('.ds2-top-nav__panel');
        const button = panel && buttonFor.get(panel);
        if (!button) return;
    
        backButton.addEventListener('click', () => {
          closePanel(button);
          button.focus();
        });
      });
    
      if (menuToggle) {
        menuToggle.addEventListener('click', () => {
          setMenuOpen(!isMenuOpen());
        });
      }
    
      if (searchToggle) {
        searchToggle.addEventListener('click', () => {
          // the icon only opens the search box, it never opens the menu
          setSearchOpen(true);
    
          if (searchInput) searchInput.focus();
        });
      }
    
      // closing puts focus back on the icon, which is where the user started and is visible again by now
      if (searchClose) {
        searchClose.addEventListener('click', () => {
          setSearchOpen(false);
          if (searchToggle) searchToggle.focus();
        });
      }
    
      // Escape closes things and puts focus back on the control that opened them (WCAG 2.1.1)
      // it's on the document, not the header, so a hover-opened dropdown can be dismissed
      // without moving the pointer or having focus in the header (WCAG 1.4.13)
      document.addEventListener('keydown', (event) => {
        if (event.key !== 'Escape') return;
    
        // let escape clear a search input that has text in it first
        if (event.target === searchInput && searchInput.value) return;
    
        const focusInHeader = header.contains(document.activeElement);
    
        // the search box collapses back to its icon first, on every screen size
        if (isSearchOpen()) {
          setSearchOpen(false);
          if (focusInHeader && searchToggle) searchToggle.focus();
          return;
        }
    
        if (isDesktop()) {
          // desktop: close the open dropdown and return focus to its top level button
          const topButton = openButtons().find(isTopLevel);
          if (!topButton) return;
    
          closeAllPanels();
          if (focusInHeader) topButton.focus();
        } else if (isMenuOpen()) {
          // mobile and tablet: close the whole menu and return focus to the menu button
          closeAll();
          if (focusInHeader && menuToggle) menuToggle.focus();
        }
      });
    
      document.addEventListener('click', (event) => {
        if (!header.contains(event.target)) closeAll();
      });
    
      header.addEventListener('focusin', (event) => {
        if (!isDesktop()) return;
    
        // desktop: tabbing to another part of the header closes dropdowns focus has left
        openButtons().forEach((button) => {
          const item = button.closest('.ds2-top-nav__item');
    
          if (isExpanded(button) && item && !item.contains(event.target)) closePanel(button);
        });
    
        // the close button is left out: focusing it would shut an empty box before the click landed,
        // taking the button out of the page mid-click and leaving focus nowhere
        const movingToClose = searchClose && searchClose.contains(event.target);
    
        if (searchForm && !searchForm.contains(event.target) && !movingToClose) closeSearchIfEmpty();
      });
    
      header.addEventListener('focusout', (event) => {
        // relatedTarget is null when the window loses focus, keep the menu open in that case
        if (event.relatedTarget && !header.contains(event.relatedTarget)) closeAll();
      });
    
      // ACCESSIBILITY: tab order follows the markup, and one markup order can't suit both layouts.
      // the burger has to come before the menu it opens on mobile, which puts the search ahead of the
      // links on desktop, where you'd then tab from the logo out to the search icon and back to the
      // middle for the nav. So the nav is moved ahead of the search on desktop and put back below the
      // breakpoint (WCAG 2.4.3). Moving a node keeps its listeners, and the nav is positioned against
      // the header on mobile, so nothing shifts on screen either way
      const placeNav = () => {
        if (!bar || !searchGroup) return;
    
        if (isDesktop()) {
          // into the bar, between the logo and the search. The bar is display: contents up here,
          // so the nav still takes its place in the header's grid
          if (nav.parentNode !== bar || nav.nextElementSibling !== searchGroup) {
            bar.insertBefore(nav, searchGroup);
          }
        } else if (nav.parentNode !== header || nav.nextElementSibling !== navHome) {
          header.insertBefore(nav, navHome);
        }
      };
    
      // start each layout clean when crossing the desktop breakpoint
      desktopQuery.addEventListener('change', () => {
        // closing first, so focus is never inside the nav while it's being moved
        closeAll();
        setSearchOpen(false);
        placeNav();
      });
    
      // matchMedia only fires on a change, so the starting width has to be handled here
      placeNav();
    
      setMenuOpen(false);
      header.classList.add('ds2-top-nav--js');
    };
  • URL: /components/raw/ds2-top-nav/ds2-top-nav.module.js
  • Filesystem Path: src/library/ds2/02-components/ds2-top-nav/ds2-top-nav.module.js
  • Size: 16.6 KB
{
  "idPrefix": "ds2-top-nav",
  "navLabel": "Main",
  "menuLabel": "Menu",
  "logo": {
    "url": "https://www.leeds.ac.uk/",
    "label": "University of Leeds homepage"
  },
  "search": {
    "action": "https://www.leeds.ac.uk/search",
    "inputName": "q",
    "label": "Search leeds.ac.uk",
    "placeholder": "Search",
    "submitLabel": "Search",
    "openLabel": "Search",
    "closeLabel": "Close search",
    "hiddenFields": [
      {
        "name": "searchOption",
        "value": "searchSite"
      }
    ]
  },
  "level1Options": [
    {
      "title": "Study",
      "level2Options": [
        {
          "title": "Course search",
          "url": "https://courses.leeds.ac.uk/"
        },
        {
          "title": "Undergraduate",
          "level3Options": [
            {
              "title": "Undergraduate home page",
              "url": "https://www.leeds.ac.uk/undergraduate"
            },
            {
              "title": "Clearing 2026",
              "url": "https://www.leeds.ac.uk/clearing"
            },
            {
              "title": "Undergraduate course search",
              "url": "https://courses.leeds.ac.uk/course-search/undergraduate-courses"
            },
            {
              "title": "Fees and funding",
              "url": "https://www.leeds.ac.uk/undergraduate-fees/doc/fees-undergraduate-fees"
            },
            {
              "title": "Student life",
              "url": "https://www.leeds.ac.uk/undergraduate-student-life/doc/students-union"
            }
          ]
        },
        {
          "title": "Online courses",
          "level3Options": [
            {
              "title": "Overview",
              "url": "https://www.leeds.ac.uk/online-courses"
            },
            {
              "title": "Online Masters study",
              "url": "https://www.leeds.ac.uk/online-masters-study"
            },
            {
              "title": "Short online courses",
              "url": "https://www.leeds.ac.uk/short-online-courses"
            },
            {
              "title": "PhD by distance learning",
              "url": "https://www.leeds.ac.uk/online-courses/doc/phd-distance-learning"
            }
          ]
        },
        {
          "title": "Masters",
          "level3Options": [
            {
              "title": "Masters home page",
              "url": "https://www.leeds.ac.uk/masters"
            },
            {
              "title": "Postgraduate open day",
              "url": "https://www.leeds.ac.uk/postgraduate-events"
            },
            {
              "title": "Masters course search",
              "url": "https://courses.leeds.ac.uk/course-search/masters-courses?type=PGT&query="
            },
            {
              "title": "Applying",
              "url": "https://www.leeds.ac.uk/masters-applying/doc/apply-masters-courses"
            },
            {
              "title": "Fees and funding",
              "url": "https://www.leeds.ac.uk/masters-offer/doc/masters-fees-funding"
            }
          ]
        },
        {
          "title": "Research degrees",
          "url": "https://www.leeds.ac.uk/research-degrees"
        },
        {
          "title": "International students",
          "url": "https://www.leeds.ac.uk/international-students"
        },
        {
          "title": "Foundation courses",
          "url": "https://www.leeds.ac.uk/foundation-courses"
        },
        {
          "title": "Mature students",
          "url": "https://www.leeds.ac.uk/mature-students"
        }
      ]
    },
    {
      "title": "Faculties",
      "level2Options": [
        {
          "title": "Arts, Humanities and Cultures",
          "url": "https://ahc.leeds.ac.uk/"
        },
        {
          "title": "Biological Sciences",
          "url": "https://biologicalsciences.leeds.ac.uk/"
        },
        {
          "title": "Business School",
          "url": "https://business.leeds.ac.uk/"
        },
        {
          "title": "Engineering and Physical Sciences",
          "url": "https://eps.leeds.ac.uk/"
        },
        {
          "title": "Environment",
          "url": "https://environment.leeds.ac.uk/"
        },
        {
          "title": "Medicine and Health",
          "url": "https://medicinehealth.leeds.ac.uk/"
        },
        {
          "title": "Social Sciences",
          "url": "https://essl.leeds.ac.uk/"
        },
        {
          "title": "Faculty contacts",
          "url": "https://www.leeds.ac.uk/about/doc/faculties-contacts"
        }
      ]
    },
    {
      "title": "Research",
      "level2Options": [
        {
          "title": "Research overview",
          "url": "https://www.leeds.ac.uk/research-and-innovation"
        },
        {
          "title": "Research excellence and impact",
          "url": "https://www.leeds.ac.uk/research-impact"
        },
        {
          "title": "Strategy and environment",
          "url": "https://www.leeds.ac.uk/research-strategy-environment"
        },
        {
          "title": "Researcher and staff development",
          "url": "https://www.leeds.ac.uk/researcher-staff-development"
        },
        {
          "title": "Public engagement",
          "url": "https://www.leeds.ac.uk/research-public-engagement"
        },
        {
          "title": "Research and innovation contacts",
          "url": "https://www.leeds.ac.uk/research-and-innovation/doc/research-innovation-contacts"
        }
      ]
    },
    {
      "title": "Business",
      "level2Options": [
        {
          "title": "Overview",
          "url": "https://www.leeds.ac.uk/business-partnerships"
        },
        {
          "title": "Access our expertise",
          "url": "https://www.leeds.ac.uk/business-partnerships-access-expertise/doc/work-with-academics"
        },
        {
          "title": "Training and development",
          "url": "https://www.leeds.ac.uk/training-development/doc/cpd-professional-learning-businesses-employers"
        },
        {
          "title": "Recruit our students and graduates",
          "url": "https://www.leeds.ac.uk/business-partnerships/doc/recruit-students"
        },
        {
          "title": "Use our facilities",
          "url": "https://www.leeds.ac.uk/business-partnerships/doc/use-facilities"
        },
        {
          "title": "Support for small businesses",
          "url": "https://www.leeds.ac.uk/business-partnerships/doc/support-small-businesses"
        },
        {
          "title": "Our business partners",
          "url": "https://www.leeds.ac.uk/business-partnerships/doc/business-partners"
        }
      ]
    },
    {
      "title": "About",
      "level2Options": [
        {
          "title": "Campus map",
          "url": "https://www.leeds.ac.uk/campusmap"
        },
        {
          "title": "Our history",
          "url": "https://www.leeds.ac.uk/about/doc/about-history"
        },
        {
          "title": "Faculties",
          "url": "https://www.leeds.ac.uk/our-faculties"
        },
        {
          "title": "Governance",
          "url": "https://www.leeds.ac.uk/structure-governance/doc/governance"
        },
        {
          "title": "Reports, accounts and policies",
          "url": "https://www.leeds.ac.uk/structure-governance/doc/reports-accounts-policies"
        },
        {
          "title": "Strategies and values",
          "url": "https://www.leeds.ac.uk/about/doc/strategies-values"
        },
        {
          "title": "Equality",
          "url": "https://equality.leeds.ac.uk/"
        },
        {
          "title": "Executive team",
          "url": "https://www.leeds.ac.uk/university-executive"
        },
        {
          "title": "News",
          "url": "https://www.leeds.ac.uk/university-news"
        },
        {
          "title": "Events",
          "url": "https://www.leeds.ac.uk/events/"
        }
      ]
    },
    {
      "title": "Alumni",
      "url": "https://www.leeds.ac.uk/alumni"
    }
  ]
}