3DFree

Hand Tracking UI

Three glass buttons (Select, Grab, Point) and a 24 px ring that follows the pointer across the whole frame, jumps onto the button that gets the keyboard focus and squeezes to 80% when you press: a simulation of a virtual hand pointer, driven by the mouse, a finger or the keyboard — no camera and no hand detection, despite its name.

JSGesturesUI

Updated

Hover or click the scene to interact

This effect is free — the 3D category contains 50 effects total, including 8 free. Effect.Labs has 840 vanilla effects. Explore the category →

Usage examples

Musée de la Batellerie · reception kioskChoose your tour

Mouse demo of the future kiosk's pointer

① Mock-up of a gesture-driven museum kiosk — presentation of an outreach project

WhenThe project page shows the welcome screen of the future kiosk, "Choose your tour", with three buttons "Kids", "Express", "Full"; the ring stands for the visitor's hand, and the caption says so: "Mouse demo of the future kiosk's pointer".
WhyA ring that follows the pointer and squeezes on click shows how the kiosk will respond, without claiming to detect a hand: the honest caption avoids promising hand tracking the page does not do.
SettingsRoot styled as a kiosk screen (6 px dark bezel, 14 px corners, cream background); enlarged buttons (16 × 22 px padding, 15 px, bold), cream with a beige border, brown when active; ring and center recolored brown #9a6b3a; title and caption placed absolutely inside the area.
Help · Display settingsEnlarged pointer

A thick ring follows your mouse and squeezes when you click: the pointer stays easy to spot on every page.

Enlarged pointer: on

② Enlarged pointer for visually impaired users — help page of a town hall website

WhenThe "Display settings" page presents the "Enlarged pointer" option: a thick ring follows the mouse over three buttons "Services", "Events", "Contact" and squeezes when you click.
WhyA thick ring and visual click feedback make the pointer easy to spot; it is a useful setting, not a gimmick, and the buttons stay real buttons for the keyboard.
Settings40 px ring with a 4 px orange #f59e0b border, recentered by margin: -8px 0 0 -8px (the script places it for 24 px); white buttons with a gray border, blue #1d4ed8 when active; light background; "on" switch drawn in CSS.

Slide 4 / 12Quarterly results

Live · laser pointer on · 23 participants

③ Virtual laser pointer for a video conference — page of a presentation software

WhenThe software page shows a "Quarterly results" slide with three areas "Sales", "Stocks", "Team"; the red ring follows the mouse like a laser pointer and marks the clicked area.
WhyA ring following the cursor is visible from afar, even compressed in a video stream, better than a mouse arrow; marking the active area helps the speaker say "right here".
SettingsButtons turned into 118 × 96 px cards (label and figure stacked); active card outlined in red #ef4444 with a 3 px glow; red ring with a 45% red center; white slide in a dark frame, "Live" bar at the bottom.

How it works

The root .fx-0048-root (250 px tall, rgb(26, 26, 36) background) holds .xr-hand-tracking, an area spanning the whole frame that centers three real <button type="button"> .xr-hand-btn buttons: indigo fill at 15%, indigo border at 30%, #a1a1aa text at 0.8rem, 8 px corners. Above them sits the ring .xr-hand-cursor: 24 px, 2 px #6366f1 border, center tinted at 30%, pointer-events: none so it never gets in the way of a click. At first it waits below the row of buttons, centered, without covering one.

The script goes through every root on the page and wires one instance per root. On each pointer move or press inside the area, it centers the ring under the pointer (left and top positions); when the pointer leaves, the ring stays at its last position. When a button gets the keyboard focus (:focus-visible), the ring jumps onto its center. A click, Enter or Space marks that button as active (active class and aria-pressed="true", the other two go back to false) and squeezes the ring to 80% for 200 ms. Measured in Chromium 151 and Firefox 153: two Tab presses put the ring at the center of "Grab", Enter marks it active; two copies on one page each have their own ring.

The catalog name speaks of hand tracking: the code uses no camera and no gesture detection. It shows what a virtual hand pointer would look like, and the ring follows anything that points: mouse, pen, finger (touch-action: pan-y keeps vertical scrolling, a sideways drag moves the ring, a tap places it and activates the button touched) and keyboard focus. For real hand tracking, you would need a detection library hooked to the camera, calling the same moveTo(x, y) function with the finger position. The buttons are real buttons: Tab, Enter and Space work with no extra code, and screen readers announce their pressed state.

