Skip to content

feat: mobile header and sidebar navigation#2180

Merged
g-francesca merged 61 commits intoredesignfrom
header-sidebar-navigation
Feb 18, 2026
Merged

feat: mobile header and sidebar navigation#2180
g-francesca merged 61 commits intoredesignfrom
header-sidebar-navigation

Conversation

@g-francesca
Copy link
Collaborator

@g-francesca g-francesca commented Feb 5, 2026

Preview: https://deploy-preview-2180--expressjscom-preview.netlify.app/

This PR introduces a comprehensive sidebar navigation system for the Express.js documentation with multi-level navigation support, version-aware content filtering, and accessibility features.

Overview

The Sidebar component provides a hierarchical navigation system that supports:

  • Multi-level navigation (up to 5 levels deep)
  • Version-aware content filtering with automatic item omission
  • Keyboard navigation and focus management
  • Automatic active state detection based on current page

Menu Configuration

Menus are configured using TypeScript files in astro/src/config/:

Menu Structure

export const menu: Menu = {
  sections: [
    {
      title?: string;              // Optional section title
      basePath?: string;           // Base path prepended to all hrefs
      versioned?: boolean;         // Whether URLs include version prefix
      omitFrom?: VersionPrefix[];  // Hide entire section from specific versions
      items: MenuItem[];           // Array of menu items
    }
  ],
  items?: MenuItem[];              // Items outside sections
};

Menu Items

Menu items can be either links or submenu triggers:

// Link item
{
  label: 'Overview',
  ariaLabel?: 'Overview page',   // Optional accessible label
  icon?: 'files',                 // Optional icon (for root level)
  href: '/docs/overview',
  omitFrom?: ['v3']              // Hide from specific versions
}

// Submenu item
{
  label: 'API Reference',
  icon?: 'code',
  submenu: {
    basePath: '/api-reference',
    versioned: true,
    sections: [...]
  },
  omitFrom?: ['v3']
}

Organizing Navigation Items

Sections

Related items can be grouped under titled sections:

sections: [
  {
    title: 'Application',
    items: [
      { href: '/application/overview', label: 'Overview' },
      { href: '/application/app-all', label: 'app.all' },
      { href: '/application/app-delete', label: 'app.delete' },
    ],
  },
  {
    title: 'Request',
    items: [
      { href: '/request/overview', label: 'Overview' },
      { href: '/request/req-accepts', label: 'req.accepts' },
    ],
  },
];

Nested Submenus (Multi-level Navigation)

Create up to 5 levels of navigation by nesting submenus:

// Level 0: Root menu
{
  label: 'Docs',
  submenu: {
    // Level 1: First submenu
    sections: [
      {
        title: 'Getting Started',
        items: [
          { href: '/installation', label: 'Installation' },
          {
            label: 'Advanced',
            submenu: {
              // Level 2: Second submenu
              items: [
                { href: '/advanced/middleware', label: 'Middleware' }
              ]
            }
          }
        ]
      }
    ]
  }
}

Version-Based Content Filtering

Adding/Omitting Items by Version

Use the omitFrom property to hide items from specific versions:

// Item visible in all versions
{ href: '/application/overview', label: 'Overview' }

// Item hidden from v3 only
{
  href: '/application/app-delete',
  label: 'app.delete',
  omitFrom: ['v3']
}

// Item only visible in v3
{
  href: '/request/req-accepted',
  label: 'req.accepted',
  omitFrom: ['v5', 'v4']
}

Omitting Entire Sections

Hide entire sections from specific versions:

{
  title: 'New Features',
  omitFrom: ['v3', 'v4'],  // Only show in v5
  items: [
    { href: '/new-feature-1', label: 'Feature 1' },
    { href: '/new-feature-2', label: 'Feature 2' },
  ]
}

How Version Filtering Works

  1. Detection: The sidebar automatically detects the current version from the URL path (e.g., /en/docs/v5/...)
  2. Filtering: Items and sections with omitFrom containing the current version are excluded
  3. Rendering: Only applicable items are rendered in the DOM
  4. Version Switching: When the user switches versions:
    • If the current page exists in the new version → navigates to the same page
    • If the current page is omitted in the new version → navigates to the first available page

Version Switcher Integration

The sidebar includes an integrated version switcher that appears when navigating into versioned submenus:

versions = [
  { id: 'v5', label: 'v5.x', isDefault: true },
  { id: 'v4', label: 'v4.x' },
  { id: 'v3', label: 'v3.x (deprecated)' },
];

Key features:

  • Appears only in versioned navigation panels
  • Automatically hides when navigating back to root level
  • Handles graceful fallback when switching to versions that omit the current page
  • Updates URL structure from /en/docs/v4/page to /en/docs/v5/page

