README · NearMouse — Diamond Light Effect

12 coloured gems scattered randomly, each reacting to the cursor: proximity halo, repulsion and 3-D cast shadow — driven entirely by native CSS trigonometry with a single mousemove listener.

CSS Custom Properties CSS Trig Functions Vanilla JS Zero dependencies Standalone HTML
What is it

Overview

NearMouse is a pure CSS + 3-line JS interactive effect. Diamonds are scattered at random positions. As the cursor enters a gem's --impact-radius, it glows, slides away, and casts a perspective shadow.

No canvas, no WebGL, no animation loop — the browser re-evaluates CSS custom properties on every mousemove at native frame-rate.

Capabilities

Features

  • CSS cos() / sin() / atan2() / sqrt()
  • Distance factor --dist-factor — clamped 0 → 1
  • Diamond shape via clip-path polygon
  • Proximity halo via CSS drop-shadow()
  • Brightness boost on approach
  • 3-D perspective cast shadow per gem
  • Random scatter via JS — refreshes on reload
  • Only 2 variables to customise
Internals

How it works

1
Random scatter — JS generates 12 random --x / --y pairs (range ±3.8) and sets them as inline CSS custom properties on each gem & shadow pair.
2
Mouse trackingmousemove writes --mx / --my (offset from demo-box centre) onto the scene element. A mouseleave resets them to −9999 so gems go idle.
3
Distance factor — per gem, CSS computes Euclidean distance sqrt(dx²+dy²) then clamps to [0,1]: (impact-radius − min(dist, impact-radius)) / impact-radius.
4
Reactivity cascade--dist-factor drives the repulsion translation along atan2 direction, the drop-shadow glow radius, brightness() boost, and the shadow opacity & 3-D tilt simultaneously.
Customisation

Variables

VariableDefaultDescription
--diamond-size38 Gem size in px (unitless, used as × 1px). Also scales orbit & shadow.
--impact-radius170 Activation radius in px. Gems outside this range stay idle.
--mx / --my-9999 Cursor offset from scene centre. Set by JS. Defaults keep gems idle on load.
--x / --yvia JS Random gem position in diamond-size units. Set at scatter time.
Compatibility

Browser support

Requires CSS trigonometric functions — cos(), sin(), atan2(), sqrt() — available in all evergreen browsers since mid-2023.

Chrome 111+
Firefox 108+
Safari 15.4+
Edge 111+

No build step, no dependencies, no framework. A single .html file is all you need.

HTML

<div class="demo-box" id="demo">
  <div class="baseLight"></div>

  <div class="shadows">
    <!-- 12 × .shadow divs -->
    <div class="shadow"></div>
    <!-- × 12 -->
  </div>

  <div class="diamonds">
    <!-- 12 × .diamond divs -->
    <div class="diamond"></div>
    <!-- × 12 -->
  </div>
</div>

CSS

.demo-box {
  position: relative;
  height: 380px;
  background: #010f1e;
  overflow: hidden;
  cursor: crosshair;
  /* ── tune these two ── */
  --diamond-size: 38;
  --impact-radius: 170;
}
.baseLight {
  position: absolute;
  inset: calc(50% - (var(--diamond-size) * 5px));
  background-image:
    radial-gradient(closest-side,
      rgba(255,255,255,.12) 2%, transparent);
  transform: translate(
    calc(var(--mx,-9999) * 1px),
    calc(var(--my,-9999) * 1px));
}
.shadows, .diamonds {
  position: absolute;
  inset: 50%; /* 0×0 anchor at centre */
  pointer-events: none;
}
.shadows > div, .diamonds > div {
  position: absolute;
  /* --x / --y set by JS (scatter) */
  --hue: calc((var(--x,0) + var(--y,0)) * 22);
  --dx: calc(var(--x,0) * var(--diamond-size)
         - var(--mx,-9999));
  --dy: calc(var(--y,0) * var(--diamond-size)
         - var(--my,-9999));
  --dist: calc(sqrt(
    var(--dx)*var(--dx) + var(--dy)*var(--dy)));
  --dist-factor: calc(
    (var(--impact-radius)
     - min(var(--dist),var(--impact-radius)))
    / var(--impact-radius));
  --angle: atan2(var(--dy), var(--dx));
  --_t: calc(
    var(--dist-factor) * var(--diamond-size) * 1px);
}
.shadow {
  left:   calc(var(--x,0) * var(--diamond-size) * 1px);
  top:    calc((var(--y,0) - .35) * var(--diamond-size) * 1px);
  width:  calc(var(--diamond-size) * 2.4px);
  height: calc(var(--diamond-size) * 0.6px);
  transform-origin: left;
  background-image: linear-gradient(to right, #000, transparent);
  opacity: var(--dist-factor, 0);
  transform:
    rotate(var(--angle)) translate(var(--_t))
    perspective(calc(var(--diamond-size) * 2px))
    rotateY(calc(var(--dist-factor) * -50deg - 20deg));
}
.diamond {
  left:  calc((var(--x,0) - .5) * var(--diamond-size) * 1px);
  top:   calc((var(--y,0) - .5) * var(--diamond-size) * 1px);
  width: calc(var(--diamond-size) * 1px);
  aspect-ratio: 1;
  clip-path: polygon(50% 0%,100% 50%,50% 100%,0% 50%);
  background-color: hsl(var(--hue), 85%, 50%);
  background-image:
    linear-gradient(135deg,rgba(255,255,255,.80) 0%,transparent 42%),
    linear-gradient(315deg,rgba(0,0,30,.52)       0%,transparent 50%),
    linear-gradient(225deg,rgba(255,255,255,.18)  0%,transparent 38%);
  filter:
    drop-shadow(0 0
      calc(var(--dist-factor,0)*calc(var(--diamond-size)*.7px))
      hsl(var(--hue),100%,70%))
    brightness(calc(1 + var(--dist-factor,0) * .5));
  transform: rotate(var(--angle)) translate(var(--_t));
}

JavaScript

const demo     = document.querySelector('#demo');
const shadows  = [...demo.querySelectorAll('.shadow')];
const diamonds = [...demo.querySelectorAll('.diamond')];

/* ── random scatter ── */
shadows.forEach((s, i) => {
  const x = (Math.random() * 7.6 - 3.8).toFixed(3);
  const y = (Math.random() * 5.0 - 2.5).toFixed(3);
  s.style.setProperty('--x', x);
  s.style.setProperty('--y', y);
  diamonds[i].style.setProperty('--x', x);
  diamonds[i].style.setProperty('--y', y);
});

/* ── mouse tracking (demo-relative) ── */
window.addEventListener('mousemove', e => {
  const r = demo.getBoundingClientRect();
  demo.style.setProperty('--mx', e.clientX - r.left - r.width  / 2);
  demo.style.setProperty('--my', e.clientY - r.top  - r.height / 2);
});
demo.addEventListener('mouseleave', () => {
  demo.style.setProperty('--mx', -9999);
  demo.style.setProperty('--my', -9999);
});
Drop-in

Quick integration

1
Copy the HTML: one .demo-box, 12 .shadow, 12 .diamond divs.
2
Add the CSS block to your stylesheet or a <style> tag.
3
Add the JS snippet — or merge the mousemove listener into an existing one.
4
Tune --diamond-size (size & orbit scale) and --impact-radius (activation zone) on .demo-box.
5
To use a fixed layout instead of random, replace the scatter loop with pre-computed --x / --y values (e.g. cos/sin for a ring).