awesome-magnetic-grid
v0.1.0
Published
A physics-driven magnetic repulsion grid React component. Cards spring away from the cursor with inverse-square force, 3D tilt on the closest card, variable card sizes, and a built-in detail overlay.
Downloads
13
Maintainers
Readme
awesome-magnetic-grid
A physics-driven magnetic repulsion grid React component. Cards spring away from the cursor using inverse-square repulsion force, snap back with Hooke's law, and the closest card gets a subtle 3D tilt toward the cursor — all running in a native RAF loop at 60fps, bypassing React's render cycle entirely for zero-jank animation.
Features
- Magnetic repulsion physics — inverse-square force pushes cards away as the cursor approaches, with quadratic falloff at the field boundary
- Spring return — each card snaps back to its rest position via Hooke's law once the cursor moves away
- Frame-rate-independent damping — velocity decay is corrected per frame so behaviour is identical at 30fps or 144fps
- 3D tilt — the card closest to the cursor gets a
rotateX/rotateYtilt proportional to cursor position for depth - Proximity glow — each card's border and box-shadow intensity scales with cursor distance via a
--proxCSS custom property set directly on the DOM node - Variable card sizes —
size: 1(1×1),size: 2(2 columns wide),size: 3(2 rows tall); bin-packed automatically - Metric display — optional big-number stat with label, prominently displayed on the card face and in the detail overlay
- Custom magnetic cursor — replaces the system cursor with a dot + ring + 4-directional field lines
- Built-in detail overlay — click a non-flying card to open an animated full-detail panel; press
Escapeor click outside to close onCardClickoverride — provide your own handler to skip the built-in overlay- Background grid — fine + major grid lines plus centre crosshairs for a technical HUD aesthetic
- ESM + CJS dual build — works in Vite, Next.js, CRA, and any modern bundler
- Full TypeScript types included
Installation
npm install awesome-magnetic-grid
# or
yarn add awesome-magnetic-grid
# or
pnpm add awesome-magnetic-gridSimple Example