Accessibility

  • Reduced motion: under prefers-reduced-motion: reduce, a block scoped to .fx-0048-root removes the ring squeeze (transform: none), its transitions and the buttons' growth on hover. The ring still follows the pointer, and click feedback stays visible through the active button's color. Measured in Chromium, Firefox and WebKit: no change of scale on the ring after a click.
  • Keyboard: Tab moves from one button to the next, with a 2 px #a5b4fc outline (8.7:1 contrast on the background) and the ring jumping onto the button; Enter or Space activates it. Safari only gives Tab focus to buttons when the system keyboard option is turned on (measured in WebKit: no focus by default); Chromium and Firefox always do.
  • Screen readers: the ring is aria-hidden="true"; the buttons announce their name and state (aria-pressed). If your buttons trigger a real action, keep aria-pressed for an exclusive choice, or remove it for a one-off action.
  • Contrast: #a1a1aa text on the button at 5.8:1 at rest, 4.1:1 on the active button; #6366f1 ring at 3.9:1 on the background, above the 3:1 threshold for graphics.

Browser compatibility

CSS (absolute positioning, calc(), transition, touch-action, :focus-visible) + JS with pointer, focusin and click events, no dependencies. Measured in Chromium 151, Firefox 153 and WebKit 26.5: the ring follows the mouse, squeezes on click, two independent copies; with the keyboard in Chromium and Firefox; with a finger in Chromium in touch mode. No CSS 3D and no WebXR: nothing beyond the pointer is simulated in the browser.

Chrome 86+✓ Full
Firefox 85+✓ Full
Safari 15.4+✓ Measured on WebKit; Tab on buttons depends on the system keyboard option
Edge 86+✓ Full
Mobile iOS 15.4+✓ Not checked on a real iPhone
Android Chrome✓ Full, with the finger

Without :focus-visible (browsers older than those listed), the ring no longer jumps onto the button with the keyboard but the buttons stay usable; without JavaScript, the three buttons stay clickable and the ring stays still below the row.

The code

Copy the three blocks into your page. No dependencies.

HTML
<div class="fx-0048-root">
  <div class="xr-hand-tracking">
    <button type="button" class="xr-hand-btn" aria-pressed="false">Select</button>
    <button type="button" class="xr-hand-btn" aria-pressed="false">Grab</button>
    <button type="button" class="xr-hand-btn" aria-pressed="false">Point</button>
    <div class="xr-hand-cursor" aria-hidden="true"></div>
  </div>
</div>
CSS
.fx-0048-root,
.fx-0048-root *,
.fx-0048-root *::before,
.fx-0048-root *::after {
  box-sizing: border-box
}

