mobileNavigation.html
<nav class="mobileNavigation">
  <p-openable class="mobileNavigation__menu button -circle -ghost" label="menu">
    <template #toggle>
        <svg class="button__icon icon"><use href="/main-icons-sprite.svg#bars" /></svg>
    </template>
    <template v-slot="{ click }">
      <div class="mobileNavigation__pane" v-scrolllock>
        <div class="mobileNavigation__title">
          Menu
        </div>
        <ul class="mobileNavigation__list" v-directionals>
          <li>
            <p-openable class="mobileNavigation__button" name="submenu">
              <template #toggle>
                Submenu
                <svg class="icon"><use href="/main-icons-sprite.svg#chevron-right" /></svg>
              </template>
              <template v-slot="{ click }">
                <div class="mobileNavigation__pane">
                  <div class="mobileNavigation__title">
                    <div class="mobileNavigation__actions">
                      <button class="button -circle -ghost" @click.stopPropagation="click" tabindex="-1">
                        <svg class="icon"><use href="/main-icons-sprite.svg#chevron-left" /></svg>
                      </button>
                    </div>
                    Menu
                  </div>
                  <ul class="mobileNavigation__list" v-directionals>
                    <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Consectetur asperiores</a></li>
                    <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Consectetur asperiores</a></li>
                    <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Amet porro.</a></li>
                    <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Ipsum dolorem</a></li>
                  </ul>
                </div>
              </template>
            </p-openable>
          </li>
          <li>
            <p-openable class="mobileNavigation__button" name="submenu">
              <template #toggle>
                STUFF
                <svg class="icon"><use href="/main-icons-sprite.svg#chevron-right" /></svg>
              </template>
              <template v-slot="{ click }">
                <div class="mobileNavigation__pane">
                  <div class="mobileNavigation__title">
                    <div class="mobileNavigation__actions">
                      <button class="button -circle -ghost" @click.stopPropagation="click" tabindex="-1">
                        <svg class="icon"><use href="/main-icons-sprite.svg#chevron-left" /></svg>
                      </button>
                    </div>
                    Menu
                  </div>
                  <ul class="mobileNavigation__list" v-directionals>
                    <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Consectetur asperiores</a></li>
                    <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Consectetur asperiores</a></li>
                    <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Amet porro.</a></li>
                    <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Ipsum dolorem</a></li>
                  </ul>
                </div>
              </template>
            </p-openable>
          </li>
          <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Consectetur asperiores</a></li>
          <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Amet porro.</a></li>
          <li class="mobileNavigation__item"><a href="" class="mobileNavigation__link">Ipsum dolorem</a></li>
          <li class="mobileNavigation__item">
            <a class="mobileNavigation__link" href="">Amet impedit cumque</a>
          </li>
          <li class="mobileNavigation__item">
            <a class="mobileNavigation__link" href="">Consectetur vitae temporibus!</a>
          </li>
          <li class="mobileNavigation__item">
            <a class="mobileNavigation__link" href="">Consectetur doloremque reiciendis</a>
          </li>
          <li class="mobileNavigation__item">
            <a class="mobileNavigation__link" href="">Dolor pariatur rerum.</a>
          </li>
          <li class="mobileNavigation__item">
            <a class="mobileNavigation__link" href="">Ipsum a officiis.</a>
          </li>
          <li class="mobileNavigation__item">
            <a class="mobileNavigation__link" href="">Dolor unde debitis.</a>
          </li>
          <li class="mobileNavigation__item">
            <a class="mobileNavigation__link" href="">Adipisicing delectus velit</a>
          </li>
          <li class="mobileNavigation__item">
            <a class="mobileNavigation__link" href="">Amet aliquam voluptates</a>
          </li>
        </ul>
      </div>
      <div class="mobileNavigation__closeButton">
        <button class="button -circle -ghost" @click="click">
          <svg class="icon"><use href="/main-icons-sprite.svg#close" /></svg>
        </button>
      </div>
    </template>
  </p-openable>
</nav>