Peer dependencies — React ≥ 18 and ReactDOM ≥ 18 must already be installed.
Importing Styles
The component relies on CSS custom properties (--bg, --bg-card, --border, --text, --text-dim, --text-dimmer, --font-display, --font-mono, --font-body) and keyframe animations (pulseRing, overlayIn, panelIn). Import the stylesheet once at your app root:
import 'awesome-magnetic-grid/style.css';For the full typographic effect, load these Google Fonts in your index.html <head>:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Mono:wght@300;400&family=DM+Sans:wght@300;400;500&display=swap"
rel="stylesheet"
/>Falls back to
sans-serif/monospacewithout the fonts — the component still renders correctly.
Quick Start
import { AwesomeMagneticGrid } from 'awesome-magnetic-grid';
import 'awesome-magnetic-grid/style.css';
var cards = [
{ id: 1, title: 'Velocity', subtitle: 'Real-time stream processor', tag: 'CORE', accent: '#00ff88', metric: '12ms', metricLabel: 'Latency' },
{ id: 2, title: 'Spectrum', subtitle: 'Frequency analyser', tag: 'AUDIO', accent: '#7c3aed', metric: '96k', metricLabel: 'Sample Rate' },
{ id: 3, title: 'Axiom', subtitle: 'Rule engine v3', tag: 'LOGIC', accent: '#f97316', size: 2 },
{ id: 4, title: 'Nexus', subtitle: 'Inter-service message bus', tag: 'NETWORK', accent: '#38bdf8', metric: '2.4M', metricLabel: 'msg/s', size: 3 },
{ id: 5, title: 'Quasar', subtitle: 'Distributed query planner', tag: 'QUERY', accent: '#e879f9' },
{ id: 6, title: 'Forge', subtitle: 'Build pipeline orchestrator', tag: 'CI/CD', accent: '#facc15', metric: '4.1s', metricLabel: 'Avg Build' },
{ id: 7, title: 'Cipher', subtitle: 'End-to-end encryption layer', tag: 'SEC', accent: '#f43f5e' },
{ id: 8, title: 'Orbit', subtitle: 'Scheduler & job runner', tag: 'OPS', accent: '#22d3ee', metric: '99.97', metricLabel: '% Uptime' },
];
export default function App() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<AwesomeMagneticGrid cards={cards} />
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| cards | GridCard[] | required | Array of card objects. See GridCard below. |
| cols | number | 4 | Number of grid columns. |
| cardW | number | 220 | Base card width in px. Wide cards (size: 2) are 2 × cardW + gap. |
| cardH | number | 160 | Base card height in px. Tall cards (size: 3) are 2 × cardH + gap. |
| gap | number | 12 | Gap between cards in px. |
| radius | number | 280 | Magnetic repulsion field radius in px. Cards outside this range are unaffected. |
| strength | number | 1.0 | Repulsion force multiplier. Higher = more aggressive push. |
| spring | number | 0.12 | Spring stiffness (Hooke constant). Higher = snappier return to rest. |
| damping | number | 0.72 | Velocity damping per frame (frame-rate corrected). Lower = more oscillation. |
| onCardClick | (card: GridCard) => void | — | If provided, fires on card click and skips the built-in detail overlay. |
GridCard object
| Field | Type | Required | Description |
|---|---|---|---|
| id | number | ✅ | Unique identifier. Used as React key and shown as #001 in the overlay. |
| title | string | ✅ | Main heading on the card and in the detail overlay. |
| subtitle | string | ➖ | Secondary line shown below the title. |
| tag | string | ➖ | Small monospaced badge shown top-left of the card. |
| metric | string | ➖ | Large stat or number displayed prominently (e.g. '12ms', '99.9%'). |
| metricLabel | string | ➖ | Small label below the metric (e.g. 'Latency', 'Uptime'). |
| body | string | ➖ | Body paragraph shown only in the detail overlay. |
| accent | string | ✅ | Hex neon accent colour. Drives glow, borders, the top accent bar, metric colour, and the overlay highlight. |
| size | 1 \| 2 \| 3 | ➖ | 1 = 1×1 (default), 2 = 2 columns wide, 3 = 2 rows tall. Cards are bin-packed automatically. |
Examples
Example 1 — Minimal, all size-1 cards
import { AwesomeMagneticGrid } from 'awesome-magnetic-grid';
import 'awesome-magnetic-grid/style.css';
var cards = [
{ id: 1, title: 'Alpha', accent: '#00ff88' },
{ id: 2, title: 'Beta', accent: '#7c3aed' },
{ id: 3, title: 'Gamma', accent: '#f97316' },
{ id: 4, title: 'Delta', accent: '#38bdf8' },
{ id: 5, title: 'Epsilon', accent: '#e879f9' },
{ id: 6, title: 'Zeta', accent: '#facc15' },
{ id: 7, title: 'Eta', accent: '#f43f5e' },
{ id: 8, title: 'Theta', accent: '#22d3ee' },
];
export default function MinimalDemo() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<AwesomeMagneticGrid cards={cards} cols={4} />
</div>
);
}Example 2 — Mixed sizes, metrics, and tuned physics
Wide and tall cards mixed into a 4-column layout, with a stronger repulsion field and snappier spring return.
import { AwesomeMagneticGrid } from 'awesome-magnetic-grid';
import 'awesome-magnetic-grid/style.css';
var cards = [
{
id: 1, title: 'Revenue', tag: 'FINANCE',
metric: '$4.2M', metricLabel: 'This Quarter',
subtitle: 'Up 18% from last quarter',
accent: '#00ff88', size: 2,
},
{
id: 2, title: 'Uptime', tag: 'OPS',
metric: '99.97', metricLabel: '% SLA',
subtitle: '43 days without incident',
accent: '#22d3ee', size: 3,
},
{
id: 3, title: 'Users', tag: 'GROWTH',
metric: '1.4M', metricLabel: 'Active Monthly',
accent: '#a855f7',
},
{
id: 4, title: 'Latency', tag: 'PERF',
metric: '8ms', metricLabel: 'p99',
accent: '#f97316',
},
{
id: 5, title: 'Deploys', tag: 'CI/CD',
metric: '312', metricLabel: 'Last 30 Days',
subtitle: 'Zero rollbacks',
accent: '#facc15',
},
{
id: 6, title: 'Errors', tag: 'MONITOR',
metric: '0.02', metricLabel: '% Error Rate',
accent: '#f43f5e',
},
{
id: 7, title: 'Storage', tag: 'INFRA',
metric: '18TB', metricLabel: 'Used / 40TB',
accent: '#38bdf8',
},
{
id: 8, title: 'API Calls', tag: 'GATEWAY',
metric: '84M', metricLabel: 'Per Day',
accent: '#e879f9', size: 2,
},
];
export default function DashboardDemo() {
return (
<div style={{ width: '100vw', height: '100vh' }}>
<AwesomeMagneticGrid
cards={cards}
cols={4}
cardW={220}
cardH={160}
radius={320}
strength={1.4}
spring={0.18}
damping={0.68}
/>
</div>
);
}Example 3 — onCardClick with a custom side panel
Bypasses the built-in overlay and renders a custom drawer instead. Uses a smaller card size and tighter physics for a compact feel.
import { useState } from 'react';
import { AwesomeMagneticGrid } from 'awesome-magnetic-grid';
import 'awesome-magnetic-grid/style.css';
var cards = [
{ id: 1, title: 'Photon', subtitle: 'Light-speed transport layer', tag: 'NET', accent: '#00ff88', body: 'Zero-copy packet forwarding with eBPF offload and DPDK ring buffers for sub-microsecond switching.' },
{ id: 2, title: 'Prism', subtitle: 'Spectral analytics engine', tag: 'DATA', accent: '#7c3aed', body: 'Decomposes multi-dimensional metrics into frequency bands for anomaly isolation at any granularity.' },
{ id: 3, title: 'Helios', subtitle: 'Solar-powered edge node', tag: 'EDGE', accent: '#f97316', body: 'Off-grid compute node harvesting 40W from photovoltaic cells, with supercapacitor burst storage.' },
{ id: 4, title: 'Pulsar', subtitle: 'Rotating event log', tag: 'LOG', accent: '#38bdf8', body: 'Append-only structured event store with magnetic-head inspired sequential read optimisation.' },
{ id: 5, title: 'Flux', subtitle: 'Real-time state sync', tag: 'SYNC', accent: '#e879f9', body: 'CRDTs over WebTransport ensure eventual consistency across 150+ global edge PoPs in under 20ms.' },
{ id: 6, title: 'Anchor', subtitle: 'Distributed consensus', tag: 'RAFT', accent: '#facc15', body: 'Modified Raft implementation with pre-vote phase to eliminate spurious leader elections.' },
];
var drawerStyle = {
position: 'fixed',
top: 0,
right: 0,
width: '360px',
height: '100vh',
background: '#0a0a0a',
borderLeft: '1px solid rgba(255,255,255,0.08)',
padding: '48px 32px',
zIndex: 500,
display: 'flex',
flexDirection: 'column',
gap: '20px',
color: '#fff',
fontFamily: 'sans-serif',
overflowY: 'auto',
};
export default function CustomPanelDemo() {
var [selected, setSelected] = useState(null);
return (
<div style={{ width: '100vw', height: '100vh' }}>
<AwesomeMagneticGrid
cards={cards}
cols={3}
cardW={200}
cardH={150}
radius={240}
strength={0.9}
onCardClick={function(card) {
setSelected(function(prev) { return prev && prev.id === card.id ? null : card; });
}}
/>
{selected && (
<div style={drawerStyle}>
{/* Close */}
<button
onClick={function() { setSelected(null); }}
style={{
alignSelf: 'flex-end', background: 'transparent',
border: '1px solid rgba(255,255,255,0.15)',
color: '#fff', borderRadius: 4,
width: 28, height: 28, cursor: 'pointer', fontSize: 14,
}}
>✕</button>
{/* Accent bar */}
<div style={{ height: 2, borderRadius: 1, background: selected.accent, boxShadow: '0 0 12px ' + selected.accent }} />
{/* Tag */}
<span style={{
alignSelf: 'flex-start',
background: selected.accent + '18',
border: '1px solid ' + selected.accent + '44',
color: selected.accent,
borderRadius: 2, padding: '3px 10px',
fontSize: 10, fontWeight: 700,
letterSpacing: '0.2em', textTransform: 'uppercase',
fontFamily: 'monospace',
}}>{selected.tag}</span>
{/* Title */}
<div style={{ fontSize: 36, fontWeight: 700, lineHeight: 1.1, letterSpacing: '0.04em' }}>
{selected.title}
</div>
{/* Subtitle */}
<div style={{ fontSize: 13, color: selected.accent, fontWeight: 500 }}>
{selected.subtitle}
</div>
{/* Divider */}
<div style={{ height: 1, background: 'rgba(255,255,255,0.08)' }} />
{/* Body */}
{selected.body && (
<p style={{ fontSize: 13, lineHeight: 1.75, color: 'rgba(200,200,200,0.7)', fontWeight: 300 }}>
{selected.body}
</p>
)}
{/* ID */}
<div style={{ marginTop: 'auto', fontSize: 10, color: 'rgba(255,255,255,0.2)', fontFamily: 'monospace', letterSpacing: '0.15em' }}>
OBJECT #{String(selected.id).padStart(3, '0')}
</div>
</div>
)}
</div>
);
}Physics Tuning Guide
| Goal | Adjustment |
|---|---|
| More aggressive repulsion | Increase strength (try 1.5–2.0) |
| Larger affected area | Increase radius (try 350–450) |
| Snappier return to rest | Increase spring (try 0.18–0.25) |
| More oscillation / bounce | Decrease damping (try 0.60–0.68) |
| Stiff, quick settle | Increase damping (try 0.80–0.88) |
| Gentler, floatier feel | Decrease strength + spring, increase damping |
CSS Custom Properties
The stylesheet exposes these variables — override in your own :root:
:root {
--bg: #080808;
--bg-card: #101010;
--border: rgba(255, 255, 255, 0.08);
--text: #f0f0f0;
--text-dim: rgba(255, 255, 255, 0.45);
--text-dimmer: rgba(255, 255, 255, 0.2);
--font-display: 'Bebas Neue', sans-serif;
--font-mono: 'DM Mono', monospace;
--font-body: 'DM Sans', sans-serif;
}License
MIT
