Présentation de base sur la création d'un menu de jeu 3D adaptatif, responsive et accessible.
Dans cet article, je vais vous expliquer comment créer un composant de menu de jeu 3D. Essayez la démonstration.
Si vous préférez les vidéos, voici une version YouTube de cet article :
Présentation
Les jeux vidéo présentent souvent aux utilisateurs un menu créatif et inhabituel, animé et en espace 3D. Cette technique est souvent utilisée dans les nouveaux jeux de RA/RV pour donner l'impression que le menu flotte dans l'espace. Aujourd'hui, nous allons recréer les éléments essentiels de cet effet, mais avec l'élégance supplémentaire d'un jeu de couleurs adaptatif et des options pour les utilisateurs qui préfèrent réduire les mouvements.
HTML
Un menu de jeu est une liste de boutons. La meilleure façon de représenter cela en HTML est la suivante :
<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>
Une liste de boutons sera bien annoncée aux technologies de lecteur d'écran et fonctionnera sans JavaScript ni CSS.

CSS
La mise en forme de la liste de boutons se décompose en plusieurs étapes générales :
- Configurer des propriétés personnalisées
- Mise en page Flexbox.
- Bouton personnalisé avec des pseudo-éléments décoratifs.
- Placer des éléments dans l'espace 3D.
Présentation des propriétés personnalisées
Les propriétés personnalisées permettent de clarifier les valeurs en donnant des noms significatifs à des valeurs qui semblent aléatoires, en évitant le code répété et en partageant les valeurs entre les enfants.
Vous trouverez ci-dessous des requêtes média enregistrées en tant que variables CSS, également appelées média personnalisé. Elles sont globales et seront utilisées dans différents sélecteurs pour que le code reste concis et lisible. Le composant de menu du jeu utilise les préférences de mouvement, le jeu de couleurs du système et les fonctionnalités de gamme de couleurs de l'écran.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --HDcolor (dynamic-range: high);
Les propriétés personnalisées suivantes gèrent le jeu de couleurs et contiennent les valeurs de position de la souris pour rendre le menu du jeu interactif au survol. Nommer les propriétés personnalisées permet de rendre le code plus lisible, car cela révèle le cas d'utilisation de la valeur ou un nom convivial pour le résultat de la valeur.
.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);
@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);
}
}
}
Arrière-plans coniques pour les thèmes clair et sombre
Le thème clair présente un dégradé conique vibrant allant de cyan à deeppink, tandis que le thème sombre présente un dégradé conique sombre et subtil. Pour en savoir plus sur ce qu'il est possible de faire avec les dégradés coniques, consultez conic.style.
html {
background: conic-gradient(at -10% 50%, deeppink, cyan);
@media (--dark) {
background: conic-gradient(at -10% 50%, #212529, 50%, #495057, #212529);
}
}
Activer la perspective 3D
Pour que des éléments existent dans l'espace 3D d'une page Web, une fenêtre d'affichage avec perspective doit être initialisée. J'ai choisi de placer la perspective sur l'élément body et j'ai utilisé des unités de fenêtre d'affichage pour créer le style qui me plaisait.
body {
perspective: 40vw;
}
Il s'agit du type d'impact que peut avoir une perspective.
Appliquer un style à la liste de boutons <ul>
Cet élément est responsable de la mise en page globale de la macro de liste de boutons, ainsi que d'une fiche flottante interactive et en 3D. Voici comment procéder.
Mise en page du groupe de boutons
Flexbox peut gérer la mise en page du conteneur. Modifiez la direction par défaut du flex de lignes à colonnes avec flex-direction et assurez-vous que chaque élément a la taille de son contenu en remplaçant stretch par start pour align-items.
.threeD-button-set {
/* remove <ul> margins */
margin: 0;
/* vertical rag-right layout */
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2.5vh;
}
Ensuite, définissez le conteneur comme contexte d'espace 3D et configurez les fonctions CSS clamp() pour vous assurer que la carte ne pivote pas au-delà des rotations lisibles. Notez que la valeur intermédiaire de la fonction clamp est une propriété personnalisée. Les valeurs --x et --y seront définies à partir de JavaScript lors d'une interaction de la souris ultérieure.
.threeD-button-set {
…
/* 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)
)
)
;
}
Ensuite, si le mouvement est acceptable pour l'utilisateur qui visite la page, ajoutez un indice au navigateur indiquant que la transformation de cet élément changera constamment avec will-change.
Activez également l'interpolation en définissant un transition sur les transformations. Cette transition se produit lorsque la souris interagit avec la carte, ce qui permet des transitions fluides vers les changements de rotation. L'animation est une animation continue qui montre l'espace 3D dans lequel se trouve la carte, même si une souris ne peut pas interagir avec le composant ou ne le fait pas.
@media (--motionOK) {
.threeD-button-set {
/* browser hint so it can be prepared and optimized */
will-change: transform;
/* transition transform style changes and run an infinite animation */
transition: transform .1s ease;
animation: rotate-y 5s ease-in-out infinite;
}
}
L'animation rotate-y ne définit l'image clé intermédiaire qu'à 50%, car le navigateur définira par défaut 0% et 100% sur le style par défaut de l'élément. Il s'agit d'une abréviation pour les animations qui alternent et qui doivent commencer et se terminer à la même position. C'est un excellent moyen d'exprimer des animations alternées infinies.
@keyframes rotate-y {
50% {
transform: rotateY(15deg) rotateX(-6deg);
}
}
Appliquer un style aux éléments <li>
Chaque élément de la liste (<li>) contient le bouton et ses éléments de bordure. Le style display est modifié pour que l'élément n'affiche pas de ::marker. Le style position est défini sur relative afin que les pseudo-éléments du bouton à venir puissent se positionner dans toute la zone consommée par le bouton.
.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;
}

