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&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>
// 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;
}
}
}
}
// 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;
@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;
}
}
// 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');
};
{
"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"
}
]
}