<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 : 40 vw ;
}
. threeD-button-set {
--y :;
--x :;
--distance : 1 px ;
--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 : 10 deg ;
--_max-rotateX : 15 deg ;
--_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.5 vh ;
/* 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 1 px 1 px var ( -- _btn -text-shadow );
font-size : min ( 5 vmin , 3 rem );
font-family : Audiowide ;
padding-block : .75 ch ;
padding-inline : 2 ch ;
border-radius : 5 px 20 px ;
/* 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 : 15 px ;
background-color : var ( -- _btn -bg-hover );
/* if motion is OK, setup transitions and increase distance */
@media (--motionOK) {
--distance : 3 vmax ;
transition-timing-function : var ( -- _bounce -ease );
transition-duration : .4 s ;
&::after { transition-duration : .5 s }
& :: before { transition-duration : .3 s }
}
}
& :: after ,
& :: before {
/* create empty element */
content : '' ;
opacity : .8 ;
/* cover the parent (button) */
position : absolute ;
inset : 0 ;
/* style the element for border accents */
border : 1 px solid var ( --theme );
border-radius : 5 px 20 px ;
/* 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 .1 s ease-out ;
}
}
/* exceptions for one of the pseudo elements */
/* this will be pushed back and have a thicker border */
& :: before {
border-width : 3 px ;
transform : translateZ ( calc ( var ( --distance ) / 3 * -1 ));
/* in dark mode, it glows! */
@media (--dark) {
box-shadow :
0 0 25 px var ( --theme ),
inset 0 0 25 px var ( --theme );
}
}
@ media ( --motionOK ) {
will-change : transform ;
transition :
transform . 2s ease ,
background-color . 5s ease ;
}
}
@ keyframes rotate-y {
50 % {
transform : rotateY ( 15 deg ) rotateX ( -6 deg );
}
}
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 }
}
Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 4.0 License , and code samples are licensed under the Apache 2.0 License . For details, see the Google Developers Site Policies . Java is a registered trademark of Oracle and/or its affiliates.
Last updated 2023-07-05 UTC.
[[["Easy to understand","easyToUnderstand","thumb-up"],["Solved my problem","solvedMyProblem","thumb-up"],["Other","otherUp","thumb-up"]],[["Missing the information I need","missingTheInformationINeed","thumb-down"],["Too complicated / too many steps","tooComplicatedTooManySteps","thumb-down"],["Out of date","outOfDate","thumb-down"],["Samples / code issue","samplesCodeIssue","thumb-down"],["Other","otherDown","thumb-down"]],["Last updated 2023-07-05 UTC."],[],[]]