Appliquer un style aux éléments <button>
Il peut être difficile de styliser des boutons, car il existe de nombreux états et types d'interaction à prendre en compte. Ces boutons deviennent rapidement complexes en raison de l'équilibrage des pseudo-éléments, des animations et des interactions.
Styles <button> initiaux
Vous trouverez ci-dessous les styles de base qui prendront en charge les autres états.
.threeD-button-set button {
/* strip out default button styles */
appearance: none;
outline: none;
border: none;
/* bring in brand styles via props */
background-color: var(--_btn-bg);
color: var(--_btn-text);
text-shadow: 0 1px 1px var(--_btn-text-shadow);
/* large text rounded corner and padded*/
font-size: 5vmin;
font-family: Audiowide;
padding-block: .75ch;
padding-inline: 2ch;
border-radius: 5px 20px;
}

Pseudo-éléments de bouton
Les bordures du bouton ne sont pas des bordures traditionnelles, mais des pseudo-éléments positionnés de manière absolue avec des bordures.

Ces éléments sont essentiels pour mettre en avant la perspective 3D établie. L'un de ces pseudo-éléments sera éloigné du bouton, tandis que l'autre sera rapproché de l'utilisateur. L'effet est plus visible sur les boutons en haut et en bas.
.threeD-button button {
…
&::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;
}
/* exceptions for one of the pseudo elements */
/* this will be pushed back (3x) and have a thicker border */
&::before {
border-width: 3px;
/* in dark mode, it glows! */
@media (--dark) {
box-shadow:
0 0 25px var(--theme),
inset 0 0 25px var(--theme);
}
}
}
Styles de transformation 3D
La valeur de transform-style est définie sur preserve-3d afin que les enfants puissent s'espacer sur l'axe z. La propriété transform est définie sur la propriété personnalisée --distance, qui sera augmentée lors du survol et de la sélection.
.threeD-button-set button {
…
transform: translateZ(var(--distance));
transform-style: preserve-3d;
&::after {
/* pull forward in Z space with a 3x multiplier */
transform: translateZ(calc(var(--distance) / 3));
}
&::before {
/* push back in Z space with a 3x multiplier */
transform: translateZ(calc(var(--distance) / 3 * -1));
}
}
Styles d'animation conditionnels
Si l'utilisateur accepte le mouvement, le bouton indique au navigateur que la propriété de transformation doit être prête à être modifiée et qu'une transition est définie pour les propriétés transform et background-color. Notez la différence de durée. Je trouve que cela crée un bel effet subtil et décalé.
.threeD-button-set button {
…
@media (--motionOK) {
will-change: transform;
transition:
transform .2s ease,
background-color .5s ease
;
&::before,
&::after {
transition: transform .1s ease-out;
}
&::after { transition-duration: .5s }
&::before { transition-duration: .3s }
}
}
Styles d'interaction avec le pointeur et la sélection
L'objectif de l'animation d'interaction est de répartir les calques qui composent le bouton plat. Pour ce faire, définissez la variable --distance sur 1px au départ. Le sélecteur affiché dans l'exemple de code suivant vérifie si le bouton est pointé ou sélectionné par un appareil qui doit afficher un indicateur de sélection, et non activé. Si c'est le cas, il applique le CSS pour effectuer les opérations suivantes :
- Appliquez la couleur d'arrière-plan au passage de la souris.
- Augmentez la distance .
- Ajoutez un effet de rebond.
- Échelonnez les transitions des pseudo-éléments.
.threeD-button-set button {
…
&: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 }
}
}
}
La perspective 3D était toujours très intéressante pour la préférence de mouvement reduced.
Les éléments en haut et en bas montrent l'effet de manière subtile.
Petites améliorations avec JavaScript
L'interface est déjà utilisable avec un clavier, un lecteur d'écran, une manette de jeu, un écran tactile et une souris, mais nous pouvons ajouter quelques touches de JavaScript pour faciliter certains scénarios.
Prise en charge des touches fléchées
La touche de tabulation est un bon moyen de parcourir le menu, mais je m'attendrais à ce que le pavé directionnel ou les joysticks déplacent la sélection sur une manette de jeu. La bibliothèque roving-ux, souvent utilisée pour les interfaces de défi GUI, gérera les touches fléchées pour nous. Le code ci-dessous indique à la bibliothèque de piéger la sélection dans .threeD-button-set et de la transférer aux enfants du bouton.
import {rovingIndex} from 'roving-ux'
rovingIndex({
element: document.querySelector('.threeD-button-set'),
target: 'button',
})
Interaction de parallaxe de la souris
Le suivi de la souris et l'inclinaison du menu sont destinés à imiter les interfaces de jeux vidéo en AR et VR, où vous pouvez avoir un pointeur virtuel au lieu d'une souris. Il peut être amusant de voir des éléments hyper sensibles au pointeur.
Comme il s'agit d'une petite fonctionnalité supplémentaire, nous allons placer l'interaction derrière une requête concernant la préférence de mouvement de l'utilisateur. De plus, lors de la configuration, stockez le composant de liste de boutons en mémoire avec querySelector et mettez en cache les limites de l'élément dans menuRect. Utilisez ces limites pour déterminer le décalage de rotation appliqué à la carte en fonction de la position de la souris.
const menu = document.querySelector('.threeD-button-set')
const menuRect = menu.getBoundingClientRect()
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
Ensuite, nous avons besoin d'une fonction qui accepte les positions x et y de la souris et renvoie une valeur que nous pouvons utiliser pour faire pivoter la carte. La fonction suivante utilise la position de la souris pour déterminer de quel côté de la boîte elle se trouve et dans quelle mesure. Le delta est renvoyé par la fonction.
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}
}
Enfin, observez le mouvement de la souris, transmettez la position à notre fonction getAngles() et utilisez les valeurs delta comme styles de propriétés personnalisés. J'ai divisé par 20 pour augmenter le delta et le rendre moins instable. Il existe peut-être une meilleure façon de le faire. Si vous vous souvenez du début, nous avons placé les props --x et --y au milieu d'une fonction clamp(). Cela empêche la position de la souris de faire pivoter la carte de manière excessive, au point de la rendre illisible.
if (motionOK) {
window.addEventListener('mousemove', ({target, clientX, clientY}) => {
const {dx,dy} = getAngles(clientX, clientY)
menu.attributeStyleMap.set('--x', `${dy / 20}deg`)
menu.attributeStyleMap.set('--y', `${dx / 20}deg`)
})
}
Traductions et itinéraires
Lors du test du menu du jeu dans d'autres modes d'écriture et langues, nous avons rencontré un problème.
Les éléments <button> ont un style !important pour writing-mode dans la feuille de style de l'agent utilisateur. Cela signifiait que le code HTML du menu du jeu devait être modifié pour s'adapter à la conception souhaitée. En remplaçant la liste de boutons par une liste de liens, les propriétés logiques peuvent modifier la direction du menu, car les éléments <a> n'ont pas de style !important fourni par le navigateur.
Conclusion
Maintenant que vous savez comment j'ai fait, comment feriez-vous‽ 🙂 Pouvez-vous ajouter une interaction avec l'accéléromètre au menu, afin que l'inclinaison de votre téléphone fasse pivoter le menu ? Pouvons-nous améliorer l'expérience sans mouvement ?
Diversifions nos approches et découvrons toutes les façons de créer sur le Web. Créez une démo, tweetez-moi les liens et je l'ajouterai à la section des remix de la communauté ci-dessous !
Remix de la communauté
Aucun élément à afficher pour le moment.