Skip to content

Navigation - AvSideMenu ​

✨ Introduction ​

The AvSideMenu component is a flexible and accessible navigation side-menu that provides a collapsible sidebar container for navigation purposes. It focuses on layout and collapse functionality, allowing you to insert any content via slots while maintaining full accessibility and keyboard navigation support.

Built with Vue 3 and TypeScript, this component offers smooth transitions, responsive design, and optional collapse functionality that can be enabled or disabled as needed. The component uses defineModel for automatic internal state management, working seamlessly both with and without v-model.

🏗️ Structure ​

The side-menu component consists of the following elements:

  • Header (optional): Contains the collapse/expand toggle button when collapsible=true
  • Toggle Button (optional): Interactive button to collapse/expand the menu (only when collapsible)
  • Content Area: Scrollable area containing your custom content via default slot

The component integrates focus management, proper ARIA attributes, and responsive design patterns for optimal accessibility.

🏷️ Props ​

NameTypeDefaultMandatoryDescription
idstringundefinedUnique identifier for the side-menu
collapsiblebooleantrueWhether the collapsing feature is enabled
widthstring'16rem'Width of the side-menu when expanded
collapsedWidthstring'5rem'Width of the side-menu when collapsed
hideContentWhenCollapsedbooleanfalseWhether to hide the content when the menu is collapsed
stickybooleanfalseWhether the side-menu should be sticky (fixed position on scroll)
stickyOffsetstring'0'Offset from the top when sticky is true (e.g., '0', '1rem', '10px', var(--spacing-lg), etc.)
collapseButtonAriaLabelstring'Collapse button'ARIA label for the collapse button (for accessibility)
expandButtonAriaLabelstring'Expand button'

v-model ​

NameTypeDefaultDescription
collapsedbooleanfalseWhether the side-menu is collapsed (supports v-model with defineModel)

🔊 Events ​

NameData (payload)Description
update:collapsedbooleanEmitted when the collapse state changes (defineModel automatic event)

🎨 Slots ​

NameDescription
defaultDefault slot for custom content (navigation items, menus, etc.)

🚀 Storybook demos ​

You can find examples of use and demo of the component on its dedicated Storybook page.

💡 Examples of use ​

With v-model (Controlled) ​

vue
<script setup lang="ts">
const isCollapsed = ref(false)
</script>

<template>
  <AvSideMenu
    id="main-nav"
    v-model:collapsed="isCollapsed"
  >
    <nav>
      <ul>
        <li>
          <router-link to="/">
            Home
          </router-link>
        </li>
        <li>
          <router-link to="/profile">
            Profile
          </router-link>
        </li>
        <li>
          <router-link to="/settings">
            Settings
          </router-link>
        </li>
      </ul>
    </nav>
  </AvSideMenu>
</template>

Without v-model (Uncontrolled) ​

vue
<template>
  <AvSideMenu id="uncontrolled-nav">
    <nav>
      <ul>
        <li>
          <router-link to="/">
            Home
          </router-link>
        </li>
        <li>
          <router-link to="/profile">
            Profile
          </router-link>
        </li>
        <li>
          <router-link to="/settings">
            Settings
          </router-link>
        </li>
      </ul>
    </nav>
  </AvSideMenu>
</template>

Non-Collapsible Sidemenu ​

vue
<template>
  <AvSideMenu
    id="static-nav"
    :collapsible="false"
  >
    <div class="navigation-content">
      <h3>Navigation</h3>
      <nav>
        <router-link to="/">
          Dashboard
        </router-link>
        <router-link to="/users">
          Users
        </router-link>
        <router-link to="/reports">
          Reports
        </router-link>
      </nav>
    </div>
  </AvSideMenu>
</template>

Custom Width ​

vue
<script setup lang="ts">
import { MDI_ICONS } from '@/components/tokens/icons'

const sidebarCollapsed = ref(true)
</script>

<template>
  <AvSideMenu
    id="custom-nav"
    v-model:collapsed="sidebarCollapsed"
    width="20rem"
    collapsed-width="3rem"
  >
    <nav class="nav-menu">
      <div class="nav-item">
        <AvIcon :name="MDI_ICONS.HOME_VARIANT_OUTLINE" />
        <span v-if="!sidebarCollapsed">Home</span>
      </div>
      <div class="nav-item">
        <AvIcon :name="MDI_ICONS.ACCOUNT_CIRCLE_OUTLINE" />
        <span v-if="!sidebarCollapsed">Profile</span>
      </div>
    </nav>
  </AvSideMenu>
</template>

With Event Handlers ​

vue
<script setup lang="ts">
function handleCollapseChange (collapsed: boolean) {
  console.log('Sidemenu collapsed:', collapsed)
  // Save user preference, update layout, etc.
}
</script>

<template>
  <AvSideMenu
    id="event-nav"
    @update:collapsed="handleCollapseChange"
  >
    <div class="custom-navigation">
      <!-- Your navigation content here -->
    </div>
  </AvSideMenu>
</template>

Complete Application Layout ​

vue
<script setup lang="ts">
import { MDI_ICONS } from '@/components/tokens/icons'

const sidebarCollapsed = ref(false)

const navigationItems = [
  {
    label: 'Mon parcours',
    icon: MDI_ICONS.HOME_VARIANT_OUTLINE,
    path: '/'
  },
  {
    label: 'Mes formations',
    icon: MDI_ICONS.BOOK_LOCATION_OUTLINE,
    path: '/formations'
  },
  {
    label: 'Mes expériences',
    icon: MDI_ICONS.BRIEFCASE_VARIANT_OUTLINE,
    path: '/experiences'
  },
  {
    label: 'Mes activités',
    icon: MDI_ICONS.STAR_SHOOTING_OUTLINE,
    path: '/activities'
  }
]

function onSidebarToggle (collapsed: boolean) {
  // Save user preference to localStorage
  localStorage.setItem('sidebar-collapsed', collapsed.toString())
}
</script>

<template>
  <div class="app-layout">
    <AvSideMenu
      id="app-nav"
      v-model:collapsed="sidebarCollapsed"
      width="18rem"
      collapsed-width="4rem"
      @update:collapsed="onSidebarToggle"
    >
      <div class="navigation-wrapper">
        <div class="nav-header">
          <h2 v-if="!sidebarCollapsed">
            Cofolio
          </h2>
        </div>

        <nav class="nav-menu">
          <router-link
            v-for="item in navigationItems"
            :key="item.path"
            :to="item.path"
            class="nav-item"
            :class="{ 'nav-item--active': $route.path === item.path }"
          >
            <AvIcon :name="item.icon" />
            <span v-if="!sidebarCollapsed">{{ item.label }}</span>
          </router-link>
        </nav>
      </div>
    </AvSideMenu>

    <main class="main-content">
      <router-view />
    </main>
  </div>
</template>

<style scoped>
.app-layout {
  display: flex;
  height: 100vh;
}

.main-content {
  flex: 1;
  overflow: auto;
  padding: 2rem;
}

.navigation-wrapper {
  height: 100%;
  display: flex;
  flex-direction: column;
}

.nav-header {
  padding: 1rem;
  border-bottom: 1px solid var(--divider);
}

.nav-menu {
  flex: 1;
  padding: 1rem 0;
}

.nav-item {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.75rem 1rem;
  margin: 0.25rem;
  color: var(--text1);
  text-decoration: none;
  border-radius: var(--radius-md);
  transition: all 0.2s ease;
}

.nav-item:hover {
  background-color: var(--surface-background);
}

.nav-item--active {
  background-color: var(--dark-background-primary1);
  color: white;
}
</style>