- for (var i = 1; i <= 9; i++)
  .stretcher-cell
.view
  .ball
  
//- not crucial swipe-aware structure
.layout-reference
  each rowVal in [-1, 0, 1]
    each colVal in [-1, 0, 1]
      span(data-point=`(${rowVal}, ${colVal})`)
      
//- config menu
.config-menu
  label.config-menu__toggler
    input(type="checkbox" name="toggle-config-menu" id="toggle-config-menu")
    span.icon ⚙
  form.config-menu__form
    fieldset
      legend Value Type
      label
        input(type="radio" name="valueType" value="discrete" checked)
        span Discrete
      label
        input(type="radio" name="valueType" value="continuous")
        span Continuous

    fieldset
      legend Snap to Points
      label
        input(type="radio" name="snap" value="none")
        span None
      label
        input(type="radio" name="snap" value="center" checked)
        span Center
      label
        input(type="radio" name="snap" value="all")
        span All

    fieldset#optimized-for-diagonal-swipe
      legend Optimization
      label
        input(type="checkbox" name="diagonal" id="checkbox-diagonal")
        span Optimize for diagonal swipes
        .info-icon
          i.icon-info(title="Reduces the L-shaped motion when swiping diagonally")
View Compiled
:root {
  --x: 0;
  --y: 0;
  --discrete-x: 0;
  --discrete-y: 0;
  --continuous-x: 0;
  --continuous-y: 0;
  --swipe-displacement: max(25vmin, 12.5vh);
}

html,
body {
  overscroll-behavior: contain;
}

html {
  scroll-snap-type: both mandatory;
  scroll-snap-stop: always;

  /* hide scrollbar */

  -ms-overflow-style: none; /* IE and Edge */
  scrollbar-width: none; /* Firefox */

  &::-webkit-scrollbar {
    display: none; /* Chrome, Safari and Opera */
  }
}

body {
  margin: 0;
  font-family: Helvetica, Arial, sans-serif;
  --x-offset: calc(10rem + 10lvw);
  --y-offset: calc(10rem + 10lvh);
  --scroll-orientation: -1; /* [-1, 1] */
  scrollbar-width: none;
  -ms-overflow-style: none;
  position: relative;
  flex-grow: 0;
  flex-shrink: 0;
  display: grid;
  height: calc(90lvh + var(--y-offset));
  width: calc(90lvw + var(--x-offset));
  grid-template-columns: calc(var(--x-offset) / 2) 90lvw calc(
      var(--x-offset) / 2
    );
  grid-template-rows: calc(var(--y-offset) / 2) 90lvh calc(var(--y-offset) / 2);
}

body::-webkit-scrollbar {
  width: 0;
  height: 0;
}

.stretcher-cell {
  box-shadow: inset 0 0 0 1px gray;
}

@keyframes discrete-horizontal-motion {
  0% {
    --discrete-x: -1;
  }
  50% {
    --discrete-x: 0;
  }
  100% {
    --discrete-x: 1;
  }
}

@keyframes discrete-vertical-motion {
  0% {
    --discrete-y: -1;
  }
  50% {
    --discrete-y: 0;
  }
  100% {
    --discrete-y: 1;
  }
}

