01 / Controls
Magnetic Tabs
Familiar tabs with a gentle pull toward the pointer. Selection stays clear and keyboard navigation remains immediate.
Everything important is close by.
Your download is ready and notifications are on.
"use client"
import { MagneticTabs } from "mischief-ui/magnetic-tabs"
const items = [
{
value: "overview",
label: "Overview",
content: (
<div>
<p className="font-semibold">Everything important is close by.</p>
<p className="text-muted-foreground mt-1">
Your download is ready and notifications are on.
</p>
</div>
),
},
{
value: "activity",
label: "Activity",
content: (
<div>
<p className="font-semibold">Two files finished downloading.</p>
<p className="text-muted-foreground mt-1">
Nothing needs your attention right now.
</p>
</div>
),
},
{
value: "settings",
label: "Settings",
content: (
<div>
<p className="font-semibold">Notifications are on.</p>
<p className="text-muted-foreground mt-1">
We will only tell you when something is worth seeing.
</p>
</div>
),
},
]
export function MagneticTabsDemo() {
return (
<div className="demo-content">
<MagneticTabs items={items} />
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/magnetic-tabsimport { MagneticTabs } from "mischief-ui/magnetic-tabs"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { Tabs } from "@base-ui/react/tabs"import { motion, useMotionValue, useReducedMotion, useSpring,} from "motion/react"import { cn } from "@/lib/utils" export interface MagneticTabItem { value: stringUsage
const items = [
{ value: "overview", label: "Overview", content: <p>Ready to go.</p> },
{ value: "activity", label: "Activity", content: <p>No new activity.</p> },
]
export function Example() {
return <MagneticTabs items={items} />
}What it needs installed
This is one of the seven components that reach for something beyond React. Base UI supplies the tab semantics -- roving focus, the tab and panel relationship, arrow key movement -- and Motion drives the indicator. Both are optional peers, so they are only installed if you ask for them.
npm install mischief-ui @base-ui/react motionThe root import deliberately does not carry it, because a barrel holding it would fail for everyone who had not installed those two. That is the trade: a subpath import here, and no unexpected dependencies anywhere else.
The magnetism, and doing without it
The indicator is spring-driven and leans towards the pointer as it moves across a tab, then settles when the pointer leaves. It is a stiff, light spring, so it arrives quickly rather than wobbling -- the effect should read as responsive, not bouncy.
When the reader has asked for reduced motion the lean is not applied at all and the indicator moves straight to the selected tab. Nothing about which tab is selected, or how it is reached from the keyboard, depends on any of this.
API
itemsMagneticTabItem[]Labels, values, panels, and disabled states.defaultValuestringThe initially selected tab.valuestringThe selected value when controlled.onValueChange(value: string) => voidRuns when selection changes.classNamestringClasses for the root element.MagneticTabItem
valuestringIdentifies the tab. What value and onValueChange speak in.labelReactNodeThe tab itself.contentReactNodeThe panel shown while the tab is selected.disabledbooleanListed but unselectable, and skipped by the arrow keys.Accessibility
Base UI supplies tab semantics, arrow-key navigation, focus handling, and panel relationships. Pointer attraction is removed when reduced motion is enabled.