Keyboard Navigation & Accessibility

Keyboard Controls

  • Escape: Navigate back one level (or close sidebar if at root)
  • Tab/Shift+Tab: Navigate between focusable elements
  • Arrow keys: Navigate between menu items
  • Focus trap: Focus stays within sidebar when open

ARIA Implementation

  • role="dialog" and aria-modal="true" for sidebar
  • aria-expanded on submenu triggers
  • aria-hidden for inactive navigation panels
  • aria-label for navigation landmarks
  • Screen reader announcements for navigation changes

Active State Detection

The sidebar automatically detects and highlights the current page:

  1. Path Matching: Compares current URL with menu item hrefs
  2. Deep Navigation: Opens nested submenus containing the current page
  3. Initial State: Restores proper navigation level on page load
  4. Visual Indicator: Adds sidebar-nav-item--active class to current page link

Technical Architecture

Component Structure

Sidebar.astro                      # Container component
├── SidebarMenu.astro             # Menu renderer (recursive for levels)
│   └── SidebarItemsList.astro    # Items list renderer
│       └── SidebarNavItem.astro  # Individual item renderer
├── SidebarController.ts          # Navigation state management
├── SidebarVersionManager.ts      # Version switching logic
├── SidebarFocusTrap.ts          # Focus management
└── utils.ts                      # Helper functions

Key Logic

  • collectAllSubmenus(): Recursively collects all nested submenus during build
  • shouldOmitItem()/shouldOmitSection(): Version-based filtering
  • resolveHref(): Constructs full URLs with language, version, and basePath
  • submenuContainsCurrentPath(): Checks if a submenu contains the active page
  • calculateInitialActiveLevel(): Determines which navigation level to show on load

Configuration Files

Create new menu configurations in astro/src/config/:

Example: Adding a New Menu Item

// In astro/src/config/api-reference-menu.ts

{
  title: 'Application',
  items: [
    { href: `/application/overview`, label: 'Overview' },
    { href: `/application/app-all`, label: 'app.all' },
    // Add new item here
    {
      href: `/application/app-engine`,
      label: 'app.engine',
      omitFrom: ['v3']  // Optional: hide from v3
    },
  ]
}

Notes

Please note that:

  • This PR focuses exclusively on the mobile sidebar navigation.
  • Language switcher and search integration are intentionally excluded and will be delivered in separate PRs.
  • Menu configuration currently includes test scenarios for multiple edge cases; final configuration refinements will follow.

@g-francesca g-francesca requested a review from a team as a code owner February 5, 2026 18:08
@netlify
Copy link

netlify bot commented Feb 5, 2026

Deploy Preview for expressjscom-preview ready!