@property --continuous-x {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

@property --continuous-y {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

@keyframes continuous-horizontal-motion {
  0% {
    --continuous-x: -1;
  }
  50% {
    --continuous-x: 0;
  }
  100% {
    --continuous-x: 1;
  }
}

@keyframes continuous-vertical-motion {
  0% {
    --continuous-y: -1;
  }
  50% {
    --continuous-y: 0;
  }
  100% {
    --continuous-y: 1;
  }
}

.view {
  position: fixed;
  top: 0;
  left: 0;
  width: 100dvw;
  height: 100dvh;
  pointer-events: none;
}

@function abs($val) {
  @return max(#{$val}, -1 * #{$val});
}

@function sign($val) {
  @return calc(#{$val}/#{abs(#{$val})});
}

@function delayed($val) {
  // returns 0 until (x, y) value is >= 1.
  @return calc(10000 * (max(0.9999, #{$val}) - 0.9999));
}

.ball {
  --ball-size: max(5vmax, 7.5vh);
  position: absolute;
  width: var(--ball-size);
  height: var(--ball-size);
  border-radius: 50%;
  background: dodgerblue;
  top: calc(50dvh - var(--ball-size) / 2);
  left: calc(50dvw - var(--ball-size) / 2);
  flex-grow: var(--x);
  flex-shrink: var(--y);
  transition: transform 125ms ease-out;
}

/* ------------------------------------------------------------ */
/* ----------------- Swipe behavior variations ---------------- */
/* ------------------------------------------------------------ */

body {
  /* Discrete vs Continuous */
  &:has(.config-menu input[name="valueType"][value="discrete"]:checked):not(
      :has(.config-menu #checkbox-diagonal:checked)
    ) {
    animation: discrete-horizontal-motion 1ms linear,
      discrete-vertical-motion 1ms linear;
    animation-timeline: scroll(nearest inline), scroll(nearest block);
    transition: --discrete-x 50ms ease-out, --discrete-y 50ms ease-out;

    & > * {
      --x: var(--discrete-x);
      --y: var(--discrete-y);
    }

    .ball {
      transform: translate(
        calc(-1 * var(--swipe-displacement) * var(--x)),
        calc(-1 * var(--swipe-displacement) * var(--y))
      );
    }
  }

  &:has(.config-menu input[name="valueType"][value="continuous"]:checked),
  &:has(.config-menu #checkbox-diagonal:checked) {
    animation: continuous-horizontal-motion 1ms linear,
      continuous-vertical-motion 1ms linear;
    animation-timeline: scroll(nearest inline), scroll(nearest block);
    transition: --continuous-x 50ms ease-out, --continuous-y 50ms ease-out;

    & > * {
      --x: var(--continuous-x);
      --y: var(--continuous-y);
    }

    .ball {
      transform: translate(
        calc(-1 * var(--swipe-displacement) * var(--x)),
        calc(-1 * var(--swipe-displacement) * var(--y))
      );
    }
  }

  /* Grid snap */
  &:has(.config-menu input[name="snap"][value="none"]:checked):not(
      :has(.config-menu #checkbox-diagonal:checked)
    ) {
  }

  &:has(.config-menu input[name="snap"][value="center"]:checked),
  &:has(.config-menu #checkbox-diagonal:checked) {
    .stretcher-cell:nth-of-type(5) {
      background: #ccc;
      scroll-snap-align: center center;
    }
  }

  &:has(.config-menu input[name="snap"][value="all"]:checked):not(
      :has(.config-menu #checkbox-diagonal:checked)
    ) {
    .stretcher-cell {
      background: #ccc;
      scroll-snap-align: center center;
    }
  }

  /* Optimized for Diagonal Swipe */
  &:has(.config-menu #checkbox-diagonal:checked) {
    .layout-reference {
      &::before {
        content: "";
        position: absolute;
        inset: 0;
        background: linear-gradient(
          45deg,
          transparent 49.875%,
          navy 49.875%,
          navy 50.125%,
          transparent 50.125%
        );
        opacity: 0.1;
      }

      &::after {
        content: "";
        position: absolute;
        inset: 0;
        background: linear-gradient(
          -45deg,
          transparent 49.875%,
          navy 49.875%,
          navy 50.125%,
          transparent 50.125%
        );
        opacity: 0.1;
      }
    }

    .ball {
      --sum: min(
        1,
        pow(#{abs(var(--x, 0))}, 0.05) + pow(#{abs(var(--y, 0))}, 0.05)
      );
      --delay-result: #{delayed(var(--sum))};
      transform: translate(
        calc(
          -1 * var(--swipe-displacement) * #{sign(var(--x, 0))} *
            var(--delay-result)
        ),
        calc(
          -1 * var(--swipe-displacement) * #{sign(var(--y, 0))} *
            var(--delay-result)
        )
      );
    }
  }
}

/* ------------------------------------------------------------ */
/* --------------------- Aux UI component --------------------- */
/* ------------------------------------------------------------ */

.layout-reference {
  position: fixed;
  width: calc(2 * var(--swipe-displacement));
  height: calc(2 * var(--swipe-displacement));
  top: calc(50% - var(--swipe-displacement));
  left: calc(50% - var(--swipe-displacement));
  background: #d7ecff; // "lightdodgerblue"
  font-family: monospace;
  font-variant-numeric: tabular-nums;
  margin: auto;
  pointer-events: none;
  display: grid;
  grid-template-columns: repeat(3, auto);
  grid-template-rows: repeat(3, auto);
  justify-content: space-between;
  align-content: space-between;
  mix-blend-mode: multiply;

  span {
    position: relative;
    text-align: center;
  }

  span::before {
    position: absolute;
    white-space: nowrap;
    transform: translate(-50%, -50%);
    content: attr(data-point);
  }
}

/* ------------------------------------------------------------ */
/* --------------- Config menu - swipe behavior --------------- */
/* ------------------------------------------------------------ */

.config-menu {
  --config-menu_bg-color: #333;
  --config-menu_text-color: #fff;
  --config-menu_accent-color: dodgerblue;
  --config-menu_border-radius: 4px;
  --config-menu_offset: 1ch;

  position: fixed;
  top: var(--config-menu_offset);
  right: var(--config-menu_offset);
  z-index: 99999999;
  padding-left: var(--config-menu_offset);

  & > * {
    top: 0;
    right: 0;
    background: var(--config-menu_bg-color);
    color: var(--config-menu_text-color);
    border-radius: var(--config-menu_border-radius);
    box-shadow: 0 0.5ch 1ch rgba(0 0 0 / 0.25), 0 1ch 3ch rgba(0 0 0 / 0.25);
  }
}

.config-menu__toggler {
  position: absolute;
  display: flex;
  font-size: 2em;
  line-height: 0.5em;
  padding: 0.5ch;
  cursor: pointer;
  z-index: 1;

  &:has(input:checked) {
    box-shadow: none;

    .icon {
      rotate: 25deg;
    }

    & + .config-menu__form {
      transform: translateX(0);
    }
  }

  input {
    /* Add if not using autoprefixer */
    -webkit-appearance: none;
    /* Remove most all native input styles */
    appearance: none;
    /* For iOS < 15 */
    background-color: var(--config-menu_bg-color);
    /* Not removed via appearance */
    margin: 0;
    width: 0;
    height: 0;
  }

  .icon {
    display: inline-block;
    transition: 125ms ease-out;
    user-select: none;
  }

  &:active .icon {
    rotate: 25deg;
  }
}

.config-menu__form {
  font-family: monospace;
  font-weight: 300;
  font-size: 12px;
  padding: 1ch;
  width: fit-content;
  transition: transform 250ms cubic-bezier(0.28, 0.95, 0.28, 0.95);
  transform: translateX(calc(100% + 5 * var(--config-menu_offset)));

  fieldset {
    border: 1px solid #ccc;
    padding: 10px;

    &:not(:last-of-type) {
      margin-bottom: 1em;
    }

    legend {
      font-weight: bold;
    }

    label {
      display: flex;
      margin-bottom: 5px;

      & > * {
        flex-shrink: 0;
      }

      span {
        margin-left: 5px;
      }
    }
  }

  &:has(#checkbox-diagonal:checked)
    fieldset:not(#optimized-for-diagonal-swipe) {
    opacity: 0.5;
    cursor: not-allowed;

    label,
    input {
      pointer-events: none;
    }
  }

  .info-icon {
    display: inline-block;
    position: relative;

    i.icon-info {
      cursor: pointer;

      &:before {
        content: "ⓘ";
        text-align: center;
        margin-left: 0.5ch;
      }

      &:hover + .tooltip {
        visibility: visible;
        opacity: 1;
      }
    }

    .tooltip {
      visibility: hidden;
      opacity: 0;
      position: absolute;
      left: 50%;
      bottom: 100%;
      transform: translateX(-50%);
      background-color: #333;
      color: #fff;
      padding: 5px 10px;
      border-radius: 5px;
      transition: opacity 0.3s ease;

      &:before {
        content: "";
        position: absolute;
        top: 100%;
        left: 50%;
        transform: translateX(-50%);
        border-width: 5px;
        border-style: solid;
        border-color: #333 transparent transparent transparent;
      }
    }
  }

  /*    
    Repurpused radio buttons from Stephanie Eckles
    https://moderncss.dev/pure-css-custom-styled-radio-buttons/
    */
  input:is([type="radio"], [type="checkbox"]) {
    /* Add if not using autoprefixer */
    -webkit-appearance: none;
    /* Remove most all native input styles */
    appearance: none;
    /* For iOS < 15 */
    background-color: var(--config-menu_bg-color);
    /* Not removed via appearance */
    margin: 0;
    overflow: hidden;

    font: inherit;
    color: currentColor;
    width: 1.15em;
    height: 1.15em;
    border: 0.15em solid currentColor;
    transform: translateY(-0.075em);

    display: grid;
    place-content: center;

    &::before {
      width: 0.65em;
      height: 0.65em;
      transform: scale(0);
      transition: 125ms transform ease-in-out;
      /* Windows High Contrast Mode */
      background-color: CanvasText;
    }

    &:focus {
      outline: max(2px, 0.15em) solid currentColor;
      outline-offset: max(2px, 0.15em);
    }

    &:checked::before {
      transform: scale(1);
    }
  }

  input[type="radio"] {
    border-radius: 50%;

    &::before {
      content: "";
      border-radius: inherit;
      box-shadow: inset 1em 1em var(--config-menu_accent-color);
    }
  }

  input[type="checkbox"] {
    border-radius: 2px;

    &::before {
      content: "✓";
      border-radius: none;
      font-size: 1.5em;
      line-height: 1.1ch;
      background: var(--config-menu_accent-color);
    }
  }
}
View Compiled
/* 
            / _ \
          \_\(_)/_/
           _//"\\_  Max
            /   \
  
CSS-only, JavaScript-free code!
-------------------------------

Preview for: 
Desert Racer 🏜️: World's First CSS-only Swipe-Aware Game!
Article at:
https://dev.to/warkentien2/desert-racer-worlds-first-css-only-swipe-aware-game-4j0h
by @warkentien2

MIT License
Copyright (c) 2023 Philip Warkentien II

*/

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.