'use client';
import * as React from 'react';
import { cn } from '@/lib/utils';
import styles from './magnetic-button.module.css';
export type MagneticButtonProps =
React.ButtonHTMLAttributes<HTMLButtonElement> & {
strength?: number;
};
const MagneticButton = React.forwardRef<HTMLButtonElement, MagneticButtonProps>(
function MagneticButton(
{
children = 'Explore system',
className,
strength = 4,
type = 'button',
onBlur,
onPointerLeave,
onPointerMove,
style,
...props
},
ref,
) {
const magneticStrength = Math.min(Math.max(strength, 0), 10);
const resetPosition = (button: HTMLButtonElement) => {
button.style.setProperty('--magnetic-x', '0px');
button.style.setProperty('--magnetic-y', '0px');
button.style.setProperty('--pointer-x', '50%');
button.style.setProperty('--pointer-y', '50%');
};
return (
<button
ref={ref}
data-slot="magnetic-button"
type={type}
className={cn(styles.root, className)}
style={style}
onPointerMove={(event) => {
const bounds = event.currentTarget.getBoundingClientRect();
const x = (event.clientX - bounds.left) / bounds.width;
const y = (event.clientY - bounds.top) / bounds.height;
event.currentTarget.style.setProperty('--pointer-x', `${x * 100}%`);
event.currentTarget.style.setProperty('--pointer-y', `${y * 100}%`);
event.currentTarget.style.setProperty(
'--magnetic-x',
`${(x - 0.5) * magneticStrength * 2}px`,
);
event.currentTarget.style.setProperty(
'--magnetic-y',
`${(y - 0.5) * magneticStrength * 2}px`,
);
onPointerMove?.(event);
}}
onPointerLeave={(event) => {
resetPosition(event.currentTarget);
onPointerLeave?.(event);
}}
onBlur={(event) => {
resetPosition(event.currentTarget);
onBlur?.(event);
}}
{...props}
>
<span className={styles.glow} aria-hidden="true" />
<span className={styles.content}>
<span>{children}</span>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M3 8h9M8.5 4.5 12 8l-3.5 3.5" />
</svg>
</span>
</button>
);
},
);
MagneticButton.displayName = 'MagneticButton';
export { MagneticButton };
Magnetic Button
By John Mamanao
Try the interaction, copy the React source files, or download the complete example.
About Magnetic Button
Magnetic Button is an animated React and CSS button by John Mamanao. Try the interaction, copy the React source, or download the example with creator credit.
- Effects and categories
- Hover effects, Animated
- Using this example
- Copy a TSX file or download the original source and standalone demo. Test keyboard focus, motion and contrast in your own project.
Usage, Tailwind and licensing questions
Read the source files
import-examples/magnetic-button.tsx
import {MagneticButton} from "@/components/ui/magnetic-button";export default function Demo(){return <MagneticButton/>}components/ui/magnetic-button.tsx
'use client';
import * as React from 'react';
import { cn } from '@/lib/utils';
import styles from './magnetic-button.module.css';
export type MagneticButtonProps =
React.ButtonHTMLAttributes<HTMLButtonElement> & {
strength?: number;
};
const MagneticButton = React.forwardRef<HTMLButtonElement, MagneticButtonProps>(
function MagneticButton(
{
children = 'Explore system',
className,
strength = 4,
type = 'button',
onBlur,
onPointerLeave,
onPointerMove,
style,
...props
},
ref,
) {
const magneticStrength = Math.min(Math.max(strength, 0), 10);
const resetPosition = (button: HTMLButtonElement) => {
button.style.setProperty('--magnetic-x', '0px');
button.style.setProperty('--magnetic-y', '0px');
button.style.setProperty('--pointer-x', '50%');
button.style.setProperty('--pointer-y', '50%');
};
return (
<button
ref={ref}
data-slot="magnetic-button"
type={type}
className={cn(styles.root, className)}
style={style}
onPointerMove={(event) => {
const bounds = event.currentTarget.getBoundingClientRect();
const x = (event.clientX - bounds.left) / bounds.width;
const y = (event.clientY - bounds.top) / bounds.height;
event.currentTarget.style.setProperty('--pointer-x', `${x * 100}%`);
event.currentTarget.style.setProperty('--pointer-y', `${y * 100}%`);
event.currentTarget.style.setProperty(
'--magnetic-x',
`${(x - 0.5) * magneticStrength * 2}px`,
);
event.currentTarget.style.setProperty(
'--magnetic-y',
`${(y - 0.5) * magneticStrength * 2}px`,
);
onPointerMove?.(event);
}}
onPointerLeave={(event) => {
resetPosition(event.currentTarget);
onPointerLeave?.(event);
}}
onBlur={(event) => {
resetPosition(event.currentTarget);
onBlur?.(event);
}}
{...props}
>
<span className={styles.glow} aria-hidden="true" />
<span className={styles.content}>
<span>{children}</span>
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M3 8h9M8.5 4.5 12 8l-3.5 3.5" />
</svg>
</span>
</button>
);
},
);
MagneticButton.displayName = 'MagneticButton';
export { MagneticButton };
lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
components/ui/magnetic-button.module.css
.root {
--magnetic-x: 0px;
--magnetic-y: 0px;
--pointer-x: 50%;
--pointer-y: 50%;
position: relative;
display: inline-grid;
min-width: 178px;
height: 48px;
padding: 0 18px;
place-items: center;
overflow: hidden;
border: 1px solid rgb(255 255 255 / 12%);
border-radius: 13px;
color: #f7f7f5;
background: #151515;
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 7%),
0 11px 28px rgb(0 0 0 / 18%);
cursor: pointer;
transition:
transform 90ms cubic-bezier(0.25, 0.1, 0.25, 1),
border-color 160ms ease-out,
box-shadow 180ms ease-out;
}
.root:hover {
border-color: rgb(255 255 255 / 22%);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 10%),
0 15px 34px rgb(0 0 0 / 24%);
transform: translateY(-1px);
}
.root:active {
transform: translateY(0) scale(0.98);
}
.root:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.root:disabled {
cursor: not-allowed;
opacity: 0.48;
transform: none;
}
.glow {
position: absolute;
inset: -40%;
background: radial-gradient(
circle at var(--pointer-x) var(--pointer-y),
rgb(255 255 255 / 22%),
rgb(150 154 255 / 8%) 24%,
transparent 48%
);
opacity: 0.18;
pointer-events: none;
transition: opacity 180ms ease-out;
}
.root:hover .glow {
opacity: 1;
}
.content {
position: relative;
display: inline-flex;
z-index: 1;
align-items: center;
gap: 11px;
font-size: 13px;
font-weight: 630;
letter-spacing: -0.01em;
transform: translate(var(--magnetic-x), var(--magnetic-y));
transition: transform 180ms cubic-bezier(0.2, 0, 0, 1);
}
.content svg {
width: 15px;
height: 15px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.4;
transition: transform 180ms cubic-bezier(0.2, 0, 0, 1);
}
.root:hover .content svg {
transform: translateX(2px);
}
@media (prefers-reduced-motion: reduce) {
.root,
.content,
.content svg,
.glow {
transition: none;
}
.root:hover,
.root:active,
.content,
.root:hover .content svg {
transform: none;
}
}
@media (forced-colors: active) {
.root {
border: 1px solid ButtonText;
color: ButtonText;
background: ButtonFace;
box-shadow: none;
}
.glow {
display: none;
}
}
demo.tsx
export { default as default } from "@/import-examples/magnetic-button";CSS
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--sans);
--default-mono-font-family: var(--mono);
}
}
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0 solid;
}
html, :host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1, h2, h3, h4, h5, h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
b, strong {
font-weight: bolder;
}
code, kbd, samp, pre {
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
font-feature-settings: var(--default-mono-font-feature-settings, normal);
font-variation-settings: var(--default-mono-font-variation-settings, normal);
font-size: 1em;
}
small {
font-size: 80%;
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
:-moz-focusring:where(:not(iframe)) {
outline: auto;
}
progress {
vertical-align: baseline;
}
summary {
display: list-item;
}
ol, ul, menu {
list-style: none;
}
img, svg, video, canvas, audio, iframe, embed, object {
display: block;
vertical-align: middle;
}
img, video {
max-width: 100%;
height: auto;
}
button, input, select, optgroup, textarea, ::file-selector-button {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
background-color: transparent;
opacity: 1;
}
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
::file-selector-button {
margin-inline-end: 4px;
}
::placeholder {
opacity: 1;
}
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
::placeholder {
color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, currentcolor 50%, transparent);
}
}
}
textarea {
resize: vertical;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-date-and-time-value {
min-height: 1lh;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
::-webkit-calendar-picker-indicator {
line-height: 1;
}
:-moz-ui-invalid {
box-shadow: none;
}
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
height: auto;
}
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
}
@layer utilities {
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.outline {
outline-style: var(--tw-outline-style);
outline-width: 1px;
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
}
:root {
--background: #fff;
--foreground: #111;
--primary: #171717;
--primary-foreground: #fff;
--secondary: #eee;
--secondary-foreground: #171717;
--muted: #eee;
--muted-foreground: #666;
--accent: #eee;
--accent-foreground: #171717;
--border: #ddd;
--input: #ddd;
--ring: #777;
--destructive: #dc2626;
--destructive-foreground: #fff;
--popover: #fff;
--popover-foreground: #111;
--success: #15803d;
--success-foreground: #fff;
--radius: .625rem;
--hairline: 1px;
}
.dark {
--background: #090909;
--foreground: #fafafa;
--primary: #fafafa;
--primary-foreground: #171717;
--secondary: #262626;
--secondary-foreground: #fafafa;
--muted: #262626;
--muted-foreground: #a3a3a3;
--accent: #262626;
--accent-foreground: #fafafa;
--border: #333;
--input: #333;
--popover: #171717;
--popover-foreground: #fafafa;
}
:root {
--page-max: 1180px;
--page-gutter: clamp(20px, 4vw, 52px);
--sans: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue',
'Segoe UI', sans-serif;
--mono: 'SFMono-Regular', 'SF Mono', 'Cascadia Code', Consolas, monospace;
--bg: #f4f3ef;
--text: #171716;
--surface: #ffffff;
--surface-2: #e9e8e3;
--muted: #66645f;
--line: rgb(23 23 22 / 9%);
--line-strong: rgb(23 23 22 / 20%);
--control-primary-bg: #1d1d1f;
--control-primary-fg: #ffffff;
--control-accent-subtle: #e8e8ea;
--focus-ring: #1d1d1f;
--red: #b23a31;
--sidebar-active: #5d7300;
--warm: #e4e0d7;
--warm-text: #1a1917;
--scrollbar-thumb: rgb(23 23 22 / 32%);
--scrollbar-thumb-hover: rgb(23 23 22 / 48%);
--landing-primary-bg: #1d1d1f;
--landing-primary-text: #ffffff;
--landing-secondary-bg: #e8e8ea;
--landing-secondary-text: #1d1d1f;
--hero-field: #e7e4dd;
--hero-center: rgb(255 255 255 / 88%);
--hero-cool-lustre: rgb(172 191 218 / 34%);
--hero-warm-lustre: rgb(224 194 174 / 38%);
--cta-text: #24231f;
--cta-muted: #716f68;
--cta-accent: #718a48;
--cta-glow: rgb(126 157 82 / 10%);
--cta-wash: rgb(255 255 255 / 46%);
--cta-control-bg: rgb(23 23 22 / 4%);
--cta-control-border: rgb(23 23 22 / 12%);
--cta-control-highlight: rgb(255 255 255 / 68%);
--cta-primary-bg: #1d1d1f;
--cta-primary-text: #ffffff;
--type-micro: 12px;
--type-caption: 13px;
--type-ui: 14px;
--type-body: 16px;
--type-lead: 18px;
}
.dark {
--bg: #0b0b0a;
--text: #f2f1ed;
--surface: #171716;
--surface-2: #222220;
--muted: #9c9b95;
--line: rgb(255 255 255 / 8%);
--line-strong: rgb(255 255 255 / 17%);
--control-primary-bg: #f5f5f7;
--control-primary-fg: #111111;
--control-accent-subtle: #242426;
--focus-ring: #f5f5f7;
--red: #ff9189;
--sidebar-active: #d4ff00;
--scrollbar-thumb: rgb(255 255 255 / 30%);
--scrollbar-thumb-hover: rgb(255 255 255 / 46%);
--landing-primary-bg: #f5f5f7;
--landing-primary-text: #111111;
--landing-secondary-bg: #242426;
--landing-secondary-text: #f5f5f7;
--hero-field: #11110f;
--hero-center: rgb(255 255 255 / 7%);
--hero-cool-lustre: rgb(88 105 132 / 23%);
--hero-warm-lustre: rgb(105 73 58 / 22%);
--cta-text: #f2f1ed;
--cta-muted: #969691;
--cta-accent: #a6bd7d;
--cta-glow: rgb(183 216 135 / 7%);
--cta-wash: rgb(255 255 255 / 3%);
--cta-control-bg: rgb(255 255 255 / 4%);
--cta-control-border: rgb(255 255 255 / 10%);
--cta-control-highlight: rgb(255 255 255 / 4%);
--cta-primary-bg: #f5f5f7;
--cta-primary-text: #111111;
}
.dark .brand img {
filter: invert(1);
mix-blend-mode: screen;
}
.dark .theme-icon-light {
display: none;
}
.dark .theme-icon-dark {
display: block;
}
.dark .preview-theme-light {
display: none;
}
.dark .preview-theme-dark {
display: block;
}
@keyframes changelog-pulse {
0%, 45% {
opacity: 0.72;
transform: scale(0.68);
}
80%, 100% {
opacity: 0;
transform: scale(1.35);
}
}
.dark .changelog-rail-signature img {
filter: invert(1);
mix-blend-mode: screen;
}
@keyframes nacre-motion-sample {
0%, 16% {
left: 8%;
}
70%, 100% {
left: calc(100% - 28px);
}
}
.dark .home-cta-mark img {
filter: invert(1);
}
.dark .docs-search-dialog::backdrop {
background: rgb(0 0 0 / 52%);
}
.dark .docs-search-dialog {
background: color-mix(in srgb, #0b0b0d 92%, transparent) !important;
box-shadow: 0 40px 120px rgb(0 0 0 / 62%), inset 0 1px 0 rgb(255 255 255 / 5%);
}
@keyframes docs-search-enter {
from {
opacity: 0;
transform: translate(-50%, -10px) scale(0.985);
}
to {
opacity: 1;
transform: translate(-50%, 0) scale(1);
}
}
@keyframes docs-search-exit {
from {
opacity: 1;
transform: translate(-50%, 0) scale(1);
}
to {
opacity: 0;
transform: translate(-50%, -6px) scale(0.99);
}
}
.e {
--magnetic-x: 0px;
--magnetic-y: 0px;
--pointer-x: 50%;
--pointer-y: 50%;
position: relative;
display: inline-grid;
min-width: 178px;
height: 48px;
padding: 0 18px;
place-items: center;
overflow: hidden;
border: 1px solid rgb(255 255 255 / 12%);
border-radius: 13px;
color: #f7f7f5;
background: #151515;
box-shadow: inset 0 1px #ffffff12,0 11px 28px #0000002e;
cursor: pointer;
transition: transform 90ms cubic-bezier(.25,.1,.25,1),border-color .16s ease-out,box-shadow .18s ease-out;
}
.e:hover {
border-color: #ffffff38;
box-shadow: inset 0 1px #ffffff1a,0 15px 34px #0000003d;
transform: translateY(-1px);
}
.e:active {
transform: translateY(0) scale(.98);
}
.e:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.e:disabled {
cursor: not-allowed;
opacity: .48;
transform: none;
}
.o {
position: absolute;
inset: -40%;
background: radial-gradient(circle at var(--pointer-x) var(--pointer-y),rgb(255 255 255 / 22%),rgb(150 154 255 / 8%) 24%,transparent 48%);
opacity: .18;
pointer-events: none;
transition: opacity .18s ease-out;
}
.e:hover .o {
opacity: 1;
}
.r {
position: relative;
display: inline-flex;
z-index: 1;
align-items: center;
gap: 11px;
font-size: 13px;
font-weight: 630;
letter-spacing: -.01em;
transform: translate(var(--magnetic-x),var(--magnetic-y));
transition: transform .18s cubic-bezier(.2,0,0,1);
}
.r svg {
width: 15px;
height: 15px;
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.4;
transition: transform .18s cubic-bezier(.2,0,0,1);
}
.e:hover .r svg {
transform: translate(2px);
}
@media (prefers-reduced-motion:reduce) {
.e,.r,.r svg,.o {
transition: none;
}
.e:hover,.e:active,.r,.e:hover .r svg {
transform: none;
}
}
@media (forced-colors:active) {
.e {
border: 1px solid ButtonText;
color: ButtonText;
background: ButtonFace;
box-shadow: none;
}
.o {
display: none;
}
}
@property --tw-rotate-x {
syntax: "*";
inherits: false;
}
@property --tw-rotate-y {
syntax: "*";
inherits: false;
}
@property --tw-rotate-z {
syntax: "*";
inherits: false;
}
@property --tw-skew-x {
syntax: "*";
inherits: false;
}
@property --tw-skew-y {
syntax: "*";
inherits: false;
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-outline-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-rotate-x: initial;
--tw-rotate-y: initial;
--tw-rotate-z: initial;
--tw-skew-x: initial;
--tw-skew-y: initial;
--tw-border-style: solid;
--tw-outline-style: solid;
}
}
}