An install command in npm, pnpm, yarn and bun, with a sliding pill, a copy button and a choice that follows to every block on the page.
$ npm install motion lucide-reactCustomize
Usage
import { InstallTabs } from "@/components/motionkit/install-tabs";
<InstallTabs packages="motion lucide-react" />Motion
The exact curves this component runs, plotted with Motion's own maths.
Pill slide
Package manager chosen
Command swap
Package manager chosen
Documentation, READMEs on the web, library landing pages and getting-started guides: anywhere you'd otherwise write the same install command four times.
The chosen package manager lives in a tiny external store backed by localStorage, read with useSyncExternalStore. Every Install Tabs on the page subscribes to it, so picking pnpm in one block switches them all, and the choice is still there on the next visit. The selected tab has a pill shared through layoutId that springs between tabs, and the command swaps with a short slide via AnimatePresence. On the server it renders npm, so nothing mismatches during hydration.
Works in the App Router as is. Requires Tailwind CSS v4.
One command adds the file and installs its dependencies. Needs a components.json (run npx shadcn@latest init once if you don't have one).
npx shadcn@latest add https://motionkitui.com/r/install-tabs.jsonnpm install motionSave as components/motionkit/install-tabs.tsx.
"use client";
import { useId, useState, useSyncExternalStore, type KeyboardEvent } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Manager = "npm" | "pnpm" | "yarn" | "bun";
type InstallTabsProps = {
/** Packages to install, e.g. "motion lucide-react". */
packages: string;
/** Install as development dependencies. */
dev?: boolean;
className?: string;
};
const managers: Manager[] = ["npm", "pnpm", "yarn", "bun"];
const command = (manager: Manager, packages: string, dev: boolean) =>
manager === "npm" ? `npm install ${dev ? "-D " : ""}${packages}` : `${manager} add ${dev ? "-D " : ""}${packages}`;
// one choice shared by every InstallTabs on the page, and remembered between visits
const STORAGE_KEY = "motionkit:package-manager";
const listeners = new Set<() => void>();
const read = (): Manager => {
try {
const stored = localStorage.getItem(STORAGE_KEY);
return managers.includes(stored as Manager) ? (stored as Manager) : "npm";
} catch {
return "npm";
}
};
const choose = (manager: Manager) => {
try {
localStorage.setItem(STORAGE_KEY, manager);
} catch {}
listeners.forEach((listener) => listener());
};
const subscribe = (listener: () => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
/**
* An install command in npm, pnpm, yarn and bun flavours. The pill slides to the chosen manager, the command
* swaps, and the choice follows to every other install block on the page.
*/
export function InstallTabs({ packages, dev = false, className = "" }: InstallTabsProps) {
const reduceMotion = useReducedMotion();
const id = useId();
const manager = useSyncExternalStore(subscribe, read, () => "npm" as Manager);
const [copied, setCopied] = useState(false);
const text = command(manager, packages, dev);
function onKey(event: KeyboardEvent<HTMLButtonElement>, index: number) {
const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
if (!step) return;
event.preventDefault();
const next = managers[(index + step + managers.length) % managers.length];
choose(next);
document.getElementById(`${id}-${next}`)?.focus();
}
async function copyCommand() {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
} catch {}
}
return (
<div className={`w-full max-w-md overflow-hidden rounded-xl border border-white/10 bg-[#0C1010] text-[#F6F1E8] ${className}`}>
<div className="flex items-center justify-between border-b border-white/[0.08] px-2 py-1.5">
<div role="tablist" aria-label="Package manager" className="flex">
{managers.map((option, index) => (
<button
key={option}
id={`${id}-${option}`}
role="tab"
aria-selected={manager === option}
aria-controls={`${id}-panel`}
tabIndex={manager === option ? 0 : -1}
onClick={() => choose(option)}
onKeyDown={(event) => onKey(event, index)}
className={`relative rounded-md px-2.5 py-1 font-mono text-xs transition-colors focus-visible:outline-2 focus-visible:outline-[#5FAEB4] ${manager === option ? "text-[#F6F1E8]" : "text-[#94A9A7] hover:text-[#DCE3E2]"}`}
>
{manager === option && (
<motion.span layoutId={`${id}-pill`} aria-hidden className="absolute inset-0 rounded-md bg-white/[0.08]" transition={reduceMotion ? { duration: 0 } : { type: "spring", bounce: 0.2, duration: 0.35 }} />
)}
<span className="relative">{option}</span>
</button>
))}
</div>
<button type="button" onClick={copyCommand} className="rounded-md px-2 py-1 text-xs text-[#94A9A7] transition-colors hover:text-[#F6F1E8] focus-visible:outline-2 focus-visible:outline-[#5FAEB4]">
{copied ? "Copied" : "Copy"}
</button>
</div>
<div id={`${id}-panel`} role="tabpanel" aria-labelledby={`${id}-${manager}`} className="relative overflow-hidden px-4 py-3 font-mono text-sm">
<AnimatePresence mode="popLayout" initial={false}>
<motion.code
key={manager}
className="block whitespace-nowrap"
initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: -8 }}
transition={{ duration: 0.18 }}
>
<span className="select-none text-[#5FAEB4]">$ </span>
{text}
</motion.code>
</AnimatePresence>
<span role="status" className="sr-only">{copied ? "Copied" : ""}</span>
</div>
</div>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| packages | string | — | Packages to install, e.g. "motion lucide-react". |
| dev | boolean | false | Install as development dependencies. |
How we audit: the WCAG 2.2 animation checklist and respecting reduced motion in React.
Yes. The file starts with "use client", so you can import it straight into a Server Component page or layout. It works in Vite as is, and in Astro as a React island.
Only the motion package, beside React and Tailwind CSS v4. There is no MotionKit package and no Tailwind config to add.
Yes. Run npx shadcn@latest add https://motionkitui.com/r/install-tabs.json in any project with a components.json. It saves the file to components/motionkit and installs its dependencies.
With reduced motion on, the pill jumps and the commands crossfade without sliding.
Readers use one package manager, so making them choose it on every block is friction. The choice is shared through a small store and saved in localStorage under motionkit:package-manager.
Animated Tabs
Accessible tabs with a sliding indicator and soft content transitions.
Magnetic Dock
A floating icon bar that magnifies and lifts around the pointer, with tooltips and a gliding active indicator.
Command Menu
A ⌘K palette with live filtering, grouped results and a highlight that glides between them.