28 CSS Close Buttons14 / 28

CSS + JSMIT licensed

Magnetic Cross Close Button

The award-site micro-interaction: as the cursor approaches, the × leans toward it — pulled by an invisible magnetic field — then snaps elastically home when the cursor leaves. Ten lines of pointer math feed two CSS custom properties; CSS does all the actual motion, so the effect stays 60fps and fully degradable.

Published Updated

Live Demo
Try it

The code

<section class="ccb-14" aria-label="Magnetic close button demo">
  <div class="ccb-14__zone" id="ccb-14-zone">
    <button class="ccb-14__close" type="button" aria-label="Close">
      <span class="ccb-14__x" aria-hidden="true"></span>
    </button>
  </div>
</section>
.ccb-14,
.ccb-14 *,
.ccb-14 *::before,
.ccb-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ccb-14 {
  --accent: oklch(0.72 0.17 150);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: radial-gradient(110% 120% at 50% 0%,oklch(0.24 0.04 160 / .9),transparent 60%),oklch(0.16 0.02 170);
}

.ccb-14 .ccb-14__zone {
  --mx: 0px;
  --my: 0px;
  --gx: 0px;
  --gy: 0px;
  display: grid;
  place-items: center;
  width: 170px;
  height: 170px;
  border-radius: 50%;
}

.ccb-14 .ccb-14__close {
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  border: 1.5px solid oklch(0.72 0.17 150 / .4);
  border-radius: 50%;
  background: oklch(0.2 0.03 165);
  cursor: pointer;
  transform: translate3d(var(--mx),var(--my),0);
  transition: transform .08s linear,border-color .2s ease;
  box-shadow: 0 16px 36px -18px rgba(0,0,0,.8);
}

.ccb-14 .ccb-14__x {
  position: relative;
  width: 20px;
  height: 20px;
  transform: translate3d(var(--gx),var(--gy),0);
  transition: transform .08s linear;
}

.ccb-14 .ccb-14__x::before,
.ccb-14 .ccb-14__x::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}

.ccb-14 .ccb-14__x::before {
  transform: translate(-50%,-50%) rotate(45deg);
}

.ccb-14 .ccb-14__x::after {
  transform: translate(-50%,-50%) rotate(-45deg);
}

.ccb-14 .ccb-14__zone.is-releasing .ccb-14__close,
.ccb-14 .ccb-14__zone.is-releasing .ccb-14__x {
  transition: transform .5s cubic-bezier(.34,1.8,.44,1);
}

.ccb-14 .ccb-14__close:hover {
  border-color: var(--accent);
}

.ccb-14 .ccb-14__close:active {
  transform: translate3d(var(--mx),var(--my),0) scale(.92);
}

