Various Analog Designs
These are examples with accompanied code to serve as inspiration for custom implementation and styling.
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;
will-change: opacity, transform;
}
.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);