overlay--slider.html
<div class="container">
  <h2>Background Content</h2>
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p>
  <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident.</p>
  <div class="actions">
    <button
      class="button"
      @click="
        $refs.overlay.showModal();
        $nextTick(() => $refs.overlay.querySelector('swiper-container')?.swiper?.update());
      "
    >
      Open Overlay Slider
    </button>
  </div>
</div>

<dialog
  class="overlay -slider webOnly"
  ref="overlay"
  @toggle="
    if ($event.target.open) {
      $nextTick(() => $event.target.querySelector('swiper-container')?.swiper?.update());
    }
  "
>
  <button class="overlay__close webOnly" aria-label="Close" @click="$refs.overlay.close()">
    <svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
      <rect x="32" y="34" width="2" height="48" rx="1" transform="rotate(-45 32 34)" fill="#F2E5FF" />
      <rect x="66" y="32" width="2" height="48" rx="1" transform="rotate(45 66 32)" fill="#F2E5FF" />
    </svg>
  </button>
  <div class="overlay__slider">
    <button type="button" class="overlay__nav button -circle -prev" aria-label="Previous slide" @click="$refs.overlay.querySelector('swiper-container')?.swiper?.slidePrev()">
      <svg class="button__icon" viewBox="0 0 18 18" aria-hidden="true">
        <use href="/main-icons-sprite.svg#chevron-left" />
      </svg>
    </button>
    <p-slider slides-per-view="1" space-between="0" keyboard="true" watch-overflow="false" loop="true" aria-label="Feature options">
      <swiper-slide>
        <article class="overlay__panel">
          <div class="overlay__body">
            <div class="overlay__intro">
              <p class="overlay__eyebrow">Feature</p>
              <h2 class="overlay__title">Component Options</h2>
              <p class="overlay__description">Donec sit amet vestibulum turpis. Ut eget nibh pretium ipsum auctor pharetra. Curabitur maximus tristique pellentesque. Etiam pulvinar, tortor sed finibus scelerisque, neque libero vehicula neque, ut molestie mauris risus ac dui. Quisque vel imperdiet leo. Eget nibh pretium ipsum auctor pharetra.</p>
            </div>
            <a class="overlay__cta button" href="#">Default Button</a>
          </div>
          <div class="overlay__imageWrapper">
            <img class="overlay__image" src="https://picsum.photos/seed/overlay-slide-1/800/800" alt="" />
          </div>
        </article>
      </swiper-slide>
      <swiper-slide>
        <article class="overlay__panel">
          <div class="overlay__body">
            <div class="overlay__intro">
              <p class="overlay__eyebrow">Feature</p>
              <h2 class="overlay__title">Flexible Layouts</h2>
              <p class="overlay__description">Suspendisse ac blandit nisi, non tempor purus. Pellentesque ornare tellus molestie quam pretium aliquet. Mauris eu est at velit cursus rutrum non sit amet turpis.</p>
            </div>
            <a class="overlay__cta button" href="#">Learn More</a>
          </div>
          <div class="overlay__imageWrapper">
            <img class="overlay__image" src="https://picsum.photos/seed/overlay-slide-2/800/800" alt="" />
          </div>
        </article>
      </swiper-slide>
      <swiper-slide>
        <article class="overlay__panel">
          <div class="overlay__body">
            <div class="overlay__intro">
              <p class="overlay__eyebrow">Feature</p>
              <h2 class="overlay__title">Accessible Controls</h2>
              <p class="overlay__description">Quisque vel imperdiet leo. Eget nibh pretium ipsum auctor pharetra. Curabitur maximus tristique pellentesque. Etiam pulvinar, tortor sed finibus scelerisque.</p>
            </div>
            <a class="overlay__cta button" href="#">Get Started</a>
          </div>
          <div class="overlay__imageWrapper">
            <img class="overlay__image" src="https://picsum.photos/seed/overlay-slide-3/800/800" alt="" />
          </div>
        </article>
      </swiper-slide>
    </p-slider>
    <button type="button" class="overlay__nav button -circle -next" aria-label="Next slide" @click="$refs.overlay.querySelector('swiper-container')?.swiper?.slideNext()">
      <svg class="button__icon" viewBox="0 0 18 18" aria-hidden="true">
        <use href="/main-icons-sprite.svg#chevron-right" />
      </svg>
    </button>
  </div>
</dialog>
index.scss
@use '@/core' as *;

