Components
Sliding Capsule Nav
A modern navigation component with fluid layout animations, intelligent active state detection, and z-indexed layering.
Fluid Navigation
A ghost tab appears when hovering over a tab, providing a smooth transition to the active state. And primary tab slides into place when you click the tab you want to navigate to with a spring animation.
'use client';import {NavTab,SlidingCapsuleNav,} from '@/components/satisui/sliding-capsule-nav';import { Book, Contact, DollarSign, Home, Newspaper } from 'lucide-react';import { useState } from 'react';const tabs: NavTab[] = [ { title: 'Home', url: '/home', icon: <Home size={14} /> }, { title: 'Pricing', url: '/pricing', icon: <DollarSign size={14} /> }, { title: 'Docs', url: '/docs', icon: <Book size={14} /> }, { title: 'Blog', url: '/blog', icon: <Newspaper size={14} /> }, { title: 'Contact', url: '/contact', icon: <Contact size={14} /> },];export const DemoSlidingCapsuleNav = () => {const [activeTab, setActiveTab] = useState('/home');return ( <div className='flex w-full flex-col items-center justify-center p-12 py-24'> <div className='flex flex-col items-center gap-4 bg-secondary p-1.5 rounded-full'> <SlidingCapsuleNav tabs={tabs} currentTab={activeTab} onChange={setActiveTab} className='p-2' /> </div> </div>);};Installation
CLI (Recommended)
npx shadcn@latest add https://satisui.xyz/r/sliding-capsule-nav.jsonManual
- Install the required dependencies:
npm install motion- Copy the source code into
components/satisui/sliding-capsule-nav.tsx:
'use client';
import * as React from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { motion } from 'motion/react';
import { cn } from '@/lib/utils';
export interface NavTab {
title: string;
url: string;
icon?: React.ReactNode;
}
interface SlidingCapsuleNavProps {
tabs: NavTab[];
className?: string;
activeTabClassName?: string;
tabClassName?: string;
layoutId?: string;
currentTab?: string;
onChange?: (url: string) => void;
}
export const SlidingCapsuleNav = ({
tabs,
className,
activeTabClassName,
tabClassName,
layoutId = 'capsule-nav',
currentTab,
onChange,
}: SlidingCapsuleNavProps) => {
const pathname = usePathname();
const [hoveredTab, setHoveredTab] = React.useState<string | null>(null);
const activeTabId = React.useMemo(() => {
if (currentTab) return currentTab;
const sortedTabs = [...tabs].sort((a, b) => b.url.length - a.url.length);
return sortedTabs.find((tab) => pathname?.startsWith(tab.url))?.url || null;
}, [pathname, tabs, currentTab]);
const handleLinkClick = (e: React.MouseEvent, url: string) => {
if (onChange) {
e.preventDefault();
onChange(url);
}
};
return (
<nav
className={cn(
'relative flex items-center gap-1 rounded-full border bg-background p-1 shadow-sm',
className,
)}
onMouseLeave={() => setHoveredTab(null)}
>
{tabs.map((tab) => {
const isActive = activeTabId === tab.url;
const isHovered = hoveredTab === tab.url;
return (
<Link
key={tab.url}
href={tab.url}
onClick={(e) => handleLinkClick(e, tab.url)}
onMouseEnter={() => setHoveredTab(tab.url)}
className={cn(
'relative z-10 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium transition-colors duration-200',
'rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
isActive
? 'text-primary-foreground'
: 'text-muted-foreground hover:text-foreground',
tabClassName,
)}
aria-current={isActive ? 'page' : undefined}
>
{isActive && (
<motion.div
layoutId={`${layoutId}-active`}
transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
className={cn(
'absolute inset-0 z-10 rounded-full bg-primary shadow-md',
activeTabClassName,
)}
/>
)}
{isHovered && (
<motion.div
layoutId={`${layoutId}-hover`}
transition={{ type: 'spring', bounce: 0.2, duration: 0.6 }}
className='absolute inset-0 z-0 rounded-full bg-muted'
/>
)}
<span className='relative z-20 flex items-center gap-2'>
{tab.icon}
<span>{tab.title}</span>
</span>
</Link>
);
})}
</nav>
);
};Usage
The component is designed to work automatically with Next.js routing (usePathname). Simply define your tabs and drop the component into your layout.
import {
SlidingCapsuleNav,
type NavTab,
} from '@/components/ui/sliding-capsule-nav';
import { Home, Settings, User } from 'lucide-react';
const tabs: NavTab[] = [
{ title: 'Home', url: '/', icon: <Home size={16} /> },
{ title: 'Profile', url: '/profile', icon: <User size={16} /> },
{ title: 'Settings', url: '/settings', icon: <Settings size={16} /> },
];
export function Navbar() {
return (
<div className='flex justify-center py-4'>
<SlidingCapsuleNav tabs={tabs} />
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
tabs | NavTab[] | Required | Array of navigation items { title, url, icon }. |
className | string | undefined | ClassName for the outer nav container. |
tabClassName | string | undefined | ClassName for individual tab items. |
activeTabClassName | string | undefined | ClassName for the active capsule indicator (background). |
layoutId | string | 'capsule-nav' | Unique ID for Framer Motion layout animations. |
currentTab | string | undefined | Controlled Mode: Override the active tab URL manually. |
onChange | (url: string) => void | undefined | Controlled Mode: Callback when a tab is clicked. Prevents navigation. |