.fx-0048-root {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 250px;
  overflow: hidden;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

/* Site design system variables this code depends on (values from css/style.css — adapt freely). */
.fx-0048-root {
  --text-secondary: #a1a1aa;
}

.xr-hand-tracking {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  /* Touch: a sideways drag moves the ring, a vertical swipe still scrolls the page */
  touch-action: pan-y;
}

.xr-hand-cursor {
  width: 24px;
  height: 24px;
  border: 2px solid #6366f1;
  border-radius: 50%;
  position: absolute;
  /* Starting position: below the row of buttons, not covering one */
  left: calc(50% - 12px);
  top: calc(50% + 34px);
  pointer-events: none;
  transition: transform 0.1s ease;
}

.xr-hand-cursor::after {
  content: '';
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: rgba(99, 102, 241, 0.3);
}

.xr-hand-btn {
  font-family: inherit;
  padding: 10px 20px;
  background: rgba(99, 102, 241, 0.15);
  border: 1px solid rgba(99, 102, 241, 0.3);
  border-radius: 8px;
  color: var(--text-secondary);
  font-size: 0.8rem;
  cursor: pointer;
  transition: all 0.3s ease;
  margin: 0 8px;
}

.xr-hand-btn:hover {
  background: rgba(99, 102, 241, 0.3);
  transform: scale(1.05);
}

.xr-hand-btn.active {
  background: rgba(99, 102, 241, 0.4);
  border-color: #6366f1;
}

.xr-hand-btn:focus-visible {
  outline: 2px solid #a5b4fc;
  outline-offset: 2px;
}

/* Reduced motion: the ring still follows the pointer, but it no longer squeezes and the buttons no longer grow */
@media (prefers-reduced-motion: reduce) {
  .fx-0048-root .xr-hand-cursor {
    transform: none !important;
    transition: none;
  }

  .fx-0048-root .xr-hand-btn {
    transition: none;
  }

  .fx-0048-root .xr-hand-btn:hover {
    transform: none;
  }
}
JavaScript (fx-0048)
(function() {
  // Hand Tracking UI — the ring follows the mouse, the finger or the keyboard focus (fx-0048)
  // One instance per .fx-0048-root: several copies on one page each have their own ring.
  document.querySelectorAll('.fx-0048-root').forEach(function(root) {
    var container = root.querySelector('.xr-hand-tracking');
    var cursor = root.querySelector('.xr-hand-cursor');
    if (!container || !cursor) return;
    var buttons = container.querySelectorAll('.xr-hand-btn');

    // x, y: coordinates in the window (pointer, or center of the focused button)
    function moveTo(x, y) {
      var rect = container.getBoundingClientRect();
      cursor.style.left = (x - rect.left - 12) + 'px';
      cursor.style.top = (y - rect.top - 12) + 'px';
    }

    function follow(e) {
      moveTo(e.clientX, e.clientY);
    }

    container.addEventListener('pointermove', follow);
    container.addEventListener('pointerdown', follow);

    // Keyboard: the ring jumps onto the button that gets the focus (Tab), Enter or Space presses it
    container.addEventListener('focusin', function(e) {
      var btn = e.target.closest('.xr-hand-btn');
      if (!btn || !btn.matches(':focus-visible')) return;
      var r = btn.getBoundingClientRect();
      moveTo(r.left + r.width / 2, r.top + r.height / 2);
    });

    buttons.forEach(function(btn) {
      btn.addEventListener('click', function() {
        buttons.forEach(function(b) {
          b.classList.remove('active');
          b.setAttribute('aria-pressed', 'false');
        });
        btn.classList.add('active');
        btn.setAttribute('aria-pressed', 'true');
        cursor.style.transform = 'scale(0.8)';
        setTimeout(function() {
          cursor.style.transform = 'scale(1)';
        }, 200);
      });
    });
  });
})();

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Ring size (CSS — width and height of .xr-hand-cursor; JS — the 12 offset) 24 px The script centers the ring by subtracting 12 px: for a larger ring, change both 12 values to half its size, or compensate with a negative margin as in the second scene (40 px, margin: -8px 0 0 -8px).
Ring colors (CSS — border of .xr-hand-cursor, background of its ::after) #6366f1, center at 30% Keep at least 3:1 contrast with the background so the ring stays easy to spot.
Click squeeze (JS — scale(0.8) and 200) 80% for 200 ms 0.6 gives a stronger "pinch"; past 300 ms the feedback feels slow.
Starting position (CSS — left and top of .xr-hand-cursor) centered, 34 px below the middle Put it where it hides nothing. Remove these two lines and the ring would start at the exact center, on the middle button.
Buttons (HTML — text of the <button> elements; CSS — .xr-hand-btn and .active) Select, Grab, Point; indigo at 15, 30 and 40% The script takes every .xr-hand-btn in the area, whatever their number. Keep <button> elements: keyboard support and the pressed state work with no extra code.
Height (CSS — height: 250px of .fx-0048-root) 250 px The whole area follows the pointer: make it larger to cover an entire section.

FAQ

Does the effect really track the hand?

No. It uses no camera and no gesture detection: the ring follows the mouse, a pen, a finger on a touch screen and the keyboard focus. The name describes the intended use, a virtual hand interface; to hook it to real tracking, you would need a detection library calling moveTo(x, y) with the position of the filmed finger.

Why doesn't the ring start in the center?

At the exact center, it would cover the middle button before any movement and look as if that button were already targeted. So it waits 34 px below the middle of the area, under the row of buttons; as soon as the pointer moves or a button gets the focus, it joins it.

Can I put several areas on one page?

Yes. Each copy has its own .fx-0048-root and the script starts one instance per root, without ids: each area has its own ring and active buttons. That is the case on this page, where the demo at the top and the three examples run on a single script.