.overlay {
  $b: &;

  background: rgb(13 0 26 / 0.7);
  border: 0;
  border-radius: 0;
  box-shadow: none;
  display: grid;
  height: 100%;
  inset: 0;
  justify-items: center;
  max-height: 100%;
  max-width: 100%;
  padding: var(--root-gap);
  place-content: center;
  position: fixed;
  width: 100%;

  &::backdrop {
    backdrop-filter: blur(12px);
    background: rgb(13 0 26 / 0.7);
  }

  &:not([open]) {
    display: none;
  }

  &__close {
    appearance: none;
    background: transparent;
    border: 0;
    color: #ffffff;
    cursor: pointer;
    height: 5.25rem;
    padding: 0;
    position: absolute;
    right: 1rem;
    top: 1rem;
    transition:
      opacity var(--root-ease-out-fast),
      transform var(--root-ease-out-fast);
    width: 5.25rem;
    z-index: 1;

    &:hover {
      opacity: 0.8;
      transform: scale(1.1);
    }
  }

  &__content {
    display: grid;
    gap: 1rem;
    justify-items: center;
    max-height: calc(100vh - var(--root-gap) * 2);
    max-width: min(90vw, 56rem);
  }

  &__figure {
    display: grid;
    gap: 1rem;
    justify-items: center;
    margin: 0;
  }

  &__image {
    border-radius: 0.25rem;
    box-shadow: var(--root-box-shadow-high);
    display: block;
    height: auto;
    max-height: calc(100vh - 8rem);
    max-width: 100%;
    object-fit: contain;
  }

  &__caption {
    color: rgb(255 255 255 / 0.9);
    font-size: 1rem;
    text-align: center;
  }

  /**
   * Two-column bio / profile card variant (image + text).
   * Keeps the fullscreen dark overlay; the white card lives in __panel.
   */
  &.-card {
    #{$b}__panel {
      --overlay-card-max-height: calc(100vh - var(--root-gap) * 2);
      --overlay-card-width: min(81.25rem, calc(100vw - var(--root-gap) * 2), calc(var(--overlay-card-max-height) * 2));

      background: var(--root-background-color);
      border-radius: 1rem;
      box-shadow: var(--root-box-shadow-med);
      color: var(--root-color);
      display: grid;
      grid-template-columns: 1fr;
      max-height: var(--overlay-card-max-height);
      overflow: auto;
      position: relative;
      width: min(100%, var(--overlay-card-width));

      @include at(md) {
        grid-template-columns: 1fr 1fr;
        height: min(calc(var(--overlay-card-width) / 2), var(--overlay-card-max-height));
        overflow: hidden;
        width: var(--overlay-card-width);
      }
    }

    #{$b}__close {
      color: var(--color-blue);
      height: 1.125rem;
      right: 1.5rem;
      top: 1.5rem;
      width: 1.125rem;

      svg {
        display: block;
        height: 100%;
        width: 100%;
      }
    }

    #{$b}__imageWrapper {
      background: var(--accent-color-50);
      min-height: 0;
      padding: 2rem;

      @include at(md) {
        display: grid;
      }
    }

    #{$b}__image {
      aspect-ratio: 1;
      box-shadow: none;
      display: block;
      height: auto;
      max-height: 100%;
      object-fit: cover;
      width: 100%;

      @include at(md) {
        height: 100%;
      }
    }

    #{$b}__body {
      display: grid;
      gap: 1.5rem;
      min-height: 0;
      padding: 1.5rem 2rem 2rem;

      @include at(md) {
        display: flex;
        flex-direction: column;
        gap: 4rem;
        overflow: hidden auto;
      }
    }

    #{$b}__intro {
      display: grid;
      gap: 1rem;

      @include at(md) {
        margin-top: auto;
      }
    }

    #{$b}__title {
      font-size: var(--root-font-size-h1);
      font-weight: 700;
      line-height: 1.125;
      margin: 0;
    }

    #{$b}__subtitle {
      color: var(--root-color-headings);
      font-size: var(--root-font-size-lg);
      line-height: 1.5;
      margin: 0;
    }

    #{$b}__title,
    #{$b}__subtitle {
      --root-color-headings: #333333;
    }

    #{$b}__description {
      line-height: 1.5;
      margin: 0;
    }

    #{$b}__social {
      --accent-color: var(--color-blue);

      align-items: center;
      gap: 3rem;
      margin-bottom: 0;

      .button.-square {
        height: 2rem;
        width: 2rem;
      }

      .button__icon {
        display: block;
        height: 2rem;
        margin: 0;
        width: 2rem;
      }
    }

    #{$b}__quote {
      box-shadow: none;
      display: grid;
      gap: 1rem;
      margin: 0;
      padding-left: 0;
    }

    #{$b}__quoteIcon {
      --icon-color: var(--accent-color);

      display: block;
      height: 2.75rem;
      width: 3rem;
    }

    #{$b}__quoteText {
      font-size: 1.25rem;
      line-height: 1.5;
      margin: 0;

      @include at(md) {
        font-size: var(--root-font-size-lg);
      }
    }
  }

  /**
   * Slider variant: centered feature card with side prev/next controls.
   * Each slide is text + CTA | square image (image fills card height).
   */
  &.-slider {
    --overlay-slider-nav-size: 3rem;
    --overlay-slider-nav-gap: 1.5rem;

    box-sizing: border-box;
    max-width: 100%;
    overflow-x: hidden;

    #{$b}__slider {
      box-sizing: border-box;
      display: grid;
      gap: 0.75rem;
      grid-template-areas:
        "slide slide slide"
        ". prev next";
      grid-template-columns: 1fr auto auto;
      max-width: 100%;
      width: min(100%, calc(67.375rem + (var(--overlay-slider-nav-size) + var(--overlay-slider-nav-gap)) * 2));

      @include at(md) {
        align-items: center;
        gap: var(--overlay-slider-nav-gap);
        grid-template-areas: "prev slide next";
        grid-template-columns: var(--overlay-slider-nav-size) minmax(0, 1fr) var(--overlay-slider-nav-size);
      }
    }

    #{$b}__nav {
      --accent-color: var(--color-blue);
      --accent-color-text: #ffffff;

      align-items: center;
      display: inline-flex;
      flex-shrink: 0;
      height: var(--overlay-slider-nav-size);
      justify-content: center;
      padding: 0;
      width: var(--overlay-slider-nav-size);

      .button__icon {
        height: 0.75rem;
        margin: 0;
        width: 0.75rem;
      }

      &.-prev {
        grid-area: prev;
      }

      &.-next {
        grid-area: next;
      }
    }

    swiper-container {
      display: block;
      grid-area: slide;
      max-width: 100%;
      min-width: 0;
      overflow: hidden;
      width: 100%;
    }

    swiper-container::part(container) {
      overflow: hidden;
    }

    swiper-slide {
      box-sizing: border-box;
      height: auto;
      max-width: 100%;
      width: 100%;
    }

    swiper-container::part(pagination),
    swiper-container::part(button-prev),
    swiper-container::part(button-next) {
      display: none;
    }

    #{$b}__panel {
      background: var(--root-background-color);
      border-radius: 1rem;
      box-shadow: var(--root-box-shadow-med);
      color: var(--root-color);
      display: grid;
      grid-template-columns: 1fr;
      max-height: calc(100vh - var(--root-gap) * 2);
      max-width: 100%;
      overflow: hidden;
      width: 100%;

      @include at(lg) {
        grid-template-columns: 1fr auto;
        height: min(26.75rem, calc(100vh - var(--root-gap) * 2));
      }
    }

    #{$b}__body {
      display: grid;
      gap: 2rem;
      padding: 2rem;

      @include at(md) {
        align-content: center;
        overflow: hidden auto;
      }
    }

    #{$b}__intro {
      display: grid;
      gap: 1rem;
    }

    #{$b}__eyebrow {
      color: var(--accent-color);
      font-size: 1rem;
      font-weight: 700;
      line-height: 1;
      margin: 0;
      text-transform: uppercase;
    }

    #{$b}__title {
      color: var(--root-color-headings);
      font-size: var(--root-font-size-h2);
      font-weight: 700;
      line-height: 1.125;
      margin: 0;
    }

    #{$b}__description {
      line-height: 1.5;
      margin: 0;
    }

    #{$b}__cta {
      --accent-color: var(--color-blue);

      justify-self: start;
    }

    #{$b}__imageWrapper {
      min-height: 0;

      @include at(lg) {
        aspect-ratio: 1;
        height: 100%;
        width: auto;
      }
    }

    #{$b}__image {
      aspect-ratio: 1;
      border-radius: 0;
      box-shadow: none;
      display: block;
      height: auto;
      object-fit: cover;
      width: 100%;

      @include at(lg) {
        height: 100%;
        width: 100%;
      }
    }
  }
}
PSlider.vue
<script setup>
import { register } from 'swiper/element/bundle'
import { ref } from 'vue'

