Menu del gioco

Questo pattern mostra come creare un menu di gioco 3D accessibile con mouse e tastiera.

Articolo completo · Video su YouTube · Fonte su GitHub

<ul class="threeD-button-set">
 
<li><button>New Game</button></li>
 
<li><button>Continue</button></li>
 
<li><button>Online</button></li>
 
<li><button>Settings</button></li>
 
<li><button>Quit</button></li>
</ul>

        body
{
 
perspective: 40vw;
}

.threeD-button-set {
 
--y:;
 
--x:;
 
--distance: 1px;
 
--theme: hsl(180 100% 50%);
 
--theme-bg: hsl(180 100% 50% / 25%);
 
--theme-bg-hover: hsl(180 100% 50% / 40%);
 
--theme-text: white;
 
--theme-shadow: hsl(180 100% 10% / 25%);

 
--_max-rotateY: 10deg;
 
--_max-rotateX: 15deg;
 
--_btn-bg: var(--theme-bg);
 
--_btn-bg-hover: var(--theme-bg-hover);
 
--_btn-text: var(--theme-text);
 
--_btn-text-shadow: var(--theme-shadow);
 
--_bounce-ease: cubic-bezier(.5, 1.75, .75, 1.25);

 
/* remove
     margins */
     
    margin: 0;

     
    /* vertical rag-right layout */
     
    display: flex;
     
    flex-direction: column;
     
    align-items: flex-start;
     
    gap: 2.5vh;
     
     
    /* create 3D space context */
     
    transform-style: preserve-3d;

     
    /* clamped menu rotation to not be too extreme */
     
    transform:
        rotateY
    (
          clamp
    (
            calc
    (var(--_max-rotateY) * -1),
            var
    (--y),
            var
    (--_max-rotateY)
         
    )
       
    )
        rotateX
    (
          clamp
    (
            calc
    (var(--_max-rotateX) * -1),
            var
    (--x),
            var
    (--_max-rotateX)
         
    )
       
    )
     
    ;

     
    /* removes Safari focus ring on
       after button interaction */
       
      &:focus {
         
      outline: none;
       
      }
       
       
      @media (--motionOK) {
         
      will-change: transform;
         
      transition: transform .1s ease;
         
      animation: rotate-y 5s ease-in-out infinite;
       
      }

       
      @media (--dark) {
         
      --theme: hsl(255 53% 50%);
         
      --theme-bg: hsl(255 53% 71% / 25%);
         
      --theme-bg-hover: hsl(255 53% 50% / 40%);
         
      --theme-shadow: hsl(255 53% 10% / 25%);
       
      }

       
      @media (--HDcolor) {
         
      @supports (color: color(display-p3 0 0 0)) {
           
      --theme: color(display-p3 .4 0 .9);
         
      }
       
      }
      }

      .threeD-button-set > li {
       
      /* change display type from list-item */
       
      display: inline-flex;

       
      /* create context for button pseudos */
       
      position: relative;

       
      /* create 3D space context */
       
      transform-style: preserve-3d;
      }
       
      .threeD-button-set button {
       
      /* strip out default button styles */
       
      appearance: none;
       
      outline: none;
       
      border: none;
       
      -webkit-tap-highlight-color: transparent;
       
       
      /* bring in brand styles via props */
       
      background-color: var(--_btn-bg);
       
      color: var(--_btn-text);
       
      text-shadow: 0 1px 1px var(--_btn-text-shadow);

       
      font-size: min(5vmin, 3rem);
       
      font-family: Audiowide;
       
      padding-block: .75ch;
       
      padding-inline: 2ch;
       
      border-radius: 5px 20px;

       
      /* prepare for 3D perspective transforms */
       
      transform: translateZ(var(--distance));
       
      transform-style: preserve-3d;
       
       
      &:is(:hover, :focus-visible):not(:active) {
         
      /* subtle distance plus bg color change on hover/focus */
         
      --distance: 15px;
         
      background-color: var(--_btn-bg-hover);

         
      /* if motion is OK, setup transitions and increase distance */
         
      @media (--motionOK) {
           
      --distance: 3vmax;

           
      transition-timing-function: var(--_bounce-ease);
           
      transition-duration: .4s;

           
      &::after  { transition-duration: .5s }
           
      &::before { transition-duration: .3s }
         
      }
       
      }
       
       
      &::after,
       
      &::before {
         
      /* create empty element */
         
      content: '';
         
      opacity: .8;

         
      /* cover the parent (button) */
         
      position: absolute;
         
      inset: 0;

         
      /* style the element for border accents */
         
      border: 1px solid var(--theme);
         
      border-radius: 5px 20px;

         
      /* move in Z space with a multiplier */
         
      transform: translateZ(calc(var(--distance) / 3));

         
      /* if motion is OK, transition the Z space move */
         
      @media (--motionOK) {
           
      transition: transform .1s ease-out;  
         
      }
       
      }
       
       
      /* exceptions for one of the pseudo elements */
       
      /* this will be pushed back and have a thicker border */
       
      &::before {
         
      border-width: 3px;
         
      transform: translateZ(calc(var(--distance) / 3 * -1));

         
      /* in dark mode, it glows! */
         
      @media (--dark) {
           
      box-shadow:
             
      0 0 25px var(--theme),
              inset
      0 0 25px var(--theme);
         
      }
       
      }
       
       
      @media (--motionOK) {
         
      will-change: transform;
         
      transition:
            transform
      .2s ease,
            background-color
      .5s ease;
       
      }
      }

      @keyframes rotate-y {
       
      50% {
         
      transform: rotateY(15deg) rotateX(-6deg);
       
      }
      }
             

       
import {rovingIndex} from 'https://cdn.skypack.dev/roving-ux'
   
const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()

const { matches:motionOK } = window.matchMedia(
 
'(prefers-reduced-motion: no-preference)'
)

rovingIndex
({
  element
: document.querySelector('.threeD-button-set'),
  target
: 'button',
})

if (motionOK) {
  window
.addEventListener('mousemove', ({target, clientX, clientY}) => {
   
const {dx,dy} = getAngles(clientX, clientY)

    menu
.style.setProperty('--x', `${dy / 20}deg`)
    menu
.style.setProperty('--y', `${dx / 20}deg`)
 
})
}

const getAngles = (clientX, clientY) => {
 
const { x, y, width, height } = menuRect
 
 
const dx = clientX - (x + 0.5 * width)
 
const dy = clientY - (y + 0.5 * height)
 
 
return {dx,dy}
}