<p>Elit nesciunt repellat aut exercitationem minus Ea doloribus asperiores quos aliquam repellat Et dolor minima eaque nostrum dolore animi. Debitis expedita assumenda dolores tenetur recusandae Ea nesciunt totam tenetur excepturi</p>
<p>Lorem nisi eveniet asperiores itaque fugit nam Iusto culpa enim aspernatur dolore error Doloribus eligendi ab id consequatur ratione Maxime quae et esse corrupti magni. Totam exercitationem dolor voluptate soluta.</p>
<p>Adipisicing lorem beatae recusandae nisi id Facere distinctio laboriosam similique est quas. Laboriosam quam veniam impedit nostrum rem. Reiciendis praesentium totam nobis tenetur adipisci. Fuga odit corporis ducimus ipsam alias.</p>
<p>Consectetur dolor labore modi eaque delectus Accusamus eligendi in sequi sapiente enim Corporis laudantium inventore odit minima culpa Eum ducimus molestiae sapiente assumenda facere ex. Quaerat itaque aliquam aspernatur quo</p>
<p>Adipisicing nesciunt similique illum sint iure, aliquam? Ipsam fugiat labore explicabo itaque error? Nulla sequi alias ab provident vitae Commodi quasi iste omnis provident saepe Exercitationem dolorum quod magni inventore</p>
<p>Sit fugit a distinctio ipsa fugiat. Totam in tempore tempora laboriosam rem Facilis fugit excepturi iure eius doloribus, expedita numquam? Suscipit commodi iusto cum assumenda voluptatum, unde nihil Ab molestiae?</p>
<p>Elit voluptate quos velit nostrum tempora minima. Perspiciatis odit voluptatibus placeat laudantium at Corporis illo saepe ducimus explicabo alias. Quia tenetur corporis odit dicta esse laboriosam Placeat dolore velit necessitatibus?</p>
<p>Elit ipsam alias maxime pariatur tenetur. Culpa a aspernatur quas blanditiis culpa. Laudantium exercitationem accusantium corrupti vero rerum! Eligendi corporis eveniet eos dignissimos facilis Tempora ab cum sapiente vero placeat.</p>
<p>Consectetur distinctio temporibus error voluptate nisi recusandae odio quis Enim quam officia ipsam vel consequatur beatae laudantium pariatur est? Distinctio id quos ea repellendus quo Impedit nulla necessitatibus in quisquam.</p>
<p>Lorem optio eligendi quis architecto laboriosam Similique possimus fugit dolorum sequi atque dolores. Obcaecati corporis labore accusamus fuga voluptatem inventore laboriosam quod perspiciatis. Magni sapiente accusamus veniam harum obcaecati. Quo</p>

index.scss
.mobileNavigation {
  --link-color: var(--accent-color);

  &__pane {
    background: #fff;
    box-shadow: var(--root-box-shadow-med);
    inset: 0 4.5rem 0 0;
    overflow-x: auto;
    position: fixed;
    transition: translate var(--root-ease-out-fast);
    translate: 0;
    z-index: 2;

    @starting-style {
      translate: -100%;
    }

    & & {
      inset: 0;
    }
  }

  &__closeButton {
    background: var(--color-gray-100);
    display: grid;
    inset: 0 0 0 auto;
    padding: .375rem 0;
    place-content: start center;
    position: fixed;
    transition: translate var(--root-ease-out-fast);
    translate: 0;
    width: 4.5rem;
    z-index: 1;

    @starting-style {
      translate: 100%;
    }
  }

  &__title {
    display: grid;
    place-content: center;
    padding: .25rem .5rem;
    min-height: 4.25rem;
  }


  &__actions {
    position: absolute;
    display:flex;
    gap: .5rem;
  }

  &__button,
  &__link {
    padding: 1rem .5rem 1rem 1.5rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    color: var(--accent-color);
  }

  &__list > li {
    border-top: 1px solid var(--color-gray-100);
  }

  &__button {
    appearance: none;
    border: 0;
    width: 100%;
    background: #0000;
  }
}
POpenable.vue
<script>
  const openableGroups = {}
</script>

<script setup>
import { ref, nextTick, useSlots, useTemplateRef } from 'vue'

import focusableElements from '../composables/FocusableElements.js'

const {
  closeOnBlur,
  refocus,
  label,
  name,
  openOnHover,
  hoverOrClick
} = defineProps({
  closeOnBlur: { type: Boolean, default: true },
  refocus: { type: Boolean, default: true },
  label: { type: String, default: "" },
  name: { type: String, default: "global" },
  openOnHover: { type: Boolean, default: false },
  hoverOrClick: { type: Boolean, default: false },
})
const emit = defineEmits(['open', 'close'])
const slots = useSlots()
const button = useTemplateRef('button')
const openable = useTemplateRef('openable')
const open = ref(false)

const targetOutside = evt => {
  if (openable.value && !openable.value.contains(evt.target)) {
    toggle()
  }
}

const pressEscape = evt => {
  if (evt.key === 'Escape') {
    evt.stopPropagation()
    toggle()
  }
}

let escapeHandler = null

const updateGroup = (open) => {
  if (!name) {
    return
  }

  if (!(name in openableGroups)) {
    openableGroups[name] = new Set()
  }

  if (open) {
    openableGroups[name].forEach(t => t())
    openableGroups[name].add(toggle)
  } else {
    openableGroups[name].delete(toggle)
  }
}