register()

const props = defineProps({
  ariaLabel: {
    type: String,
    default: undefined,
  },
  ariaLabelledby: {
    type: String,
    default: undefined,
  },
  autoplay: {
    type: [Boolean, Object],
    default: undefined,
  },
})

const isPlaying = ref(!!props.autoplay)

let swiperInstance = null

const toggleAutoplay = () => {
  if (!swiperInstance) return

  if (isPlaying.value) {
    swiperInstance.autoplay.stop()
    swiperInstance.wrapperEl.setAttribute('aria-live', 'polite')
  } else {
    swiperInstance.autoplay.start()
    swiperInstance.wrapperEl.setAttribute('aria-live', 'off')
  }
  isPlaying.value = !isPlaying.value
}

const updateActiveBullet = (swiper) => {
  if (!swiper.pagination?.bullets) return
  swiper.pagination.bullets.forEach((bullet) => {
    bullet.setAttribute('aria-disabled', bullet.classList.contains('swiper-pagination-bullet-active') ? 'true' : 'false')
  })
}

const init = e => {
  swiperInstance = e.detail[0]

  // The following code is for slider accessibility not covered by Swiper's a11y module.
  // Please see the slider README.md or the W3C carousel pattern for more information:
  // README: ../../../styles/organisms/slider/README.md
  // W3C: https://www.w3.org/WAI/ARIA/apg/patterns/carousel/

  // Swiper has no param for aria-labelledby; set it on the inner .swiper container
  // (swiperInstance.el), matching where the a11y module applies aria-label
  if (props.ariaLabelledby) {
    swiperInstance.el.setAttribute('aria-labelledby', props.ariaLabelledby)
    swiperInstance.el.removeAttribute('aria-label')
  }

  swiperInstance.wrapperEl.setAttribute('aria-atomic', 'false')

  // Pause autoplay on user interaction (focus or intentional click) while playing.
  // Listens on both the host (slotted play button) and swiper.el (shadow DOM controls).
  // Skips the play/pause button so clicking it to resume does not immediately re-pause.
  if (props.autoplay) {
    const stopAutoplay = () => {
      if (!isPlaying.value) return
      swiperInstance.autoplay.stop()
      swiperInstance.wrapperEl.setAttribute('aria-live', 'polite')
      isPlaying.value = false
    }

    const pauseAutoplayOnFocus = (event) => {
      if (event.target.closest?.('.slider__playPause')) return
      stopAutoplay()
    }

    // Pause on click of a slide, pagination bullet, or prev/next nav button.
    const pauseAutoplayOnClick = (event) => {
      if (!event.target.closest('.swiper-slide, .swiper-pagination-bullet, .swiper-button-prev, .swiper-button-next')) return
      stopAutoplay()
    }

    e.target.addEventListener('focusin', pauseAutoplayOnFocus)
    swiperInstance.el.addEventListener('focusin', pauseAutoplayOnFocus)
    swiperInstance.el.addEventListener('click', pauseAutoplayOnClick)
  }

  if (swiperInstance.pagination?.el) {
    swiperInstance.pagination.el.setAttribute('role', 'group')
    swiperInstance.pagination.el.setAttribute('aria-label', 'Choose a slide')
  }

  // Active bullet aria-disabled per W3C carousel pattern (Swiper sets aria-current only)
  updateActiveBullet(swiperInstance)
  swiperInstance.on('slideChange', updateActiveBullet)
  swiperInstance.on('paginationUpdate', updateActiveBullet)
}
</script>
<template>
  <swiper-container
    v-bind="$attrs"
    :autoplay="autoplay"
    a11y-container-role="group"
    a11y-container-role-description-message="carousel"
    :a11y-container-message="ariaLabel"
    a11y-item-role-description-message="slide"
    @swiperafterinit="init"
  >
    <button
      v-if="autoplay"
      slot="container-start"
      class="slider__playPause button -circle"
      :aria-label="isPlaying ? 'Stop slide rotation' : 'Start slide rotation'"
      @click="toggleAutoplay"
    >
      <svg class="button__icon" aria-hidden="true">
        <use :href="isPlaying ? '/main-icons-sprite.svg#pause' : '/main-icons-sprite.svg#play'" />
      </svg>
    </button>
    <slot />
  </swiper-container>
</template>