A collection of links for navigating websites.
Installation
Install the packages
Add the shadcn/ui component
Animate it
The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.
Usage
Import the primitive from Animate UI instead of Radix UI. Everything else stays the same.
import { NavigationMenu } from '@animate-ui/react/primitives/radix-ui';
export function SiteNav() {
return (
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>Learn</NavigationMenu.Trigger>
<NavigationMenu.Content>
<NavigationMenu.Link href="/docs">
Documentation
</NavigationMenu.Link>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link href="/blog">Blog</NavigationMenu.Link>
</NavigationMenu.Item>
<NavigationMenu.Indicator />
</NavigationMenu.List>
<NavigationMenu.Viewport className="h-(--radix-navigation-menu-viewport-height) w-(--radix-navigation-menu-viewport-width)" />
</NavigationMenu.Root>
);
}Coming from the @radix-ui/react-navigation-menu package? Use the subpath: import * as NavigationMenuPrimitive from '@animate-ui/react/primitives/radix-ui/navigation-menu'.
Animation
Radix's Navigation Menu has no Animate UI v1 counterpart: it opens like v1's Dropdown Menu, and moves between items like Animate UI's Menubar.
- Viewport fades and scales in from 0.9, dropping from its top edge, in 220ms (
{ duration: 0.22, ease: [0.215, 0.61, 0.355, 1] }, a soft ease-out), like v1's menu content, and closes the same way before it unmounts. - Moving from one item to another, the Viewport's size follows the new content instead of jumping, on a spring (stiffness 660, damping 52): it starts gently, is most of the way there in ~155ms and eases in without going past, settled by ~300ms, keeping its speed when it changes again before it has landed (never so fast it would go past the next size): Animate UI animates
--radix-navigation-menu-viewport-widthand--radix-navigation-menu-viewport-height, so classes reading them resize smoothly. - Content: moving from one item to another, the content changes in place, like the Menubar's: the previous content leaves 24px away from where the next one comes from (moving to an item on its right, to the left) while it fades out (220ms, a soft ease-out), and the next one comes in from 24px that way on a spring (stiffness 450, damping 43) and fades in behind it (260ms, on a slow start); the Viewport clips them. A copy of the previous content (
aria-hidden,inert) leaves in its place. Going back and forth faster than they settle, each content shows once: what's still leaving goes on from where it is, and back to an item whose content is still leaving, it comes back from there (where it is, as it shows) instead of coming in again. Without a Viewport, each Content fades and scales under its item like the Viewport does. - Clicking a trigger: a mouse reaches a trigger before it clicks it, and that opens its item, or switches the menu to it. A click within 500ms of that keeps the menu open, like Base UI's "patient click" (Radix toggles it closed): the menu the pointer has just opened doesn't close under the click. Later clicks, the next click, clicks on an item a click opened, triggers that are links (
asChild: keeping it open would cancel their navigation), and the keyboard toggle it closed as usual. - The highlights: the triggers, and the links beside them (
navigationMenuTriggerStyle), share one highlight, like a Menubar's: a trigger hovered, focused or open wears its own background (hover:bg-muted,focus:bg-muted,data-open:bg-muted/50…, read from your styles and cleared on the trigger), and that background glides from trigger to trigger on the highlight's spring (stiffness 500, damping 40). It fades in (200ms, on CSS'sease) when it appears from nothing and fades out where it is the same way when no trigger is lit anymore; lit again meanwhile, it comes back from there. The links of each Content share their own the same way. The List and each Content become their own stacking context (isolation: isolate), so a highlight goes under the text and above the background. - Indicator follows the active trigger like v1's menu highlight, on the highlight's spring (stiffness 500, damping 40, quicker than v1's), fading in on it and out 200ms after the menu closes.
- The highlight's color follows the trigger's state on the same curve: open (
data-open:bg-muted/50), then hovered (hover:bg-muted). Without a Viewport, Contents show inside the List, which is its own stacking context for the highlight: az-indexon a Content then orders it among what's inside the List only. - Keyboard navigation between links and triggers moves the focus, and the highlight with it wherever your styles light a focused trigger or link (
focus:bg-muted). - With
forceMount, closed parts stay in the DOM: they play their exit, then are hidden (visibility: hidden). - An item open from the start (
defaultValue) shows with the Viewport's entrance on page load. - Under reduced motion, everything only fades and sizes change at once: moving between items, each content fades in place; the highlights jump from trigger to trigger and from link to link.
API Reference
Every part takes exactly the props of Radix UI's Navigation Menu. Animate UI adds the animation props below.
Root, Sub, List, Item, Trigger, Link
Radix UI props only. Triggers and links take part in the highlights; they need nothing more.
Viewport
Radix UI props, plus:
| Prop | Type | Default |
|---|---|---|
transition? | Transition | { duration: 0.22, ease: [0.215, 0.61, 0.355, 1] } (the resizing: { type: 'spring', stiffness: 660, damping: 52 }) |
exitTransition? | Transition | transition |
Content
Radix UI props, plus:
| Prop | Type | Default |
|---|---|---|
transition? | Transition | { duration: 0.22, ease: [0.215, 0.61, 0.355, 1] } |
exitTransition? | Transition | transition |
Indicator
Radix UI props, plus:
| Prop | Type | Default |
|---|---|---|
transition? | Transition | { type: 'spring', stiffness: 500, damping: 40 } |
exitTransition? | Transition | { type: 'spring', stiffness: 500, damping: 40, delay: 0.2 } |