const toggle = (evt, { noFocus = false } = {}) => {
  evt?.stopPropagation()
  emit(open.value ? 'close' : 'open')
  open.value = !open.value
  updateGroup(open.value)
  nextTick(() => {
    if (open.value) {
      if (refocus && !noFocus) {
        focusableElements(openable)?.[0].focus()
      }

      document.documentElement.addEventListener('click', targetOutside)
      openable.value.addEventListener('keydown', pressEscape)

      if (closeOnBlur) {
        document.documentElement.addEventListener('focusin', targetOutside)
      }
    } else {
      document.documentElement.removeEventListener('click', targetOutside)
      openable.value.removeEventListener('keydown', pressEscape)

      if (closeOnBlur) {
        document.documentElement.removeEventListener('focusin', targetOutside)
      }

      if (refocus && !noFocus) {
        focusableElements(button)?.[0].focus()
      }
    }
  })
}


let timeout = null

const mouseover = () => {
  if (timeout) {
    clearTimeout(timeout)
  }

  timeout = setTimeout(() => {
    if (!open.value) {
      toggle(undefined, { noFocus: true })
    }
  }, 450)
}

const mouseout = () => {
  if (timeout) {
    clearTimeout(timeout)
  }

  timeout = setTimeout(() => {
    if (open.value) {
      toggle(undefined, { noFocus: true })
    }
  }, 450)
}

const keypress = evt => {
  if (evt.key == ' ' || evt.key == 'Enter') {
    toggle()
  }
}

let bindings = { click: toggle }
if (openOnHover) {
  bindings = { mouseover, mouseout, keypress }
} if (hoverOrClick) {
  bindings = { click: toggle, mouseover, mouseout }
}

</script>

<template>
  <button v-bind="$attrs" ref="button" v-on="bindings">
    <slot name="toggle" v-bind="bindings">{{ label }}</slot>
  </button>
  <div ref="openable" v-on="(openOnHover || hoverOrClick) ? { mouseover, mouseout } : {}">
    <slot v-if="open" v-bind="bindings" />
  </div>
</template>
vDirectionals.js
import focusableElements from '../composables/FocusableElements.js'

/**
 * Default key filters. These should be functions that 'filter out' what
 * elements should be considered when a key is hit.
 */
const KEY_BINDINGS = {
  ArrowRight: elements => elements.filter(({ x, top, bottom }) => x > 0 && top < 0 && bottom > 0),
  ArrowLeft: elements => elements.filter(({ x, top, bottom }) => x < 0 && top < 0 && bottom > 0),
  ArrowDown: elements => elements.filter(({ y, left, right }) => y > 0 && left < 0 && right > 0),
  ArrowUp: elements => elements.filter(({ y, left, right }) => y < 0 && left < 0 && right > 0),
  Home: elements => elements.length && elements.slice(0, 1),
  End: elements => elements.length && elements.slice(-1),
}

/**
 * Retrieves where the element is drawn on screen (client rects).
 * Adds in x and y for the center of the element.
 */
const getElementRects = el => {
  if (!el) {
    console.error("Unable to determine location of element", el);
    return null
  }

  const rects = el.getClientRects()[0]

  if (!rects) {
    return getElementRects(el.parentElement)
  }

  return {
    bottom: rects.bottom,
    height: rects.height,
    left: rects.left,
    right: rects.right,
    top: rects.top,
    width: rects.width,
    x: rects.left + rects.width / 2,
    y: rects.top + rects.height / 2,
  }
}

/**
 * Calls getElementRects for every element in nodeList, and adjust the
 * values to be relative to origin for simpler follow up math. Also
 * calculates the distance between the two elements' centers.
 */
const augmentElementRects = (nodeList, origin) => {
  const elements = []
  origin = getElementRects(origin)

  nodeList.forEach(el => {
    const rects = getElementRects(el)
    if (rects === null) {
      return
    }

    rects.bottom -= origin.y
    rects.left -= origin.x
    rects.right -= origin.x
    rects.top -= origin.y
    rects.x -= origin.x
    rects.y -= origin.y

    const distance = Math.sqrt(rects.x * rects.x + rects.y * rects.y)

    elements.push({ el, ...rects, distance })
  })

  return elements
}

const findTarget = (el, key, root) => {
  const elements = augmentElementRects(focusableElements(root), el)
  if (key in KEY_BINDINGS) {
    const { el } = KEY_BINDINGS[key](elements).reduce(
      (closest, el) => {
        return el.distance < closest.distance ? el : closest
      },
      { distance: Infinity }
    )

    return el
  }
  return null
}

export default {
  mounted(element) {
    const handler = evt => {
      const target = findTarget(evt.target, evt.key, element)

      if (target) {
        evt.preventDefault()
        evt.stopPropagation()
        target.focus()
      }
    }
    element.addEventListener('keydown', handler)
  },
}
vScrolllock.js
export default {
  mounted() {
    document.documentElement.style.overflow = 'hidden'
  },
  unmounted() {
    document.documentElement.style.overflow = ''
  },
}