.ccb-14 .ccb-14__close:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}
(() => {
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const zone = document.getElementById('ccb-14-zone'); if (!zone) return;
  const MAX = 14;
  const clamp = (v) => Math.max(-MAX, Math.min(MAX, v));
  zone.addEventListener('pointermove', (e) => {
    zone.classList.remove('is-releasing');
    const r = zone.getBoundingClientRect();
    const dx = e.clientX - (r.left + r.width / 2);
    const dy = e.clientY - (r.top + r.height / 2);
    zone.style.setProperty('--mx', clamp(dx * 0.3) + 'px');
    zone.style.setProperty('--my', clamp(dy * 0.3) + 'px');
    zone.style.setProperty('--gx', clamp(dx * 0.45) + 'px');  // glyph leads the chase
    zone.style.setProperty('--gy', clamp(dy * 0.45) + 'px');
  });
  zone.addEventListener('pointerleave', () => {
    zone.classList.add('is-releasing');                        // swap to elastic bezier
    ['--mx','--my','--gx','--gy'].forEach(p => zone.style.setProperty(p, '0px'));
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Close Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Magnetic Cross Close Button
Source: https://codefronts.com/components/css-close-buttons/magnetic-cross/

The award-site micro-interaction: as the cursor approaches, the × leans toward it — pulled by an invisible magnetic field — then snaps elastically home when the cursor leaves. Ten lines of pointer math feed two CSS custom properties; CSS does all the actual motion, so the effect stays 60fps and fully degradable.
## HTML
```html
<section class="ccb-14" aria-label="Magnetic close button demo">
  <div class="ccb-14__zone" id="ccb-14-zone">
    <button class="ccb-14__close" type="button" aria-label="Close">
      <span class="ccb-14__x" aria-hidden="true"></span>
    </button>
  </div>
</section>
```
## CSS
```css
.ccb-14,
.ccb-14 *,
.ccb-14 *::before,
.ccb-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.ccb-14 {
  --accent: oklch(0.72 0.17 150);
  font-family: 'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 48px 24px;
  background: radial-gradient(110% 120% at 50% 0%,oklch(0.24 0.04 160 / .9),transparent 60%),oklch(0.16 0.02 170);
}

.ccb-14 .ccb-14__zone {
  --mx: 0px;
  --my: 0px;
  --gx: 0px;
  --gy: 0px;
  display: grid;
  place-items: center;
  width: 170px;
  height: 170px;
  border-radius: 50%;
}

.ccb-14 .ccb-14__close {
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  border: 1.5px solid oklch(0.72 0.17 150 / .4);
  border-radius: 50%;
  background: oklch(0.2 0.03 165);
  cursor: pointer;
  transform: translate3d(var(--mx),var(--my),0);
  transition: transform .08s linear,border-color .2s ease;
  box-shadow: 0 16px 36px -18px rgba(0,0,0,.8);
}

.ccb-14 .ccb-14__x {
  position: relative;
  width: 20px;
  height: 20px;
  transform: translate3d(var(--gx),var(--gy),0);
  transition: transform .08s linear;
}

.ccb-14 .ccb-14__x::before,
.ccb-14 .ccb-14__x::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 2px;
  border-radius: 2px;
  background: var(--accent);
}

.ccb-14 .ccb-14__x::before {
  transform: translate(-50%,-50%) rotate(45deg);
}

.ccb-14 .ccb-14__x::after {
  transform: translate(-50%,-50%) rotate(-45deg);
}

.ccb-14 .ccb-14__zone.is-releasing .ccb-14__close,
.ccb-14 .ccb-14__zone.is-releasing .ccb-14__x {
  transition: transform .5s cubic-bezier(.34,1.8,.44,1);
}

.ccb-14 .ccb-14__close:hover {
  border-color: var(--accent);
}

.ccb-14 .ccb-14__close:active {
  transform: translate3d(var(--mx),var(--my),0) scale(.92);
}

.ccb-14 .ccb-14__close:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}
```

## JavaScript
```js
(() => {
  if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const zone = document.getElementById('ccb-14-zone'); if (!zone) return;
  const MAX = 14;
  const clamp = (v) => Math.max(-MAX, Math.min(MAX, v));
  zone.addEventListener('pointermove', (e) => {
    zone.classList.remove('is-releasing');
    const r = zone.getBoundingClientRect();
    const dx = e.clientX - (r.left + r.width / 2);
    const dy = e.clientY - (r.top + r.height / 2);
    zone.style.setProperty('--mx', clamp(dx * 0.3) + 'px');
    zone.style.setProperty('--my', clamp(dy * 0.3) + 'px');
    zone.style.setProperty('--gx', clamp(dx * 0.45) + 'px');  // glyph leads the chase
    zone.style.setProperty('--gy', clamp(dy * 0.45) + 'px');
  });
  zone.addEventListener('pointerleave', () => {
    zone.classList.add('is-releasing');                        // swap to elastic bezier
    ['--mx','--my','--gx','--gy'].forEach(p => zone.style.setProperty(p, '0px'));
  });
})();
```

How this works

JS listens to pointermove over a proximity zone (a wrapper ~2.5× the button), computes the cursor's offset from the button centre, damps it (×0.3 for the button, ×0.45 for the inner ×, so the glyph leads the chase), and writes --mx/--my. CSS consumes them: transform:translate3d(var(--mx),var(--my),0) with a fast linear transition while tracking.

The elastic return is the trick worth stealing: on pointerleave JS zeroes the variables and adds a class that swaps the transition to a 500ms overshoot bezier — the snap-home wobble is pure CSS, no spring library. The whole rig respects prefers-reduced-motion by never attaching listeners when the media query matches.

Make it yours

  • Pull strength via the two damping factors in JS (.3/.45) — keep the glyph factor higher than the button's.
  • Widen the proximity zone for a more "aware" feel.
  • Recolor the hover ring via --accent.
  • Cap displacement (the JS clamps at 14px) tighter for dense toolbars.

Gotchas — read before shipping

  • Write coordinates into CSS variables, not style.transform strings — you keep CSS in charge of transitions and reduced-motion overrides.
  • translate3d (not translate) guarantees layer promotion during rapid pointermove.
  • Always clamp the displacement; unclamped magnets fling the button outside its own hit area — a button that dodges the cursor is a dark-pattern meme, not a UI.

Browser support

ChromeSafariFirefoxEdge
111+16.4+113+111+

Pointer events + CSS custom properties — universally supported; the effect is progressive enhancement by construction.

Techniques used in this demo

Search CodeFronts

Loading…