A collection of links and menus for website navigation.
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 Base UI. Everything else stays the same.
import { NavigationMenu } from '@animate-ui/react/primitives/base-ui/navigation-menu';
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.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner sideOffset={8}>
<NavigationMenu.Popup className="h-(--popup-height) w-(--popup-width)">
<NavigationMenu.Viewport />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
);
}Using the @base-ui/react umbrella import instead? The barrel mirrors it: import { NavigationMenu } from '@animate-ui/react/primitives/base-ui'.
Animation
Base UI's Navigation Menu has no Animate UI v1 counterpart: it opens like v1's Base UI Menu, and moves between items like Animate UI's Menubar.
- Popup fades and scales in from 0.9, from its trigger (
--transform-origin), in 220ms ({ duration: 0.22, ease: [0.215, 0.61, 0.355, 1] }, a soft ease-out), and closes the same way; Base UI keeps it mounted until then, and Animate UI unmounts it once the exit has played. - Moving from one item to another, the Popup moves to the next trigger and its size follows the new content, 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 (the Popup's
transition, if you set one): Base UI resizes it through--popup-width/--popup-height, and Animate UI sets the CSS transition ofwidthandheightinline (Base UI waits for it before it sets the size back to auto); the Positioner's own CSS transition is set aside, and the Popup moves from where it was on Web Animations (itstranslate). Moving on again before it has landed, it leaves from where it is, keeping its speed toward the next item (as much of it as never takes it past that item; sent back the way it came, it turns back at once), its size too, in step with the move (Base UI holds the popup at its size for a frame before it writes the next one: Animate UI writes it at once, timed on that speed). - 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 content stays where it is on screen while the Popup slides over it, and the Popup clips it. 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. On the first opening, the Popup's entrance is enough. - 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, pinned (the pointer leaving doesn't close it), as Base UI keeps it after a hover opened it from closed (its "patient click"): Base UI 1.8 forgets it when the hover switched items, and the click closed the menu the pointer had just opened. Later clicks, the next click, 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. - Closing the menu, the content stays as it showed while the Popup fades out (Base UI removes it first: a copy,
aria-hiddenandinert, holds its place). Its highlight's color follows the trigger's state on the same curve: open (data-popup-open:bg-muted/50), then hovered (hover:bg-muted). - 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). - An item open from the start (
defaultValue) shows with the Popup's entrance on page load. - Under reduced motion, everything only fades, and sizes and places change at once: moving between items, the Popup jumps to the next trigger while 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 Base UI's Navigation Menu. Animate UI adds the animation props below. The render prop of the Popup and the Content is used by the animation.
Root, List, Item, Trigger, Link, Icon, Portal, Positioner, Viewport, Backdrop, Arrow, Content
Base UI props only. Triggers and links take part in the highlights; they need nothing more. A Content changing to another moves like the Menubar's, on fixed timings (see Animation): the Popup's transition only sets how the Popup moves and resizes. The Content's type also accepts transition and exitTransition, but it ignores them.
Popup
Base UI props, plus:
| Prop | Type | Default |
|---|---|---|
transition? | Transition | { duration: 0.22, ease: [0.215, 0.61, 0.355, 1] } (the move and resizing: { type: 'spring', stiffness: 660, damping: 52 }) |
exitTransition? | Transition | transition |