Magnetic <mv-magnetic>
마그네틱 래퍼: 자식 요소가 가까운 마우스 쪽으로 부드러운 감쇠와 함께 기울고, 호버 중에는 조금 따라가다가 스프링처럼 되돌아옵니다.
작동 방식
마그네틱 래퍼: 자식 요소가 가까운 마우스 쪽으로 부드러운 감쇠와 함께 기울고, 호버 중에는 조금 따라가다가 스프링처럼 되돌아옵니다. 대부분의 마그네틱 효과와 달리 포인터 아래에서 미끄러져 나가지 않으므로 호버와 클릭이 정확하게 유지되며, 움직임은 몇 픽셀에 그칩니다.
| 카테고리 | 커서 |
|---|---|
| 유형 | Web Component (<mv-magnetic>) |
| 상태 | 안정 |
| Keywords | safe-rewrite, magnetic, magnet, cursor, hover, spring, parallax, cta, hit-area |
When to use
- A primary CTA or icon button should lean toward the cursor and spring back
- Nav items or social links need a tactile hover with extra parallax on their inner content
- A hero button should feel alive on hover without making it harder to click
Avoid when
- Every link and button of a nav or dense toolbar needs a hover; a gliding highlight beats many moving targets → use Cursor Snap instead
- The whole cursor should be replaced by a custom follower → use Cursor Follower instead
- A card should tilt in 3D toward the pointer rather than translate → use Tilt Card instead
설치
node scripts/add.mjs magnetic --out ./src/marvelousMarvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["magnetic"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/element.js, core/motion.js, components/magnetic/magnetic.js, components/magnetic/magnetic.css.
사용법
빠른 시작, 동작하는 가장 작은 마크업:
<mv-magnetic>
<a class="mv-button" href="/signup">Start free trial</a>
</mv-magnetic>기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:
<div id="mv-mag-demo" style="display:grid;gap:2rem;justify-items:center;text-align:center">
<style>
#mv-mag-demo .mag-orb { display:grid; place-items:center; width:8.5rem; height:8.5rem; border-radius:50%; background:var(--mv-fg); color:var(--mv-bg); text-decoration:none; font:600 .95rem/1.25 var(--mv-font-sans); box-shadow:var(--mv-shadow-lg); transition:box-shadow var(--mv-duration-normal) var(--mv-ease-out); }
#mv-mag-demo mv-magnetic[data-active] .mag-orb { box-shadow:var(--mv-shadow-xl); }
#mv-mag-demo .mag-orb:focus-visible { outline:2px solid var(--mv-accent); outline-offset:4px; }
#mv-mag-demo .mag-row { display:flex; gap:.5rem; align-items:center; justify-content:center; flex-wrap:wrap; }
#mv-mag-demo[data-rest] mv-magnetic { outline:1px dashed color-mix(in oklab, var(--mv-accent) 55%, transparent); outline-offset:0; border-radius:var(--mv-radius-sm); }
#mv-mag-demo .mag-note { color:var(--mv-fg-muted); font-size:.8rem; max-width:32rem; margin:0; }
</style>
<mv-magnetic strength="0.35" radius="96" max="18">
<a class="mag-orb" href="#contact"><span data-magnetic-inner>Get in<br>touch →</span></a>
</mv-magnetic>
<div class="mag-row">
<mv-magnetic><a class="mv-button" href="#start">Start free trial</a></mv-magnetic>
<mv-magnetic><a class="mv-button" data-variant="ghost" href="#sales">Talk to sales</a></mv-magnetic>
</div>
<div class="mag-row" aria-label="Social links" role="group">
<mv-magnetic radius="32" max="6" inner="0.5">
<a class="mv-button" data-variant="outline" data-size="icon" data-shape="round" href="#instagram" aria-label="Instagram"><svg data-magnetic-inner aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="5"/><circle cx="12" cy="12" r="4"/><path d="M17.5 6.5h.01"/></svg></a>
</mv-magnetic>
<mv-magnetic radius="32" max="6" inner="0.5">
<a class="mv-button" data-variant="outline" data-size="icon" data-shape="round" href="#linkedin" aria-label="LinkedIn"><svg data-magnetic-inner aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-4 0v7h-4v-7a6 6 0 0 1 6-6zM2 9h4v12H2z"/><circle cx="4" cy="4" r="2"/></svg></a>
</mv-magnetic>
<mv-magnetic radius="32" max="6" inner="0.5">
<a class="mv-button" data-variant="outline" data-size="icon" data-shape="round" href="#dribbble" aria-label="Dribbble"><svg data-magnetic-inner aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M19.1 5.2C15.4 9.3 9.7 10.9 2.3 10.4M21.9 12.8c-4.4-1-8.2-.6-11.5 1.6-2.3 1.6-4 3.8-5.1 6.4M8.6 2.7c3.2 3.6 5.4 7.4 6.9 11.6 1 2.7 1.6 5.4 1.8 7.5"/></svg></a>
</mv-magnetic>
<mv-magnetic radius="32" max="6" inner="0.5">
<a class="mv-button" data-variant="outline" data-size="icon" data-shape="round" href="#github" aria-label="GitHub"><svg data-magnetic-inner aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 19c-4.3 1.4-4.3-2.5-6-3m12 5v-3.5c0-1 .1-1.4-.5-2 2.8-.3 5.5-1.4 5.5-6a4.6 4.6 0 0 0-1.3-3.2 4.2 4.2 0 0 0-.1-3.2s-1.1-.3-3.5 1.3a12.3 12.3 0 0 0-6.2 0C6.5 2.8 5.4 3.1 5.4 3.1a4.2 4.2 0 0 0-.1 3.2A4.6 4.6 0 0 0 4 9.5c0 4.6 2.7 5.7 5.5 6-.6.6-.6 1.2-.5 2V21"/></svg></a>
</mv-magnetic>
</div>
<label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="mv-mag-rest"> Show resting boxes</label>
<p class="mag-note">Bring a mouse near: each target leans in, follows a little, then springs back. It never slides out from under the pointer, so hover and clicks stay exact. Tab through: focus moves nothing.</p>
<script type="module">
const root = document.getElementById("mv-mag-demo");
document.getElementById("mv-mag-rest").addEventListener("change", (e) => root.toggleAttribute("data-rest", e.target.checked));
</script>
</div>API
Attributes
| Name | 유형 | Default | Description |
|---|---|---|---|
strength | number | 0.3 | Share of the pointer's offset from the center that the child leans by (0-1). |
radius | number | 64 | Attraction margin around the resting box (px); the pull fades smoothly to zero at its edge. |
inner | number | 0.4 | Extra movement of [data-magnetic-inner] (multiplier, parallax). |
max | number | 12 | Maximum displacement (px, 0 = unlimited). |
disabled | boolean | Disables the attraction (the child springs back). | |
data-active | (state) | Set by the component while the pointer is inside the zone (for styling). |
Content structure
| Name | Description |
|---|---|
(child) | The element that leans (one button, link or card). |
[data-magnetic-inner] | Descendant(s) that move further. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-magnetic-x / -y | Current offset (px), exposed for your own effects. | |
--mv-magnetic-inner | 0.4 | Parallax multiplier (driven by the inner attribute). |
Accessibility
Purely visual: the child stays a real link or button, and only its translate changes, so layout, focus order and the focus ring are untouched. Keyboard focus never moves anything. Hit area: whenever the lean puts the target under the pointer, the moving target is clamped so the pointer stays on it (a few px inside its edge), so springs, lag or overshoot never cause hover flicker or a missed click; the pull fades smoothly to zero at the zone edge, so nothing jumps. Pointer effect only with a fine hovering pointer ((hover: hover) and (pointer: fine)), touch is ignored. Reduced motion (OS or data-motion="reduce"): no movement at all, offsets reset to zero. Forced colors: unaffected, the component adds no color. The system cursor is never hidden. Performance: all instances share one pointermove listener and one frame loop that reads every box before writing, uses translate only, and stops as soon as everything rests; everything is removed with the last instance.