Building a Responsive Navigation Menu in Drupal with CSS and JavaScript
A responsive navigation menu helps Drupal visitors move between key pages on desktops, tablets and phones without forcing the theme into a separate mobile version. The most reliable approach combines Drupal’s existing menu system with semantic HTML, CSS media queries and a small JavaScript behaviour for opening and closing the mobile panel.
This pattern suits content websites, community portals, online retailers and service businesses across Australia. A visitor checking a Sydney tradesperson’s website during a commute needs fast access to contact details, while someone comparing products in Melbourne may expect a compact menu that works comfortably with one hand. A well-structured component supports both use cases while remaining manageable for editors.
Choose The Menu Structure And Breakpoint
Start with the menu configuration rather than writing front-end code. In the Drupal administration interface, create or review the menu links under Structure > Menus, then assign the relevant menu to a theme region through the block layout screen. Keep the primary navigation focused on high-value destinations such as Services, About, Resources and Contact.
Drupal renders menu trees with nested unordered lists. A typical output includes a <nav> element, links, list items and classes such as menu-item--expanded and menu-item--active-trail. Use those classes where possible instead of adding a second navigation system that could become disconnected from editorial changes.
Choose the mobile breakpoint according to the content width, not a device label. A menu with six short links may remain horizontal at 900 pixels, while a retail navigation with product categories may need to collapse at 1100 pixels. For Australian sites, test common laptop widths as well as narrow phones used on public transport in Sydney, Brisbane and Perth.
| Approach | Best use | Advantages | Trade-offs |
|---|---|---|---|
| CSS-only wrap | Short, flat menus | Very little code and good resilience | Limited control over open and closed states |
| CSS menu with JavaScript toggle | Most Drupal primary menus | Clear mobile interaction and accessible state changes | Requires behaviour and keyboard testing |
| Off-canvas panel | Large navigation trees | Makes room for many categories | More focus management and overlay logic |
| Mega menu | Commerce or large organisations | Exposes many destinations at once | Can become difficult on touch screens |
Create A Drupal Library For The Assets
Store the component’s CSS and JavaScript in the theme rather than adding inline code to a Twig template. In a custom theme, create a library in mytheme.libraries.yml:
main-navigation:
css:
component:
css/components/main-navigation.css: {}
js:
js/main-navigation.js: {}
dependencies:
- core/drupal
- core/once
The core/drupal dependency provides Drupal’s JavaScript API, while core/once prevents a behaviour from attaching repeatedly after an Ajax replacement. Attach the library globally in the theme’s .info.yml file if the navigation appears on every page:
libraries:
- mytheme/main-navigation
Alternatively, attach it only to the navigation block or a page template. Global attachment is simpler for a primary menu, but page-specific loading can reduce front-end work on landing pages that use a different header. Clear Drupal caches after changing library definitions so the updated asset collection is available.
For project maintenance, use a custom theme or sub-theme rather than modifying a contributed theme directly. This keeps updates safer and makes it easier to document why a breakpoint, class name or interaction rule exists. Drupal development notes can also provide useful context when organising theme code and administration workflows.
Render A Predictable Navigation In Twig
A theme override gives the navigation a stable hook for the button, menu wrapper and state attributes. In many Drupal themes, a file such as menu--main.html.twig can override the main menu output. The exact variables depend on the Drupal version and theme, so inspect the existing template before replacing it.
A practical structure looks like this:
<nav class="site-navigation" aria-label="{{ 'Main navigation'|t }}">
<button
class="site-navigation__toggle"
type="button"
aria-expanded="false"
aria-controls="primary-menu">
<span class="site-navigation__toggle-label">
{{ 'Menu'|t }}
</span>
</button>
<div class="site-navigation__panel" id="primary-menu">
{{ menus.menu_tree(items, attributes, 0) }}
</div>
</nav>
In a real Drupal template, retain the theme’s menu rendering logic for items, attributes and nested children rather than assuming menus.menu_tree() is available in every context. The important details are the landmark, the button type, the relationship between aria-controls and the panel ID, and the initial aria-expanded value.
Use translated strings with the |t filter. A label such as “Menu” may need localisation for multilingual websites, and visible text should match the language configured for the current page. Avoid hiding the entire navigation from assistive technology with display: none unless the JavaScript state and keyboard behaviour are designed around that change.
Style The Desktop And Mobile States
Build the desktop state first, then introduce a mobile layout at the selected breakpoint. The following CSS creates a horizontal list on wider screens and a hidden panel with a full-width button on smaller screens:
.site-navigation {
position: relative;
}
.site-navigation__panel > ul {
display: flex;
align-items: center;
gap: 1.5rem;
margin: 0;
padding: 0;
list-style: none;
}
.site-navigation__toggle {
display: none;
}
@media (max-width: 48rem) {
.site-navigation__toggle {
display: inline-flex;
align-items: center;
min-height: 2.75rem;
padding: 0.7rem 1rem;
border: 1px solid currentColor;
background: transparent;
cursor: pointer;
}
.site-navigation__panel {
display: none;
padding: 1rem 0;
}
.site-navigation__panel.is-open {
display: block;
}
.site-navigation__panel > ul {
display: block;
}
.site-navigation__panel li + li {
margin-top: 0.5rem;
}
}
Use relative units, sufficient colour contrast and a visible focus outline. A 44-pixel or larger touch target is a sensible baseline for phones. Do not rely on hover to reveal links because touch users do not have a consistent hover action. If the theme uses a dark header, check the menu text and focus indicator against the header colour rather than assuming the default browser outline will be visible.
Nested Drupal menu items need a deliberate treatment. You can leave child links visible in the mobile panel for a simple site, or add a second disclosure button for each expanded parent. The second approach requires separate state management, so it should be introduced only when the information architecture genuinely needs collapsible submenus.
Add A Drupal Behaviour For The Toggle
Drupal behaviours are preferable to a global document-ready handler because they work with BigPipe, Ajax-rendered content and other dynamic page updates. Use once() so the event listener is attached only once to each navigation component:
(function (Drupal, once) {
Drupal.behaviors.mainNavigation = {
attach(context) {
once('main-navigation', '.site-navigation', context).forEach((navigation) => {
const toggle = navigation.querySelector('.site-navigation__toggle');
const panel = navigation.querySelector('.site-navigation__panel');
if (!toggle || !panel) {
return;
}
toggle.addEventListener('click', () => {
const isOpen = toggle.getAttribute('aria-expanded') === 'true';
toggle.setAttribute('aria-expanded', String(!isOpen));
panel.classList.toggle('is-open', !isOpen);
});
});
}
};
})(Drupal, once);
The button’s ARIA state must always reflect the visual state. When the panel is open, aria-expanded is true and the CSS class is present; when it is closed, both are reset. This avoids a common implementation problem where the menu appears open but remains announced as collapsed by a screen reader.
The Escape key can close the panel and return focus to the toggle. A click outside the navigation can also close it, although that feature should be implemented carefully so it does not interfere with links or other header controls. If the menu is a simple block that flows into the page, avoid adding a modal-style focus trap. Reserve focus trapping for a true overlay where the rest of the page is intentionally inaccessible.
Handle Responsive Changes And Accessibility
A user may rotate a phone, resize a browser or connect a tablet to a keyboard. CSS responds immediately to those changes, but JavaScript state can become stale if the panel was open before the breakpoint changed. Use a matchMedia listener when the mobile menu needs explicit cleanup, or ensure the desktop CSS makes the panel visible regardless of its mobile class.
Keyboard testing should cover Tab, Shift+Tab, Enter, Space and Escape. The toggle must be reachable in a logical order, and every link inside the open panel must receive focus normally. Never remove the browser’s outline without supplying a stronger replacement, such as a two-colour ring that remains visible over both light and dark backgrounds.
Checks Before Publishing
- Open and close the menu using a keyboard and a touch screen.
- Confirm the button has a useful accessible name and changing
aria-expanded. - Test long translated labels and unusually deep menu trees.
- Check the layout at 320, 768 and 1440 CSS pixels.
Run the site through automated checks, then test with a screen reader such as VoiceOver on macOS or NVDA on Windows. Automated tools can identify missing labels and contrast failures, but they cannot confirm whether a mobile panel feels predictable. Australian government and enterprise projects commonly reference WCAG expectations, so accessibility should be treated as a delivery requirement rather than a final visual polish step.
Test Drupal Caching And Theme Integration
Drupal’s render cache, dynamic page cache and asset aggregation can make a correct change appear ineffective during development. Clear caches after editing Twig templates, library YAML or theme metadata. In a local environment, disable aggregation when inspecting individual CSS and JavaScript files, then test again with aggregation enabled before release.
Check the menu with anonymous and authenticated users. Administrative toolbar markup, contextual links and permission-based menu items can change the header’s height or available width. A local council site in Melbourne, for example, may expose different navigation links to editors than to residents, while a small Australian retailer may show account and cart controls only to signed-in customers.
Review the component across the actual content workflow. Editors should be able to add a new menu item, create a child link or reorder entries without breaking the layout. Long labels such as “Frequently Asked Questions” should wrap gracefully or trigger the mobile layout at an appropriate width. Avoid fixed heights on the header because they commonly cause clipped text and overlapping content.
Extend The Pattern Without Adding Fragility
Once the basic menu is stable, enhancements can be added in small, testable pieces. A sticky header may improve access to navigation on long pages, but it should not cover focused content or consume excessive space on a phone. An off-canvas drawer may suit a large university or commerce website, but it needs overlay styling, focus return and a reliable close button.
Submenus deserve particular attention. If a parent item is both a destination and a disclosure control, provide two separate controls so touch users can open the child list without losing the parent page. Keep the interaction consistent at every level, and ensure the current page remains identifiable through Drupal’s active trail classes and an accessible visual treatment.
Useful Enhancements
- Add a reduced-motion rule for users who prefer fewer transitions.
- Use CSS custom properties for spacing, colours and focus-ring styles.
- Add end-to-end tests for the open, close and keyboard paths.
- Document the breakpoint and menu assumptions in the theme README.
A responsive Drupal navigation component should remain understandable months after it is launched. Keep the markup semantic, let CSS handle layout, use JavaScript only for state changes, and preserve Drupal’s menu configuration as the source of truth. That balance gives Australian content teams a flexible navigation system that works across mobile networks, office screens and large editorial websites without turning routine menu updates into development tasks.