Various Analog Designs
Moba Analog
Design is inspired from the mobile game League of Legends, Wild Rift.
- html
- javascript
- css
<xyba-analog no-style class="moba-analog" trail track-from=".moba-background">
<img src="/rift-analog/overlay.svg" slot="overlay" class="overlay" loading="eager" />
<img src="/rift-analog/knob.svg" slot="knob" class="knob" loading="eager" />
<img src="/rift-analog/base.svg" slot="base" class="base" loading="eager" />
<img src="/rift-analog/base_overlay.svg" slot="base" class="base-overlay" loading="eager" />
</xyba-analog>
<div class="moba-background"></div>
const analog = document.querySelector('.moba-analog');
const base = analog.querySelector('[slot="base"]');
const overlay = analog.querySelector('[slot="overlay"]');
analog.addEventListener('change', (e) => {
let rotation = e.target.angleDegrees + -90;
if (!e.target.inDeadzone) {
base.style.rotate = `${rotation}deg`;
overlay.style.rotate = `${rotation}deg`;
} else {
base.style.rotate = "";
overlay.style.rotate = "";
}
});
.moba-analog {
--transition: .5s cubic-bezier(.23,1,.32,1);
width: 149px;
height: 149px;
z-index: 1;
.overlay {
width: calc(184px + 28px);
height: calc(184px + 28px);
/* Override responsive image styles from website */
max-width: none;
opacity: 0;
}
.base-overlay {
position: absolute;
top: 0;
left: 0;
z-index: 2;
rotate: none;
}
&:not([direction="none"]) {
.overlay {
opacity: 1;
transition: opacity var(--transition);
}
}
}
.moba-background {
position: absolute;
top: 0;
left: 0;
width: var(--demo-width);
height: 100%;
background-image: url(/img/moba.webp);
background-size: cover;
border-top-right-radius: 11px;
border-top-left-radius: 11px;
}
Nintendo 2DS D-Pad
Design and images from https://lnkd.itch.io/pbgba-skins
- html
- css
<xyba-analog no-style adapt class="ds-dpad" deadzone=".2">
<div slot="base" class="base">
<img class="dpad down" src="/2ds_buttons/cross_d.png" loading="eager" />
<img class="dpad down_left" src="/2ds_buttons/cross_dl.png" loading="eager" />
<img class="dpad down_right" src="/2ds_buttons/cross_dr.png" loading="eager" />
<img class="dpad left" src="/2ds_buttons/cross_l.png" loading="eager" />
<img class="dpad right" src="/2ds_buttons/cross_r.png" loading="eager" />
<img class="dpad up" src="/2ds_buttons/cross_u.png" loading="eager" />
<img class="dpad up_left" src="/2ds_buttons/cross_ul.png" loading="eager" />
<img class="dpad up_right" src="/2ds_buttons/cross_ur.png" loading="eager" />
<img class="dpad none" src="/2ds_buttons/cross.png" loading="eager" />
</div>
</xyba-analog>
.ds-dpad {
width: 140px;
height: 140px;
border-radius: 0;
.base {
position: relative;
width: 100%;
height: 100%;
}
.dpad {
opacity: 0;
width: 100%;
height: auto;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&[direction="none"] .none {
opacity: 1;
}
&[direction="up"] .up {
opacity: 1;
}
&[direction="up_right"] .up_right {
opacity: 1;
}
&[direction="right"] .right {
opacity: 1;
}
&[direction="down_right"] .down_right {
opacity: 1;
}
&[direction="down"] .down {
opacity: 1;
}
&[direction="down_left"] .down_left {
opacity: 1;
}
&[direction="left"] .left {
opacity: 1;
}
&[direction="up_left"] .up_left {
opacity: 1;
}
}
Playstation D-Pad
- html
- css
<xyba-analog no-style adapt class="playstation-dpad" deadzone=".2">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 160 160" slot="base">
<g id="Artboard" fill="currentColor" fill-opacity="0" fill-rule="evenodd" stroke="currentColor" stroke-width="2">
<path id="dpad-up" d="M23.136 8.4c-4.757 0-9.55.558-14.377 1.673a8.98 8.98 0 0 0-5.293 3.53 8.98 8.98 0 0 0-1.636 6.149L5.01 51.05a9 9 0 0 0 2.665 5.53l9.037 8.824A8.973 8.973 0 0 0 23 67.965c2.27 0 4.54-.853 6.288-2.56l9.037-8.826a9 9 0 0 0 2.666-5.53l3.183-31.334a8.98 8.98 0 0 0-1.634-6.145 8.98 8.98 0 0 0-5.286-3.532A62.272 62.272 0 0 0 23.136 8.4Z" transform="translate(57)"/>
<path id="dpad-right" d="M23.136 8.4c-4.757 0-9.55.558-14.377 1.673a8.98 8.98 0 0 0-5.293 3.53 8.98 8.98 0 0 0-1.636 6.149L5.01 51.05a9 9 0 0 0 2.665 5.53l9.037 8.824A8.973 8.973 0 0 0 23 67.965c2.27 0 4.54-.853 6.288-2.56l9.037-8.826a9 9 0 0 0 2.666-5.53l3.183-31.334a8.98 8.98 0 0 0-1.634-6.145 8.98 8.98 0 0 0-5.286-3.532A62.272 62.272 0 0 0 23.136 8.4Z" transform="rotate(90 51.5 108.5)"/>
<path id="dpad-down" d="M23.136 8.4c-4.757 0-9.55.558-14.377 1.673a8.98 8.98 0 0 0-5.293 3.53 8.98 8.98 0 0 0-1.636 6.149L5.01 51.05a9 9 0 0 0 2.665 5.53l9.037 8.824A8.973 8.973 0 0 0 23 67.965c2.27 0 4.54-.853 6.288-2.56l9.037-8.826a9 9 0 0 0 2.666-5.53l3.183-31.334a8.98 8.98 0 0 0-1.634-6.145 8.98 8.98 0 0 0-5.286-3.532A62.272 62.272 0 0 0 23.136 8.4Z" transform="rotate(-180 51.5 80)"/>
<path id="dpad-left" d="M23.136 8.4c-4.757 0-9.55.558-14.377 1.673a8.98 8.98 0 0 0-5.293 3.53 8.98 8.98 0 0 0-1.636 6.149L5.01 51.05a9 9 0 0 0 2.665 5.53l9.037 8.824A8.973 8.973 0 0 0 23 67.965c2.27 0 4.54-.853 6.288-2.56l9.037-8.826a9 9 0 0 0 2.666-5.53l3.183-31.334a8.98 8.98 0 0 0-1.634-6.145 8.98 8.98 0 0 0-5.286-3.532A62.272 62.272 0 0 0 23.136 8.4Z" transform="rotate(-90 51.5 51.5)"/>
</g>
</svg>
</xyba-analog>
.playstation-dpad {
width: 140px;
height: 140px;
border-radius: 0;
svg {
width: 100%;
height: 100%;
}
&[direction="left"], &[direction="up_left"], &[direction="down_left"] {
#dpad-left {
fill-opacity: 1;
}
}
&[direction="up_left"], &[direction="up"], &[direction="up_right"] {
#dpad-up {
fill-opacity: 1;
}
}
&[direction="up_right"], &[direction="right"], &[direction="down_right"] {
#dpad-right {
fill-opacity: 1;
}
}
&[direction="down_right"], &[direction="down"], &[direction="down_left"] {
#dpad-down {
fill-opacity: 1;
}
}
}
Steering wheel
- html
- css
- javascript
<xyba-analog class="steering-wheel" axis="x">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 148 148" slot="base" class="steering-wheel__base">
<g id="base-copy" fill="currentColor" fill-rule="evenodd" stroke="none" stroke-width="1">
<path id="Oval" fill-rule="nonzero" d="M74 0c40.87 0 74 33.13 74 74 0 23.816-11.25 45.004-28.727 58.54-12.05 9.333-27.06 8.591-43.384 8.523l-2.52-.003-1.274.005c-15.896.09-30.546 1.095-42.43-7.81C11.655 119.756 0 98.238 0 74 0 33.13 33.13 0 74 0Zm0 12c-34.242 0-62 27.758-62 62 0 19.584 9.127 37.628 24.383 49.29l.479.362c6.031 4.52 12.315 5.577 29.306 5.465l7.188-.057 2.891.005 4.619.026c18.033.05 24.702-1.115 31.059-6.039C127.01 111.37 136 93.44 136 74c0-34.242-27.758-62-62-62Z"/>
<path id="Oval" fill-rule="nonzero" d="M74 54c11.046 0 20 8.954 20 20s-8.954 20-20 20-20-8.954-20-20 8.954-20 20-20Zm0 10c-5.523 0-10 4.477-10 10s4.477 10 10 10 10-4.477 10-10-4.477-10-10-10Z"/>
<path id="Path-2" d="M14.23 50.131c16.959-5.327 22.356-15.687 25.116-31.251L24.31 25.544 14.23 50.131Z"/>
<path id="Path-2" d="M108.77 50.131c16.96-5.327 22.357-15.687 25.116-31.251l-15.035 6.664-10.081 24.587Z" transform="matrix(-1 0 0 1 242.656 0)"/>
<path id="Path-3" fill-rule="nonzero" d="m20.422 59.418-.607 2.349a6.5 6.5 0 0 0 3.725 7.595l27.12 11.67a1.5 1.5 0 0 1 .58 2.313l-16.574 20.79a6.5 6.5 0 0 0 .776 8.92l1.54 1.363a6.5 6.5 0 0 0 8.54.066l22.54-19.339a6.5 6.5 0 0 0 1.337-8.284l-2.176-3.617a6.501 6.501 0 0 0-.451-.655c-1.941-2.48-3.115-4.457-3.524-5.831-.39-1.305-.4-3.155.025-5.53a1.5 1.5 0 0 1 .036-.154l.88-3.021a5.572 5.572 0 0 0-4.262-7.023l-31.942-6.362a6.5 6.5 0 0 0-7.563 4.75Zm6.586.153 31.99 6.375a.572.572 0 0 1 .39.71l-.88 3.02c-.064.221-.116.444-.157.67-.552 3.083-.537 5.69.105 7.841.624 2.092 2.086 4.554 4.379 7.484a1.5 1.5 0 0 1 .104.151l2.176 3.617a1.5 1.5 0 0 1-.309 1.911l-22.54 19.339a1.5 1.5 0 0 1-1.97-.015l-1.54-1.363a1.5 1.5 0 0 1-.18-2.059l16.573-20.79a6.5 6.5 0 0 0-2.513-10.023l-27.12-11.67a1.5 1.5 0 0 1-.86-1.753l.607-2.349a1.5 1.5 0 0 1 1.745-1.096Z"/>
<path id="Path-3-Copy" fill-rule="nonzero" d="m77.662 59.418-.606 2.349a6.5 6.5 0 0 0 3.724 7.595l27.12 11.67a1.5 1.5 0 0 1 .58 2.313l-16.573 20.79a6.5 6.5 0 0 0 .775 8.92l1.54 1.363a6.5 6.5 0 0 0 8.54.066l22.54-19.339a6.5 6.5 0 0 0 1.338-8.284l-2.176-3.617a6.5 6.5 0 0 0-.451-.655c-1.942-2.48-3.115-4.457-3.525-5.831-.389-1.305-.4-3.155.026-5.53.009-.052.021-.103.036-.154l.88-3.021a5.572 5.572 0 0 0-4.263-7.023l-31.941-6.362a6.5 6.5 0 0 0-7.564 4.75Zm6.587.153 31.99 6.375a.572.572 0 0 1 .39.71l-.88 3.02c-.064.221-.116.444-.157.67-.552 3.083-.537 5.69.105 7.841.624 2.092 2.085 4.554 4.378 7.484a1.5 1.5 0 0 1 .105.151l2.176 3.617a1.5 1.5 0 0 1-.309 1.911l-22.54 19.339a1.5 1.5 0 0 1-1.97-.015l-1.541-1.363a1.5 1.5 0 0 1-.18-2.059l16.574-20.79a6.5 6.5 0 0 0-2.513-10.023l-27.12-11.67a1.5 1.5 0 0 1-.86-1.753l.607-2.349a1.5 1.5 0 0 1 1.745-1.096Z" transform="matrix(-1 0 0 1 204.42 0)"/>
</g>
</svg>
</xyba-analog>
.steering-wheel {
.steering-wheel__base {
width: 100%;
height: 100%;
opacity: .5;
}
&::part(knob) {
display: none;
}
}
const analog = document.querySelector('.steering-wheel');
const base = analog.querySelector('[slot="base"]');
analog.addEventListener('change', (e) => {
const rotation = (e.target.magnitude * Math.sign(e.target.value[0]) * 90);
base.style.transform = `rotate(${rotation}deg)`;
});
Warzone
Inspired by the touch controls design of Warzone Mobile.
- html
- javascript
- css
<xyba-analog class="warzone-analog" knob-offset="33%">
<div class="base" slot="base">
<svg class="direction-none" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
<defs>
<radialGradient id="base-a" cx="50%" cy="50%" r="100%" fx="50%" fy="50%">
<stop offset="0%" stop-color="currentColor" stop-opacity=".2"/>
<stop offset="100%" stop-color="transparent" stop-opacity="0"/>
</radialGradient>
</defs>
<circle cx="50" cy="50" r="49" fill="url(#base-a)" fill-rule="evenodd" stroke="currentColor" stroke-opacity=".489" stroke-width="2"/>
</svg>
<svg class="direction" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
<defs>
<radialGradient id="direction-b" cx="50%" cy="3.336%" r="74.851%" fx="50%" fy="3.336%" gradientTransform="rotate(-90.915 .494 .04) scale(1 1.39355)">
<stop offset="0%" stop-color="currentColor" stop-opacity=".232"/>
<stop offset="100%" stop-color="transparent" stop-opacity="0"/>
</radialGradient>
<linearGradient id="direction-a" x1="50%" x2="50%" y1="50%" y2="0%">
<stop offset="0%" stop-color="currentColor" stop-opacity="0"/>
<stop offset="100%" stop-color="currentColor" stop-opacity=".489"/>
</linearGradient>
</defs>
<circle cx="50" cy="50" r="49" fill="url(#direction-b)" fill-rule="evenodd" stroke="url(#direction-a)" stroke-width="2" />
</svg>
</div>
</xyba-analog>
const analog = document.querySelector('.warzone-analog');
const base = analog.querySelector('[slot="base"]');
analog.addEventListener('change', (e) => {
console.log(e.target.inDeadzone)
let rotation = e.target.angleDegrees + -90;
if (!e.target.inDeadzone) {
base.style.rotate = `${rotation}deg`;
} else {
base.style.rotate = "";
}
});
.warzone-analog {
background: none;
border: 0px;
.base {
width: 128px;
height: 128px;
position: relative;
}
.direction-none {
position: absolute;
top: 0;
left: 0;
z-index: 1;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity .2s;
}
.direction {
position: absolute;
top: 0;
left: 0;
z-index: 2;
opacity: 0;
width: 100%;
height: 100%;
}
&[direction="none"] .direction-none {
opacity: 1;
transition: none;
}
&:not([direction="none"]) .direction {
opacity: 1;
transition: opacity .2s;
}
}
Genesis
Digitized design inspired by the SEGA Genesis controller.
- html
- css
<xyba-analog no-style adapt class="genesis-analog" knob-offset="-28px">
<div class="base" slot="base">
<div class="dot arrow right"></div>
<div class="dot down_right"></div>
<div class="dot arrow down"></div>
<div class="dot down_left"></div>
<div class="dot arrow left"></div>
<div class="dot up_left"></div>
<div class="dot arrow up"></div>
<div class="dot up_right"></div>
<div class="inner"></div>
</div>
<div slot="knob" class="knob"></div>
</xyba-analog>
.genesis-analog {
--dot-distance: calc(100% / 8);
--dot-size: 8px;
--border-width: 24px;
--gap-width: 16px;
--transition: .25s cubic-bezier(.23,1,.32,1);
--knob-size: 48px;
width: 180px;
height: 180px;
.base {
box-sizing: border-box;
position: relative;
width: 100%;
height: 100%;
border: var(--border-width) solid color-mix(in srgb, currentColor 0%, transparent);
padding: var(--gap-width);
border-radius: 50%;
.inner {
width: 100%;
height: 100%;
border-radius: 99px;
background: color-mix(in srgb, currentColor 10%, transparent);
}
}
.knob {
width: var(--knob-size);
height: var(--knob-size);
background: currentColor;
opacity: .8;
transition: opacity var(--transition), translate var(--transition);
}
&[active] {
.knob {
opacity: 1;
transition: none;
}
}
.dot {
width: var(--dot-size);
height: var(--dot-size);
background: currentColor;
border-radius: 50%;
position: absolute;
offset-path: circle(calc(50% - (var(--border-width) / 2)));
box-sizing: border-box;
opacity: .1;
transition: opacity var(--transition);
}
.dot:nth-of-type(1) {
offset-distance: calc(var(--dot-distance) * 0);
}
.dot:nth-of-type(2) {
offset-distance: calc(var(--dot-distance) * 1);
}
.dot:nth-of-type(3) {
offset-distance: calc(var(--dot-distance) * 2);
}
.dot:nth-of-type(4) {
offset-distance: calc(var(--dot-distance) * 3);
}
.dot:nth-of-type(5) {
offset-distance: calc(var(--dot-distance) * 4);
}
.dot:nth-of-type(6) {
offset-distance: calc(var(--dot-distance) * 5);
}
.dot:nth-of-type(7) {
offset-distance: calc(var(--dot-distance) * 6);
}
.dot:nth-of-type(8) {
offset-distance: calc(var(--dot-distance) * 7);
}
.arrow {
border-radius: 0;
height: 16px;
}
&[direction="up"] .up {
opacity: 1;
}
&[direction="up_right"] .up_right {
opacity: 1;
}
&[direction="right"] .right {
opacity: 1;
}
&[direction="down_right"] .down_right {
opacity: 1;
}
&[direction="down"] .down {
opacity: 1;
}
&[direction="down_left"] .down_left {
opacity: 1;
}
&[direction="left"] .left {
opacity: 1;
}
&[direction="up_left"] .up_left {
opacity: 1;
}
}
Coastal
Bulky and sharp design, inspired by: https://coastalworld.com/
- html
- css
<div class="area">
<xyba-analog relocate persistent track-from=".area" knob-offset="50%" no-style class="coastal-analog">
<div slot="base" class="base">
<div class="dot"></div>
</div>
<div slot="knob" class="knob"></div>
</xyba-analog>
<div class="indicator"></div>
</div>
.area {
--transition: .25s cubic-bezier(.23,1,.32,1);
position: relative;
width: var(--demo-width);
height: 400px;
margin-block: calc(-1 * var(--custom-elements-demo-inner-padding-block));
border-top-left-radius: var(--custom-elements-demo-radius);
border-top-right-radius: var(--custom-elements-demo-radius);
}
.coastal-analog {
width: 140px;
height: 140px;
opacity: 0;
scale: .7;
transition: opacity var(--transition), scale var(--transition);
transition-delay: .3;
.base {
width: 100%;
height: 100%;
display: flex;
background-color: color-mix(in srgb, currentColor 10%, transparent);
}
.dot {
border-radius: 99px;
background-color: currentColor;
width: 4px;
height: 4px;
margin: auto;
}
.knob {
box-sizing: border-box;
width: 56px;
height: 56px;
border-radius: 999px;
border: 12px solid currentColor;
background: color-mix(in srgb, currentColor 20%, transparent);
}
&[active] {
scale: 1;
opacity: 1;
}
&:not([active]) {
[slot="knob"] {
transition: translate var(--transition);
}
}
}
.indicator {
pointer-events: none;
position: relative;
box-sizing: border-box;
width: 20px;
height: 20px;
border-radius: 99px;
border: 4px solid currentColor;
position: absolute;
bottom: 24px;
left: 50%;
translate: 0 -50%;
opacity: 1;
transition: opacity var(--transition);
&::after {
position: absolute;
top: -15px;
right: 50%;
width: 8px;
height: 8px;
content: "";
border-top: 4px solid #f2676f;
border-left: 4px solid #f2676f;
transform: translate(50%, -50%) rotate(45deg);
}
}
.coastal-analog[active] + .indicator {
opacity: 0;
}
Italy
A smooth and relaxed experience. Inspired by: https://dolceactivation.dolcegabbana.com/
- html
- css
<xyba-analog no-style knob-offset="-28px" adapt class="italy-analog">
<div slot="base" class="base">
<div class="arrow left"></div>
<div class="arrow up"></div>
<div class="arrow right"></div>
<div class="arrow down"></div>
</div>
<div slot="knob" class="knob"></div>
</xyba-analog>
.italy-analog {
--arrow-to-edge: 16px;
--arrow-size: 10px;
--transition: .25s cubic-bezier(.23,1,.32,1);
--knob-size: 18px;
--size: 150px;
width: var(--size);
height: var(--size);
transition: opacity var(--transition);
.base {
position: relative;
width: 100%;
height: 100%;
background-color: color-mix(in srgb, currentColor 15%, transparent);
}
.knob {
width: var(--knob-size);
height: var(--knob-size);
background: currentColor;
transition: translate var(--transition);
transition-delay: .3;
transition-duration: .5s;
opacity: .9;
}
.arrow {
position: absolute;
width: var(--arrow-size);
height: var(--arrow-size);
border-radius: 99px;
background: currentColor;
opacity: .3;
transition: opacity var(--transition);
&.left {
left: var(--arrow-to-edge);
top: 50%;
translate: 0 -50%;
}
&.up {
top: var(--arrow-to-edge);
left: 50%;
translate: -50% 0;
}
&.right {
right: var(--arrow-to-edge);
top: 50%;
translate: 0 -50% ;
}
&.down {
bottom: var(--arrow-to-edge);
left: 50%;
translate: -50% 0;
}
}
&[direction="left"], &[direction="up_left"], &[direction="down_left"] {
.arrow.left {
opacity: 1;
}
}
&[direction="up_left"], &[direction="up"], &[direction="up_right"] {
.arrow.up {
opacity: 1;
}
}
&[direction="up_right"], &[direction="right"], &[direction="down_right"] {
.arrow.right {
opacity: 1;
}
}
&[direction="down_right"], &[direction="down"], &[direction="down_left"] {
.arrow.down {
opacity: 1;
}
}
}
Attached button
- html
- css
- javascript
<xyba-analog relocate class="attachment-analog" capture></xyba-analog>
<xyba-button class="attachment-button" ignore-capture=".attachment-analog">A</xyba-button>
.attachment-button {
position: fixed;
translate: -50% -50%;
opacity: 0;
}
.attachment-analog[active] + .attachment-button {
opacity: 1;
}
const analog = document.querySelector('.attachment-analog');
const button = document.querySelector('.attachment-button');
const positionButton = (analogElement) => {
const { x, y } = analogElement.position;
const xOffset = analogElement.offsetWidth / 2;
const yOffset = -50;
button.style.left = `${x + xOffset}px`;
button.style.top = `${y + yOffset}px`;
}
analog.addEventListener('change', (e) => {
positionButton(e.target);
});
positionButton(analog);