Navigation Menu
A navigation menu with floating panels.
<script lang="ts">
import { NavigationMenu } from '@shardsui/svelte/navigation-menu'
type LinkItem = { href: string; title: string; description: string }
const topics: LinkItem[] = [
{
href: '#',
title: 'Frontend',
description: 'From first components to full working apps.'
},
{ href: '#', title: 'Design', description: 'Type scale, color tokens, and layout grids.' },
{
href: '#',
title: 'Accessibility',
description: 'Semantic HTML, ARIA, and keyboard nav.'
},
{ href: '#', title: 'Motion', description: 'Easing, timing, and interface animation.' }
]
const resources: LinkItem[] = [
{
href: '#',
title: 'Docs',
description: 'Guides and reference for every feature.'
},
{ href: '#', title: 'Changelog', description: 'What shipped and when.' },
{ href: '#', title: 'Blog', description: 'Updates, tips, and release notes.' }
]
</script>
<NavigationMenu.Root class="min-w-max rounded-lg bg-gray-50 p-1 text-gray-900">
<NavigationMenu.List class="relative flex gap-px">
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="flex h-8 items-center justify-center gap-1.5 rounded-md bg-gray-50 px-2 text-sm font-normal text-gray-900 no-underline select-none hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100 min-[501px]:px-3"
>
Products
<NavigationMenu.Icon
class="transition-transform duration-200 ease-[ease] data-popup-open:rotate-180"
>
{@render chevronDownIcon()}
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content
class="h-full w-[calc(100vw-40px)] p-4 transition-[opacity,translate] duration-[calc(var(--duration)*0.5),var(--duration)] ease-[ease,var(--easing)] data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:data-[activation-direction=left]:translate-x-1/2 data-starting-style:data-[activation-direction=left]:-translate-x-1/2 data-ending-style:data-[activation-direction=right]:-translate-x-1/2 data-starting-style:data-[activation-direction=right]:translate-x-1/2 min-[500px]:w-max min-[500px]:min-w-100"
>
<ul class="grid list-none grid-cols-1 min-[500px]:grid-cols-[13rem_13rem]">
{#each topics as item (item.title)}
{@render card(item)}
{/each}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="flex h-8 items-center justify-center gap-1.5 rounded-md bg-gray-50 px-2 text-sm font-normal text-gray-900 no-underline select-none hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100 min-[501px]:px-3"
>
Resources
<NavigationMenu.Icon
class="transition-transform duration-200 ease-[ease] data-popup-open:rotate-180"
>
{@render chevronDownIcon()}
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content
class="h-full w-[calc(100vw-40px)] p-4 transition-[opacity,translate] duration-[calc(var(--duration)*0.5),var(--duration)] ease-[ease,var(--easing)] data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:data-[activation-direction=left]:translate-x-1/2 data-starting-style:data-[activation-direction=left]:-translate-x-1/2 data-ending-style:data-[activation-direction=right]:-translate-x-1/2 data-starting-style:data-[activation-direction=right]:translate-x-1/2 min-[500px]:w-max min-[500px]:min-w-100"
>
<ul class="flex max-w-100 list-none flex-col justify-center">
{#each resources as item (item.title)}
{@render card(item)}
{/each}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link
class="flex h-8 items-center justify-center gap-1.5 rounded-md bg-gray-50 px-2 text-sm font-normal text-gray-900 no-underline select-none hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 min-[501px]:px-3"
href="#">Pricing</NavigationMenu.Link
>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
sideOffset={10}
collisionPadding={{ top: 5, bottom: 5, left: 20, right: 20 }}
collisionAvoidance={{ side: 'none' }}
style="--duration:0.35s;--easing:cubic-bezier(0.22, 1, 0.36, 1);"
class="box-border h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-(--duration) ease-(--easing) before:absolute before:content-[''] data-instant:transition-none data-[side=bottom]:before:inset-x-0 data-[side=bottom]:before:-top-2.5 data-[side=bottom]:before:h-2.5 data-[side=left]:before:inset-y-0 data-[side=left]:before:-right-2.5 data-[side=left]:before:w-2.5 data-[side=right]:before:inset-y-0 data-[side=right]:before:-left-2.5 data-[side=right]:before:w-2.5 data-[side=top]:before:inset-x-0 data-[side=top]:before:-bottom-2.5 data-[side=top]:before:h-2.5"
>
<NavigationMenu.Popup
class="relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-lg bg-gray-50 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[opacity,transform,width,height,scale] duration-(--duration) ease-(--easing) data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-ending-style:ease-[ease] data-starting-style:scale-90 data-starting-style:opacity-0"
>
<NavigationMenu.Arrow
class="flex transition-[left,right] duration-(--duration) ease-(--easing) data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
{@render arrow()}
</NavigationMenu.Arrow>
<NavigationMenu.Viewport class="relative size-full overflow-hidden rounded-[inherit]" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
{#snippet chevronDownIcon()}
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M5.75 9.5L12 15.75L18.25 9.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{/snippet}
{#snippet arrow()}
<svg width="20" height="10" viewBox="0 0 20 10" fill="none">
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
class="fill-gray-50"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
class="fill-gray-200"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
/>
</svg>
{/snippet}
{#snippet card(item: LinkItem)}
<li>
<NavigationMenu.Link
href={item.href}
class="relative block size-full rounded-md p-2 text-left text-inherit no-underline hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 min-[501px]:p-3"
>
<h3 class="m-0 mb-1 text-sm leading-4 font-normal">{item.title}</h3>
<p class="m-0 text-sm/5 text-gray-500">{item.description}</p>
</NavigationMenu.Link>
</li>
{/snippet}Anatomy
<script>
import { NavigationMenu } from '@shardsui/svelte/navigation-menu'
</script>
<NavigationMenu.Root>
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger>
<NavigationMenu.Icon />
</NavigationMenu.Trigger>
<NavigationMenu.Content>
<NavigationMenu.Link />
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Backdrop />
<NavigationMenu.Positioner>
<NavigationMenu.Popup>
<NavigationMenu.Arrow />
<NavigationMenu.Viewport />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>Examples
Nested submenus
Nest a <NavigationMenu.Root> inside a <NavigationMenu.Content> for a second level. The nested root brings its own Portal, Positioner and Viewport, so the submenu floats free of the parent panel.
<script lang="ts">
import { NavigationMenu } from '@shardsui/svelte/navigation-menu'
type LinkItem = { href: string; title: string; description: string }
const topics = [
{
href: '#',
title: 'Frontend',
description: 'From first components to full working apps.'
},
{ href: '#', title: 'Design', description: 'Type scale, color tokens, and layout grids.' }
]
const resources = [
{
href: '#',
title: 'Docs',
description: 'Guides and reference for every feature.'
},
{ href: '#', title: 'Changelog', description: 'What shipped and when.' },
{ href: '#', title: 'Blog', description: 'Updates, tips, and release notes.' }
]
</script>
<NavigationMenu.Root class="min-w-max rounded-lg bg-gray-50 p-1 text-gray-900">
<NavigationMenu.List class="relative flex gap-px">
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="m-0 box-border flex h-8 items-center justify-center gap-1.5 rounded-md bg-gray-50 px-2 text-sm/6 font-normal text-gray-900 no-underline select-none hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100 sm:px-3 sm:text-sm"
>
Products
<NavigationMenu.Icon
class="transition-transform duration-200 ease-[ease] data-popup-open:rotate-180"
>
{@render chevronDownIcon()}
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content
class="h-full w-[calc(100vw-40px)] p-4 transition-[opacity,transform,translate] duration-(--duration) ease-(--easing) data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:data-[activation-direction=left]:translate-x-1/2 data-starting-style:data-[activation-direction=left]:-translate-x-1/2 data-ending-style:data-[activation-direction=right]:-translate-x-1/2 data-starting-style:data-[activation-direction=right]:translate-x-1/2 min-[32rem]:w-max min-[32rem]:min-w-100"
>
<ul class="grid list-none grid-cols-1 gap-0 sm:grid-cols-[12rem_12rem]">
{#each topics as item (item.title)}
{@render card(item)}
{/each}
<li>
<NavigationMenu.Root orientation="vertical">
<NavigationMenu.List>
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="relative block w-full rounded-md p-2 text-left text-inherit no-underline hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 data-popup-open:bg-gray-100 sm:p-3"
>
<span class="m-0 mb-1 text-sm/5 font-normal">Resources</span>
<p class="m-0 text-sm/5 text-gray-500">Guides, changelog, and blog posts.</p>
<NavigationMenu.Icon
class="absolute top-1/2 right-2.5 flex size-4 -translate-y-1/2 items-center justify-center transition-transform duration-200 ease-[ease] data-popup-open:rotate-180"
>
{@render chevronRightIcon()}
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content
class="h-full w-[calc(100vw-40px)] p-4 transition-[opacity,transform,translate] duration-(--duration) ease-(--easing) data-ending-style:opacity-0 data-starting-style:opacity-0 data-ending-style:data-[activation-direction=left]:translate-x-1/2 data-starting-style:data-[activation-direction=left]:-translate-x-1/2 data-ending-style:data-[activation-direction=right]:-translate-x-1/2 data-starting-style:data-[activation-direction=right]:translate-x-1/2 min-[32rem]:w-max min-[32rem]:min-w-100"
>
<ul class="flex max-w-100 flex-col justify-center">
{#each resources as item (item.title)}
{@render card(item)}
{/each}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
sideOffset={8}
alignOffset={-8}
align="end"
side="right"
class="box-border h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-(--duration) ease-(--easing) before:absolute before:content-[''] data-instant:transition-none data-[side=bottom]:before:inset-x-0 data-[side=bottom]:before:-top-2.5 data-[side=bottom]:before:h-2.5 data-[side=left]:before:inset-y-0 data-[side=left]:before:-right-2.5 data-[side=left]:before:w-2.5 data-[side=right]:before:inset-y-0 data-[side=right]:before:-left-2.5 data-[side=right]:before:w-2.5 data-[side=top]:before:inset-x-0 data-[side=top]:before:-bottom-2.5 data-[side=top]:before:h-2.5"
style="--duration:0.35s;--easing:cubic-bezier(0.22,1,0.36,1);"
>
<NavigationMenu.Popup
class="relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-lg bg-gray-50 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[opacity,transform,width,height,scale,translate] duration-(--duration) ease-(--easing) data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:duration-150 data-ending-style:ease-[ease] data-starting-style:scale-95 data-starting-style:opacity-0"
>
<NavigationMenu.Viewport class="relative size-full overflow-hidden" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
</li>
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
sideOffset={10}
collisionPadding={{ top: 5, bottom: 5, left: 20, right: 20 }}
class="box-border h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-(--duration) ease-(--easing) before:absolute before:content-[''] data-instant:transition-none data-[side=bottom]:before:inset-x-0 data-[side=bottom]:before:-top-2.5 data-[side=bottom]:before:h-2.5 data-[side=left]:before:inset-y-0 data-[side=left]:before:-right-2.5 data-[side=left]:before:w-2.5 data-[side=right]:before:inset-y-0 data-[side=right]:before:-left-2.5 data-[side=right]:before:w-2.5 data-[side=top]:before:inset-x-0 data-[side=top]:before:-bottom-2.5 data-[side=top]:before:h-2.5"
style="--duration:0.35s;--easing:cubic-bezier(0.22,1,0.36,1);"
>
<NavigationMenu.Popup
class="relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-lg bg-gray-50 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[opacity,transform,width,height,scale,translate] duration-(--duration) ease-(--easing) data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:duration-150 data-ending-style:ease-[ease] data-starting-style:scale-95 data-starting-style:opacity-0"
>
<NavigationMenu.Arrow
class="flex transition-[left] duration-(--duration) ease-(--easing) data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
{@render arrow()}
</NavigationMenu.Arrow>
<NavigationMenu.Viewport class="relative size-full overflow-hidden" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
{#snippet chevronDownIcon()}
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M5.75 9.5L12 15.75L18.25 9.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{/snippet}
{#snippet chevronRightIcon()}
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M9.5 18.25L15.75 12L9.5 5.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{/snippet}
{#snippet card(item: LinkItem)}
<li>
<NavigationMenu.Link
href={item.href}
class="relative block w-full rounded-md p-2 text-left text-inherit no-underline hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 sm:p-3"
>
<h3 class="m-0 mb-1 text-sm/5 font-normal">{item.title}</h3>
<p class="m-0 text-sm/5 text-gray-500">{item.description}</p>
</NavigationMenu.Link>
</li>
{/snippet}
{#snippet arrow()}
<svg width="20" height="10" viewBox="0 0 20 10" fill="none">
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
class="fill-gray-50"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
class="fill-gray-200"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
/>
</svg>
{/snippet}Nested inline submenus
For second-level navigation that stays inside the same panel, give the nested root only a List and a Viewport — no Portal or Positioner — and seed the open submenu with value.
<script lang="ts">
import { NavigationMenu } from '@shardsui/svelte/navigation-menu'
import { MediaQuery } from 'svelte/reactivity'
const desktop = new MediaQuery('(min-width: 700px)')
const collections = [
{
value: 'getting-started',
label: 'Getting started',
hint: 'Start from zero.',
title: 'Build a foundation',
description: 'Short guides for your first steps.',
links: [
{
href: '#',
title: 'Installation',
description: 'Set up the library in your project.'
},
{
href: '#',
title: 'Quick start',
description: 'Compose your first component.'
}
]
},
{
value: 'components',
label: 'Components',
hint: 'Explore the parts.',
title: 'Browse the catalog',
description: 'Every component, documented and ready to use.',
links: [
{
href: '#',
title: 'Dialog',
description: 'Modal overlays with focus management.'
},
{
href: '#',
title: 'Popover',
description: 'Floating panels anchored to a trigger.'
}
]
},
{
value: 'advanced',
label: 'Advanced',
hint: 'Go deeper.',
title: 'Customize behavior',
description: 'Handles, detached triggers, and composition patterns.',
links: [
{
href: '#',
title: 'Detached triggers',
description: 'Share one popup across multiple triggers.'
},
{
href: '#',
title: 'State factories',
description: 'How internal state is structured.'
}
]
}
]
</script>
<NavigationMenu.Root class="min-w-max rounded-lg bg-gray-50 p-1 text-gray-900">
<NavigationMenu.List class="relative flex gap-px">
<NavigationMenu.Item>
<NavigationMenu.Trigger
class="m-0 box-border flex h-8 items-center justify-center gap-1.5 rounded-md bg-gray-50 px-2 text-sm/6 font-normal text-gray-900 no-underline select-none hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 data-popup-open:bg-gray-100 sm:px-3 sm:text-sm"
>
Browse
<NavigationMenu.Icon
class="transition-transform duration-200 ease-[ease] data-popup-open:rotate-180"
>
{@render chevronDownIcon()}
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Content
class="h-full w-[calc(100vw-40px)] p-0 transition-[opacity,translate] duration-[calc(var(--duration)*0.5),var(--duration)] ease-[ease,cubic-bezier(0.4,0,0.2,1)] data-ending-style:opacity-0 data-ending-style:duration-[calc(var(--duration)*0.5)] data-ending-style:ease-[ease] data-ending-style:data-[activation-direction=left]:translate-x-8 data-starting-style:data-[activation-direction=left]:-translate-x-8 data-starting-style:data-[activation-direction=left]:opacity-0 data-ending-style:data-[activation-direction=right]:-translate-x-8 data-starting-style:data-[activation-direction=right]:translate-x-8 data-starting-style:data-[activation-direction=right]:opacity-0 min-[700px]:max-w-168.75"
>
<NavigationMenu.Root
class="overflow-hidden text-gray-900"
orientation={desktop.current ? 'vertical' : 'horizontal'}
value="getting-started"
>
<div
class="grid grid-cols-1 overflow-clip rounded-lg min-[700px]:grid-cols-[13rem_minmax(0,1fr)]"
>
<NavigationMenu.List
class="m-0 flex list-none flex-row gap-1 overflow-x-auto bg-gray-100 p-4 min-[700px]:box-border min-[700px]:h-(--popup-height) min-[700px]:flex-col min-[700px]:gap-px min-[700px]:overflow-x-visible min-[700px]:overflow-y-auto min-[700px]:border-r min-[700px]:border-r-gray-200 min-[700px]:transition-[height] min-[700px]:duration-(--duration) min-[700px]:ease-(--easing)"
>
{#each collections as menu (menu.value)}
<NavigationMenu.Item value={menu.value}>
<NavigationMenu.Trigger
class="m-0 box-border flex w-full min-w-40 flex-col items-start gap-0.5 rounded-lg bg-transparent px-3 py-2.5 text-left text-inherit hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 data-popup-open:bg-gray-50 data-popup-open:shadow-sm"
>
<span class="text-sm leading-tight font-normal text-gray-900">{menu.label}</span
>
<span class="text-sm leading-snug text-gray-500">{menu.hint}</span>
</NavigationMenu.Trigger>
<NavigationMenu.Content
class="min-[700px]:blur-0 h-full translate-x-0 p-7 transition-[opacity,translate,filter] duration-(--duration) ease-(--easing) data-ending-style:opacity-0 data-ending-style:data-[activation-direction=left]:translate-x-1/2 data-starting-style:data-[activation-direction=left]:-translate-x-1/2 data-starting-style:data-[activation-direction=left]:opacity-0 data-ending-style:data-[activation-direction=right]:-translate-x-1/2 data-starting-style:data-[activation-direction=right]:translate-x-1/2 data-starting-style:data-[activation-direction=right]:opacity-0 min-[700px]:p-8 min-[700px]:duration-[calc(var(--duration)*1.35)] min-[700px]:ease-out-expo min-[700px]:data-ending-style:blur-[2px] min-[700px]:data-starting-style:blur-[2px] min-[700px]:data-ending-style:data-[activation-direction=down]:-translate-y-18 min-[700px]:data-starting-style:data-[activation-direction=down]:translate-y-18 min-[700px]:data-starting-style:data-[activation-direction=down]:opacity-0 min-[700px]:data-ending-style:data-[activation-direction=up]:translate-y-18 min-[700px]:data-starting-style:data-[activation-direction=up]:-translate-y-18 min-[700px]:data-starting-style:data-[activation-direction=up]:opacity-0"
>
<h4 class="m-0 text-base leading-tight font-normal">{menu.title}</h4>
<p class="m-0 mt-2.5 text-sm text-gray-500">{menu.description}</p>
<ul class="m-0 -mx-2 mt-4 grid list-none gap-0 p-0">
{#each menu.links as link (link.title)}
<li>
<NavigationMenu.Link
class="box-border block rounded-lg bg-transparent px-2 py-3 text-inherit no-underline hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950"
href={link.href}
>
<h5 class="m-0 text-sm/4.5 font-normal">{link.title}</h5>
<p class="m-0 mt-1.5 text-sm leading-normal text-gray-500">
{link.description}
</p>
</NavigationMenu.Link>
</li>
{/each}
</ul>
</NavigationMenu.Content>
</NavigationMenu.Item>
{/each}
</NavigationMenu.List>
<NavigationMenu.Viewport
class="relative min-h-66 overflow-hidden border-t border-gray-200 min-[700px]:border-t-0"
/>
</div>
</NavigationMenu.Root>
</NavigationMenu.Content>
</NavigationMenu.Item>
<NavigationMenu.Item>
<NavigationMenu.Link
class="m-0 box-border flex h-8 items-center justify-center gap-1.5 rounded-md bg-gray-50 px-2 text-sm/6 font-normal text-gray-900 no-underline select-none hover:bg-gray-100 focus-visible:relative focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 active:bg-gray-100 sm:px-3 sm:text-sm"
href="#">Pricing</NavigationMenu.Link
>
</NavigationMenu.Item>
</NavigationMenu.List>
<NavigationMenu.Portal>
<NavigationMenu.Positioner
sideOffset={10}
collisionPadding={{ top: 5, bottom: 5, left: 20, right: 20 }}
collisionAvoidance={{ side: 'none' }}
class="box-border h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-(--duration) ease-(--easing) before:absolute before:content-[''] data-instant:transition-none data-[side=bottom]:before:inset-x-0 data-[side=bottom]:before:-top-2.5 data-[side=bottom]:before:h-2.5 data-[side=left]:before:inset-y-0 data-[side=left]:before:-right-2.5 data-[side=left]:before:w-2.5 data-[side=right]:before:inset-y-0 data-[side=right]:before:-left-2.5 data-[side=right]:before:w-2.5 data-[side=top]:before:inset-x-0 data-[side=top]:before:-bottom-2.5 data-[side=top]:before:h-2.5"
style="--duration:0.35s;--easing:cubic-bezier(0.22,1,0.36,1);"
>
<NavigationMenu.Popup
class="relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-lg bg-gray-50 text-gray-900 shadow-lg outline-1 outline-gray-200 transition-[opacity,scale,width,height] duration-(--duration) ease-(--easing) data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:transition-[opacity,scale] data-ending-style:duration-150 data-ending-style:ease-[ease] data-starting-style:scale-95 data-starting-style:opacity-0"
>
<NavigationMenu.Arrow
class="flex transition-[left] duration-(--duration) ease-(--easing) data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=left]:rotate-90 data-[side=right]:-left-3.25 data-[side=right]:-rotate-90 data-[side=top]:-bottom-2 data-[side=top]:rotate-180"
>
{@render arrow()}
</NavigationMenu.Arrow>
<NavigationMenu.Viewport class="relative size-full overflow-hidden" />
</NavigationMenu.Popup>
</NavigationMenu.Positioner>
</NavigationMenu.Portal>
</NavigationMenu.Root>
{#snippet chevronDownIcon()}
<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M5.75 9.5L12 15.75L18.25 9.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
{/snippet}
{#snippet arrow()}
<svg width="20" height="10" viewBox="0 0 20 10" fill="none">
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
class="fill-gray-50"
/>
<path
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
class="fill-gray-200"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
/>
</svg>
{/snippet}Custom links
<NavigationMenu.Link> renders an <a>, so SvelteKit's router intercepts its href for client-side navigation, with no wrapper component needed.
<NavigationMenu.Link href="/docs">Docs</NavigationMenu.Link>SvelteKit link attributes, such as preloading, pass straight through:
<NavigationMenu.Link href="/docs" data-sveltekit-preload-data="hover">Docs</NavigationMenu.Link>The menu stays open after a link is clicked; add closeOnClick to dismiss it on navigation.
Large menus
When a panel is taller than the space below its trigger, cap it against --available-height so it shrinks instead of overflowing:
.content,
.popup {
max-height: var(--available-height);
}That only works if the content can compress. When it can't, let it scroll:
.content,
.popup {
max-height: var(--available-height);
}
.content {
overflow-y: auto;
}Native scrollbars stay visible while the panel transitions, so Scroll Area is the better fit here. It keeps the scrollbars hidden and lets the Arrow stay centered.
API reference
Root
Groups all parts of the navigation menu.
Renders a <nav> element at the root, or a <div> element when nested.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'nav' | |
class | string | — | |
style | string | — | |
value | unknown | null | |
onValueChange | (value: unknown) => void | — | |
delay | number | 50 | |
closeDelay | number | 50 | |
orientation | 'horizontal' | 'vertical' | 'horizontal' | |
onOpenChangeComplete | (open: boolean) => void | — | |
children | Snippet<[{ open, nested }]> | — | |
| Attribute | Description |
|---|---|
data-nested | Present when this navigation menu is nested in another. |
data-open | Present when a menu panel is open. |
List
Contains a list of navigation menu items.
Renders a <ul> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'ul' | |
class | string | — | |
style | string | — | |
children | Snippet<[{ open }]> | — | |
| Attribute | Description |
|---|---|
data-open | Present when a menu panel is open. |
Item
An individual navigation menu item.
Renders a <li> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'li' | |
class | string | — | |
style | string | — | |
value | unknown | auto-generated | |
children | Snippet | — | |
Trigger
Opens its item's content on hover or click.
Renders a <button> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'button' | |
class | string | — | |
style | string | — | |
disabled | boolean | false | |
children | Snippet<[{ open }]> | — | |
| Attribute | Description |
|---|---|
data-popup-open | Present while this item's content is showing. |
data-pressed | Present alongside data-popup-open, however the content was opened. |
Icon
An icon that indicates that the trigger button opens a menu.
Renders a <span> element, hidden from assistive technology, falling back to a ▼ glyph when given no children.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'span' | |
class | string | — | |
style | string | — | |
children | Snippet<[{ open }]> | — | |
| Attribute | Description |
|---|---|
data-popup-open | Present while this item's content is showing. |
Content
The panel for an item, rendered inside <NavigationMenu.Viewport> while that item is active.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'div' | |
class | string | — | |
style | string | — | |
keepMounted | boolean | false | |
children | Snippet<[{ open, transitionStatus, activationDirection }]> | — | |
| Attribute | Description |
|---|---|
data-open | Present when this item's content is showing. |
data-closed | Present when it is not. |
data-activation-direction | Direction the newly activated trigger sits in: left, right, up or down. |
data-starting-style | Present when the content is animating in. |
data-ending-style | Present when the content is animating out. |
Link
A link to another page or section.
Renders an <a> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'a' | |
class | string | — | |
style | string | — | |
href | string | — | |
active | boolean | false | |
closeOnClick | boolean | false | |
children | Snippet<[{ active }]> | — | |
| Attribute | Description |
|---|---|
data-active | Present when the link is the currently active page. |
Backdrop
An overlay displayed beneath the popup.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'div' | |
class | string | — | |
style | string | — | |
children | Snippet<[{ open, transitionStatus }]> | — | |
| Attribute | Description |
|---|---|
data-open | Present when the popup is open. |
data-closed | Present when the popup is closed. |
data-starting-style | Present when the backdrop is animating in. |
data-ending-style | Present when the backdrop is animating out. |
Portal
A portal that moves the popup out to <body>, clear of ancestor clipping and stacking.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
container | HTMLElement | null | — | |
keepMounted | boolean | false | |
children | Snippet | — | |
Positioner
Positions the navigation menu against the currently active trigger.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'div' | |
class | string | — | |
style | string | — | |
side | 'top' | 'bottom' | 'left' | 'right' | 'inline-start' | 'inline-end' | 'bottom' | |
align | 'start' | 'center' | 'end' | 'center' | |
sideOffset | number | OffsetFunction | 0 | |
alignOffset | number | OffsetFunction | 0 | |
collisionBoundary | 'clipping-ancestors' | Element | Element[] | Rect | 'clipping-ancestors' | |
collisionPadding | number | Padding | 5 | |
collisionAvoidance | CollisionAvoidance | { fallbackAxisSide: 'end' } when nested, { fallbackAxisSide: 'none' } otherwise | |
sticky | boolean | false | |
arrowPadding | number | 5 | |
disableAnchorTracking | boolean | false | |
anchor | Element | VirtualAnchorElement | null | active trigger | |
positionMethod | 'absolute' | 'fixed' | 'absolute' | |
children | Snippet<[{ open, side, align, anchorHidden, instant }]> | — | |
| Attribute | Description |
|---|---|
data-open | Present when the popup is open. |
data-closed | Present when the popup is closed. |
data-side | Which side of the anchor the popup is on. |
data-align | How the popup is aligned relative to the side. |
data-anchor-hidden | Present when the anchor is hidden. |
data-instant | Present when animations should be instant. |
| CSS Variable | Description |
|---|---|
--available-width | Available width between the anchor and the viewport edge. |
--available-height | Available height between the anchor and the viewport edge. |
--anchor-width | Width of the anchor element. |
--anchor-height | Height of the anchor element. |
--transform-origin | Transform origin for scale animations. |
--positioner-width | The current panel's width in pixels. Set for as long as the navigation menu is open. |
--positioner-height | The current panel's height in pixels. Set for as long as the navigation menu is open. |
Popup
A container for the navigation menu contents.
Renders a <nav> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'nav' | |
class | string | — | |
style | string | — | |
id | string | auto | |
children | Snippet<[{ open, transitionStatus, side, align, anchorHidden }]> | — | |
| Attribute | Description |
|---|---|
data-open | Present when the popup is open. |
data-closed | Present when the popup is closed. |
data-anchor-hidden | Present when the anchor is hidden. |
data-align | How the popup is aligned relative to the side. |
data-side | Which side of the anchor the popup is on. |
data-starting-style | Present when the popup is animating in. |
data-ending-style | Present when the popup is animating out. |
| CSS Variable | Description |
|---|---|
--popup-width | The popup element's width: a pixel value while <NavigationMenu.Viewport> morphs between panels and again while the popup closes, auto once it settles. |
--popup-height | The popup element's height: a pixel value while <NavigationMenu.Viewport> morphs between panels and again while the popup closes, auto once it settles. |
Viewport
The clipping viewport of the navigation menu's current content.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'div' | |
class | string | — | |
style | string | — | |
id | string | auto | |
children | Snippet | — | |
The active <NavigationMenu.Content> is rendered here, along with the previous one while it
animates out. Both carry the data attributes listed under Content.
Arrow
Displays an element positioned against the anchor.
Renders a <div> element.
| Prop | Type | Default | |
|---|---|---|---|
as | keyof HTMLElementTagNameMap | 'div' | |
class | string | — | |
style | string | — | |
children | Snippet<[{ open, side, align, uncentered }]> | — | |
| Attribute | Description |
|---|---|
data-open | Present when the popup is open. |
data-closed | Present when the popup is closed. |
data-side | Which side of the anchor the popup is on. |
data-align | How the popup is aligned relative to the side. |
data-uncentered | Present when the arrow cannot be centered. |