Name Link
🔨 Latest commit bbc768d
🔍 Latest deploy log https://app.netlify.com/projects/expressjscom-preview/deploys/699586d2754cbd0008b53bba
😎 Deploy Preview https://deploy-preview-2180--expressjscom-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 100 (🟢 up 3 from production)
Accessibility: 100 (🟢 up 13 from production)
Best Practices: 100 (no change from production)
SEO: 100 (🟢 up 6 from production)
PWA: 80 (🟢 up 50 from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@socket-security
Copy link

socket-security bot commented Feb 5, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​iconify-json/​fluent@​1.2.38961007792100
Addednpm/​postcss-nested@​7.0.21001009680100

View full report

@g-francesca
Copy link
Collaborator Author

Captura de Tela 2026-02-10 às 20 16 46 I like the language option in the navigation bar next to the search (similar to the Node.js website); I think it allows for better user experience.

@carlosstenzel We can easily move it to the header, next to the search trigger. In that case, I would also consider moving the theme switcher there as well (same as node).

On mobile, Node moves the search trigger, theme switcher, and language selector into the dropdown menu (see screenshot). I’d keep these controls always visible in the header. We just need to ensure there’s enough touch spacing between the icons to maintain good usability.

wdyt?

@bjohansebas @ShubhamOulkar

Screenshot 2026-02-11 alle 11 43 57 Screenshot 2026-02-11 alle 11 47 57

To help visualize how these actions would look in the header versus the sidebar, I’ve added them to both. This is a temporary setup and will be revisited once we make a final decision.

@carlosstenzel @ShubhamOulkar @bjohansebas

@bjohansebas
Copy link
Member

Yep, in my opinion the theme switcher and the language selector should always be visible, like Node.js has it, similar to the last change you made. For the search, I’m not sure how you’re planning to implement it, maybe a modal like in Node.js? Either way, I prefer having them easily accessible, since it reduces the number of clicks a user has to make to use those features.

I haven’t reviewed the code for the latest changes yet, I’ll do that tomorrow, but I wanted to share my opinion in advance, since the time difference might affect when I’m able to review again, usually in the afternoon/evening GMT-5.

One more note: we’ll need to fix that part, I know it was more of a demo of how you wanted it to look, but there’s a layout shift when switching between dark mode and light mode.

@g-francesca
Copy link
Collaborator Author

ep, in my opinion the theme switcher and the language selector should always be visible, like Node.js has it, similar to the last change you made. For the search, I’m not sure how you’re planning to implement it, maybe a modal like in Node.js? Either way, I prefer having them easily accessible, since it reduces the number of clicks a user has to make to use those features.

I haven’t reviewed the code for the latest changes yet, I’ll do that tomorrow, but I wanted to share my opinion in advance, since the time difference might affect when I’m able to review again, usually in the afternoon/evening GMT-5.

One more note: we’ll need to fix that part, I know it was more of a demo of how you wanted it to look, but there’s a layout shift when switching between dark mode and light mode.

Ok, so at this point, I think that we all agree to move both the theme switcher and the language selector into the header. Yep, I did notice the layout shift, I’m fixing it now, as we’ve reached a final decision on the placement.

Regarding the search: my plan was to stick to the Figma design, assuming this layout was already approved. The current proposal uses a full-screen search modal on mobile and tablet, and a lateral sidebar on desktop. Let me know if this works for you, or if you’d like to revisit the approach.

Thanks for the review, I’ll wait for your feedback today.

@bjohansebas
Copy link
Member

bjohansebas commented Feb 14, 2026

Okay, I like the code. The only thing I don’t think we should do is reload the page every time a different version is selected.

We can also complete the menu in a follow-up PR, since it’s quite a bit of manual work. What would be great to handle right away, though, is making sure the URL doesn’t change when selecting a new version.

I’m also trying to figure out what needs to be merged or added, since we haven’t been very good at adding APIs or keeping things up to date, which is something that needs to change . Anyway, I’m tracking it here #2186 . It’s not something for Orama, but it is for the Express team.

@g-francesca
Copy link
Collaborator Author

Okay, I like the code. The only thing I don’t think we should do is reload the page every time a different version is selected.

We can also complete the menu in a follow-up PR, since it’s quite a bit of manual work. What would be great to handle right away, though, is making sure the URL doesn’t change when selecting a new version.

I’m also trying to figure out what needs to be merged or added, since we haven’t been very good at adding APIs or keeping things up to date, which is something that needs to change . Anyway, I’m tracking it here #2186 . It’s not something for Orama, but it is for the Express team.

@bjohansebas Thanks for the review! I have some concerns about removing URL changes for version switching:

  • Not all pages exist in all versions: when switching to a version that doesn't have the current page, we need to redirect to a fallback page anyway (already implemented)
  • No shareability: users can't share/bookmark links specific to a version
  • Browser history broken: back/forward buttons won't work
  • SEO problems: search engines can't index versions separately

I would keep the URL versioning but try to implement smooth SPA-like transitions between versions. What do you think?

@bjohansebas
Copy link
Member

No, I don’t mean removing the URL. I mean reloading the page when a version is selected, I don’t think we should do that.

We can simply load the menu when a specific version is selected, without reloading the page.

@g-francesca
Copy link
Collaborator Author

No, I don’t mean removing the URL. I mean reloading the page when a version is selected, I don’t think we should do that.

We can simply load the menu when a specific version is selected, without reloading the page.

Got it, I misunderstood sorry! You basically want to give users the ability to explore what's available in different versions without reloading pages, right?

Example: I'm on /en/5x/api/application/overview → switch version to v4 → URL stays the same (content stays v5) → lateral menu updates to show v4 structure → page only reloads when clicking a menu item from v4.

If the example is correct and reflects your expectation, I have questions about a few critical edge cases:

Active item: Since I'm viewing v5 content but showing v4 menu, there would be no active item. Is this expected?
Page refresh: Should the menu remember the selected version (v4) or revert to match the URL (v5)?
Recovery: Is the version switcher the only way to return the menu back to v5?
Internal links: If I click a breadcrumb or a page link to another v5 page while the menu shows v4, should the menu stay on v4 or revert to v5?
No visual change: If the menu structure is identical between versions, nothing happens visually when switching. This may be perceived as an error. Is this acceptable?

Let me know so I can understand the user scenario you have in mind and implement this correctly. Ty
Of course, if you have any live examples to share, that would help.

@bjohansebas
Copy link
Member

Example: I'm on /en/5x/api/application/overview → switch version to v4 → URL stays the same (content stays v5) → lateral menu updates to show v4 structure → page only reloads when clicking a menu item from v4.

yeah!

: Since I'm viewing v5 content but showing v4 menu, there would be no active item. Is this expected?

yes it's expected

Should the menu remember the selected version (v4) or revert to match the URL (v5)?

I think we should go back to menu 5, or to the version that’s in the URL

Is the version switcher the only way to return the menu back to v5?

Hmm, yes, by reloading the page or switching between URLs of the same version.

If I click a breadcrumb or a page link to another v5 page while the menu shows v4, should the menu stay on v4 or revert to v5?

revert to v5

If the menu structure is identical between versions, nothing happens visually when switching. This may be perceived as an error. Is this acceptable?

I understand there might be some confusion, but if the guides or APIs are the same, it’s acceptable

if you have any live examples to share, that would help.

I’m not sure there’s any page that has implemented it yet

@g-francesca
Copy link
Collaborator Author

g-francesca commented Feb 16, 2026

Example: I'm on /en/5x/api/application/overview → switch version to v4 → URL stays the same (content stays v5) → lateral menu updates to show v4 structure → page only reloads when clicking a menu item from v4.

yeah!

: Since I'm viewing v5 content but showing v4 menu, there would be no active item. Is this expected?

yes it's expected

Should the menu remember the selected version (v4) or revert to match the URL (v5)?

I think we should go back to menu 5, or to the version that’s in the URL

Is the version switcher the only way to return the menu back to v5?

Hmm, yes, by reloading the page or switching between URLs of the same version.

If I click a breadcrumb or a page link to another v5 page while the menu shows v4, should the menu stay on v4 or revert to v5?

revert to v5

If the menu structure is identical between versions, nothing happens visually when switching. This may be perceived as an error. Is this acceptable?

I understand there might be some confusion, but if the guides or APIs are the same, it’s acceptable

if you have any live examples to share, that would help.

I’m not sure there’s any page that has implemented it yet

@bjohansebas Thanks for clarifying! Would you mind if I manage this in a different branch (created from this one)? Just to have the 2 different behaviours comparable.

Also, can you please clarify how you expect to handle this case:
I visit API menu -> I switch to v3 (I can do that since API page are versioned for v3) -> I interact with the menu and I go back to "Docs" menu.
Since docs pages are not versioned for v3, what should be the selected version in this case? Do we automatically move back to v5?

@bjohansebas
Copy link
Member

bjohansebas commented Feb 16, 2026

Would you mind if I manage this in a different branch (created from this one)? Just to have the 2 different behaviours comparable.

Do you want to handle this in another PR? I don’t have any problem with that, so you can merge this and continue working on the desktop part as well.

Since docs pages are not versioned for v3, what should be the selected version in this case? Do we automatically move back to v5?

Here I think we should show the guides for the latest version and display a note saying that there are no guides for v3. We should also mention that v3 is in end-of-life and include a link to the support page.

@g-francesca
Copy link
Collaborator Author

Would you mind if I manage this in a different branch (created from this one)? Just to have the 2 different behaviours comparable.

Do you want to handle this in another PR? I don’t have any problem with that, so you can merge this and continue working on the desktop part as well.

Since docs pages are not versioned for v3, what should be the selected version in this case? Do we automatically move back to v5?

Here I think we should show the guides for the latest version and display a note saying that there are no guides for v3. We should also mention that v3 is in end-of-life and include a link to the support page.

mm yes, maybe better closing this PR.
Let me know if there's something else you would like to cover in this PR, or I can proceed with the merge. Thank you!

Copy link
Member

@bjohansebas bjohansebas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my side, everything looks good. We can proceed with the next PRs

@ShubhamOulkar
Copy link
Member

I noticed that Body.astro, BodyMd.astro, BodySm.astro, and BodyXs.astro are nearly identical wrapper components.

We could significantly reduce code duplication by consolidating these into a single or component that accepts a size or variant prop (e.g., <Body size="sm"> instead of <BodySm>). The same logic applies to the heading components (H1-H6), which could be unified into a single <Heading level={1}> component.

I've left them as is for now to match the current pattern, but it's worth considering for a future cleanup!

@g-francesca
Copy link
Collaborator Author

I noticed that Body.astro, BodyMd.astro, BodySm.astro, and BodyXs.astro are nearly identical wrapper components.

We could significantly reduce code duplication by consolidating these into a single or component that accepts a size or variant prop (e.g., <Body size="sm"> instead of <BodySm>). The same logic applies to the heading components (H1-H6), which could be unified into a single <Heading level={1}> component.

I've left them as is for now to match the current pattern, but it's worth considering for a future cleanup!

Makes sense! I've noted this for future interaction. TY

@g-francesca g-francesca merged commit e8dbc45 into redesign Feb 18, 2026
8 checks passed
@ShubhamOulkar ShubhamOulkar deleted the header-sidebar-navigation branch March 1, 2026 00:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants