Navigation Menu
navigation-menuuiTop-of-page horizontal navigation with hover/click triggered megamenus. Use for marketing sites and product navs that need rich content — featured links, mini-cards, and submenu columns.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/navigation-menu.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/navigation-menu.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/navigation-menu.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/navigation-menu.jsonManifest:
https://uipkge.dev/r/svelte/navigation-menu.jsonInstalls to:src/lib/components/ui/navigation-menu/Examples
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
valueControlled open item value. When omitted the menu is uncontrolled. | string | — | optional |
defaultValue | string | — | optional |
viewportRender the shared viewport panel. Disable for per-item flyouts. | boolean | true | optional |
delayDurationHover delay (ms) before opening when nothing is open. | number | 200 | optional |
skipDelayDurationGrace window (ms) after closing during which hover opens instantly. | number | 300 | optional |
ref | HTMLDivElement | null | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
RegisteredContentinterface RegisteredContent {
snippet: Snippet | undefined
contentId: string
triggerId: string
className?: ClassValue | null
}NavigationMenuRootContextValueinterface NavigationMenuRootContextValue {
getValue: () => string | undefined
setValue: (value: string | undefined, refocusTrigger?: boolean) => void
isViewportEnabled: () => boolean
getRootEl: () => HTMLElement | null
getContentEl: () => HTMLElement | null
setContentEl: (el: HTMLElement | null) => void
registerTrigger: (value: string, getEl: () => HTMLElement | null) => () => void
/** Focus the sibling trigger in `dir`; returns the newly focused value. */
focusSiblingTrigger: (value: string, dir: 1 | -1 | 'first' | 'last') => string | null
registerContent: (value: string, content: RegisteredContent) => () => void
getContent: (value: string) => RegisteredContent | undefined
/** Enter direction for the active panel (viewport motion classes). */
getMotion: (value: string) => 'from-start' | 'from-end' | undefined
openWithDelay: (value: string) => void
cancelDelayedOpen: () => void
}NavigationMenuItemContextValueinterface NavigationMenuItemContextValue {
value: string
triggerId: string
contentId: string
isOpen: () => boolean
getTriggerEl: () => HTMLElement | null
setTriggerEl: (el: HTMLElement | null) => void
}npm dependencies
Used by
Files installed (12)
src/lib/components/ui/navigation-menu/NavigationMenu.svelte5.4 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' import type { RegisteredContent } from './NavigationMenuContext' export interface NavigationMenuProps extends HTMLAttributes<HTMLDivElement> { /** Controlled open item value. When omitted the menu is uncontrolled. */ value?: string defaultValue?: string /** Render the shared viewport panel. Disable for per-item flyouts. */ viewport?: boolean /** Hover delay (ms) before opening when nothing is open. */ delayDuration?: number /** Grace window (ms) after closing during which hover opens instantly. */ skipDelayDuration?: number ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { setNavigationMenuRootContext } from './NavigationMenuContext' import NavigationMenuViewport from './NavigationMenuViewport.svelte' let { class: className, value = $bindable(), defaultValue, viewport = true, delayDuration = 200, skipDelayDuration = 300, children, ref = $bindable(null), onmouseleave, ...restProps }: NavigationMenuProps = $props() let seeded = false $effect.pre(() => { if (!seeded && value === undefined && defaultValue !== undefined) value = defaultValue seeded = true }) let rootEl: HTMLDivElement | null = $state(null) let contentEl: HTMLElement | null = $state(null) $effect(() => { ref = rootEl }) const triggers: Array<{ value: string; getEl: () => HTMLElement | null }> = [] let contents = $state<Record<string, RegisteredContent | undefined>>({}) let prevValue: string | undefined = undefined let lastCloseAt = 0 let openTimer: ReturnType<typeof setTimeout> | null = null function setValue(next: string | undefined, refocusTrigger = false) { if (openTimer) { clearTimeout(openTimer) openTimer = null } if (value !== next) { prevValue = value value = next if (next === undefined) lastCloseAt = Date.now() } if (refocusTrigger && prevValue !== undefined) { const target = prevValue requestAnimationFrame(() => triggers.find((t) => t.value === target)?.getEl()?.focus()) } } function orderOf(v: string | undefined): number { if (v === undefined) return -1 const i = triggers.findIndex((t) => t.value === v) return i === -1 ? Number.MAX_SAFE_INTEGER : i } setNavigationMenuRootContext({ getValue: () => value, setValue, isViewportEnabled: () => viewport, getRootEl: () => rootEl, getContentEl: () => contentEl, setContentEl: (el) => { contentEl = el }, registerTrigger(triggerValue, getEl) { const entry = { value: triggerValue, getEl } triggers.push(entry) return () => { const i = triggers.indexOf(entry) if (i >= 0) triggers.splice(i, 1) } }, focusSiblingTrigger(triggerValue, dir) { const live = triggers.filter((t) => t.getEl()?.isConnected) if (live.length === 0) return null let next: number if (dir === 'first') next = 0 else if (dir === 'last') next = live.length - 1 else { const index = Math.max(0, live.findIndex((t) => t.value === triggerValue)) next = (index + dir + live.length) % live.length } const target = live[next]! target.getEl()?.focus() return target.value }, registerContent(contentValue, content) { contents[contentValue] = content return () => { if (contents[contentValue] === content) delete contents[contentValue] } }, getContent: (v) => contents[v], getMotion: (v) => { if (prevValue === undefined || v === prevValue) return undefined return orderOf(v) > orderOf(prevValue) ? 'from-start' : 'from-end' }, openWithDelay: (triggerValue) => { if (value === triggerValue) return if (openTimer) clearTimeout(openTimer) // Instant when something is already open or inside the skip window. if (value !== undefined || Date.now() - lastCloseAt < skipDelayDuration) { prevValue = value value = triggerValue return } openTimer = setTimeout(() => { openTimer = null prevValue = value value = triggerValue }, delayDuration) }, cancelDelayedOpen: () => { if (openTimer) { clearTimeout(openTimer) openTimer = null } }, }) $effect(() => { return () => { if (openTimer) clearTimeout(openTimer) } }) // Outside pointer down closes the open panel. $effect(() => { if (value === undefined) return function onPointerDown(e: PointerEvent) { if (rootEl && !rootEl.contains(e.target as Node)) { prevValue = value value = undefined lastCloseAt = Date.now() } } const t = setTimeout(() => document.addEventListener('pointerdown', onPointerDown), 0) return () => { clearTimeout(t) document.removeEventListener('pointerdown', onPointerDown) } }) </script> <div bind:this={rootEl} data-uipkge="" data-slot="navigation-menu" data-viewport={viewport} data-orientation="horizontal" class={cn('group/navigation-menu relative flex max-w-max flex-1 items-center justify-center', className)} onmouseleave={(e) => { setValue(undefined) onmouseleave?.(e) }} {...restProps} > {@render children?.()} {#if viewport} <NavigationMenuViewport /> {/if} </div>src/lib/components/ui/navigation-menu/NavigationMenuContent.svelte3.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface NavigationMenuContentProps extends HTMLAttributes<HTMLDivElement> { /** * Force inline-flyout rendering even when the root viewport is enabled. * Defaults to following the root `viewport` prop. */ forceMount?: boolean ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { getNavigationMenuItemContext, getNavigationMenuRootContext } from './NavigationMenuContext' import { navigationMenuContentVariants } from './navigation-menu-content.variants' let { class: className, forceMount = false, children, ref = $bindable(null), onkeydown, ...restProps }: NavigationMenuContentProps = $props() const root = getNavigationMenuRootContext() const item = getNavigationMenuItemContext() // Viewport mode: register the children snippet with the root — the shared // NavigationMenuViewport renders the active item's snippet (a snippet // hand-off, since Svelte has no portal). Otherwise render inline. const useViewport = $derived((root?.isViewportEnabled() ?? true) && !forceMount) $effect(() => { if (useViewport && item && root) { const v = item.value // Read className inside the effect so consumer class changes re-register. const payload = { snippet: children, contentId: item.contentId, triggerId: item.triggerId, className } return root.registerContent(v, payload) } }) let el: HTMLDivElement | null = $state(null) $effect(() => { ref = el // Inline mode owns the content element for ArrowDown focus jumps. if (!useViewport && item && root && root.getValue() === item.value) root.setContentEl(el) }) const isOpen = $derived(item?.isOpen() ?? false) const motion = $derived(item && root ? root.getMotion(item.value) : undefined) const panelClass = $derived( cn( navigationMenuContentVariants(), 'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:motion-safe:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:motion-safe:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:motion-safe:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:motion-safe:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:motion-safe:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:motion-safe:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none', className, ), ) </script> {#if !useViewport && isOpen && item} <div bind:this={el} id={item.contentId} aria-labelledby={item.triggerId} data-uipkge="" data-slot="navigation-menu-content" data-state="open" data-motion={motion} class={panelClass} onkeydown={(e) => { if (e.key === 'Escape') { e.preventDefault() root?.setValue(undefined, true) } onkeydown?.(e) }} {...restProps} > {@render children?.()} </div> {/if}src/lib/components/ui/navigation-menu/NavigationMenuContext.ts2.2 kB
import type { Snippet } from 'svelte' import type { ClassValue } from 'clsx' import { getContext, setContext } from 'svelte' /** Panel payload one item hands to the shared viewport for rendering. */ export interface RegisteredContent { snippet: Snippet | undefined contentId: string triggerId: string className?: ClassValue | null } /** Root open-value state, trigger registry, and shared-viewport content map. */ export interface NavigationMenuRootContextValue { getValue: () => string | undefined setValue: (value: string | undefined, refocusTrigger?: boolean) => void isViewportEnabled: () => boolean getRootEl: () => HTMLElement | null getContentEl: () => HTMLElement | null setContentEl: (el: HTMLElement | null) => void registerTrigger: (value: string, getEl: () => HTMLElement | null) => () => void /** Focus the sibling trigger in `dir`; returns the newly focused value. */ focusSiblingTrigger: (value: string, dir: 1 | -1 | 'first' | 'last') => string | null registerContent: (value: string, content: RegisteredContent) => () => void getContent: (value: string) => RegisteredContent | undefined /** Enter direction for the active panel (viewport motion classes). */ getMotion: (value: string) => 'from-start' | 'from-end' | undefined openWithDelay: (value: string) => void cancelDelayedOpen: () => void } /** One item's identity + trigger/content pairing. */ export interface NavigationMenuItemContextValue { value: string triggerId: string contentId: string isOpen: () => boolean getTriggerEl: () => HTMLElement | null setTriggerEl: (el: HTMLElement | null) => void } const RootKey = Symbol('NavigationMenuRoot') const ItemKey = Symbol('NavigationMenuItem') export function setNavigationMenuRootContext(v: NavigationMenuRootContextValue) { setContext(RootKey, v) } export function getNavigationMenuRootContext(): NavigationMenuRootContextValue | null { return getContext<NavigationMenuRootContextValue | null>(RootKey) ?? null } export function setNavigationMenuItemContext(v: NavigationMenuItemContextValue) { setContext(ItemKey, v) } export function getNavigationMenuItemContext(): NavigationMenuItemContextValue | null { return getContext<NavigationMenuItemContextValue | null>(ItemKey) ?? null }src/lib/components/ui/navigation-menu/navigation-menu-content.variants.ts0.8 kB
import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than the package * `index.ts`) so consuming components can import without creating a circular * dependency through the index. See card.variants.ts for the canonical * example + the SSR symptom that motivated the split. */ export const navigationMenuContentVariants = cva( 'data-[motion^=from-]:motion-safe:animate-in data-[motion^=to-]:motion-safe:animate-out data-[motion^=from-]:motion-safe:fade-in data-[motion^=to-]:motion-safe:fade-out data-[motion=from-end]:motion-safe:slide-in-from-right-52 data-[motion=from-start]:motion-safe:slide-in-from-left-52 data-[motion=to-end]:motion-safe:slide-out-to-right-52 data-[motion=to-start]:motion-safe:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto', )src/lib/components/ui/navigation-menu/NavigationMenuIndicator.svelte1.8 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface NavigationMenuIndicatorProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { tick } from 'svelte' import { cn } from '$lib/utils' import { getNavigationMenuRootContext } from './NavigationMenuContext' let { class: className, ref = $bindable(null), ...restProps }: NavigationMenuIndicatorProps = $props() const root = getNavigationMenuRootContext() const value = $derived(root?.getValue()) const visible = $derived(value !== undefined) // Measured position of the open trigger's center relative to the root // (reka positions this automatically; the hand-rolled twin measures). let left = $state(0) $effect(() => { value if (!visible) return tick().then(() => { const rootEl = root?.getRootEl() const trigger = rootEl?.querySelector<HTMLElement>('[data-slot="navigation-menu-trigger"][data-state="open"]') if (rootEl && trigger) { const rootRect = rootEl.getBoundingClientRect() const rect = trigger.getBoundingClientRect() left = rect.left - rootRect.left + rect.width / 2 } }) }) </script> {#if visible} <div bind:this={ref} data-uipkge="" data-slot="navigation-menu-indicator" data-state="visible" class={cn( 'data-[state=visible]:motion-safe:animate-in data-[state=hidden]:motion-safe:animate-out data-[state=hidden]:motion-safe:fade-out data-[state=visible]:motion-safe:fade-in absolute top-full z-[1] flex h-1.5 -translate-x-1/2 items-end justify-center overflow-hidden', className, )} style:left="{left}px" aria-hidden="true" {...restProps} > <div class="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md"></div> </div> {/if}src/lib/components/ui/navigation-menu/NavigationMenuItem.svelte1.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface NavigationMenuItemProps extends HTMLAttributes<HTMLLIElement> { /** * Stable value for this item. Defaults to a generated id. Pass one when * controlling the open panel through the root `value` / `defaultValue`. */ value?: string ref?: HTMLLIElement | null } let itemCounter = 0 </script> <script lang="ts"> import { untrack } from 'svelte' import { cn } from '$lib/utils' import { getNavigationMenuRootContext, setNavigationMenuItemContext } from './NavigationMenuContext' let { class: className, value: valueProp, children, ref = $bindable(null), ...restProps }: NavigationMenuItemProps = $props() const root = getNavigationMenuRootContext() // Computed once: ids must stay stable for the component's lifetime (aria // references + root tracking depend on it). const value = untrack(() => valueProp ?? `item-${++itemCounter}`) const triggerId = `navigation-menu-trigger-${value}` const contentId = `navigation-menu-content-${value}` const isOpen = $derived(root?.getValue() === value) let triggerEl: HTMLElement | null = $state(null) setNavigationMenuItemContext({ value, triggerId, contentId, isOpen: () => isOpen, getTriggerEl: () => triggerEl, setTriggerEl: (el) => { triggerEl = el }, }) </script> <li bind:this={ref} data-uipkge="" data-slot="navigation-menu-item" data-value={value} data-state={isOpen ? 'open' : 'closed'} class={cn('relative', className)} {...restProps} > {@render children?.()} </li>src/lib/components/ui/navigation-menu/NavigationMenuLink.svelte1.4 kB
<script lang="ts" module> import type { HTMLAnchorAttributes } from 'svelte/elements' export interface NavigationMenuLinkProps extends HTMLAnchorAttributes { active?: boolean ref?: HTMLAnchorElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { getNavigationMenuRootContext } from './NavigationMenuContext' let { class: className, active = false, children, ref = $bindable(null), onclick, ...restProps }: NavigationMenuLinkProps = $props() const root = getNavigationMenuRootContext() </script> <a bind:this={ref} data-uipkge="" data-slot="navigation-menu-link" data-active={active ? '' : undefined} aria-current={active ? 'page' : undefined} class={cn( "data-active:focus:bg-accent data-active:hover:bg-accent data-active:bg-accent/50 data-active:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4", className, )} onclick={(e) => { onclick?.(e) // Following a link dismisses the menu. root?.setValue(undefined) }} {...restProps} > {@render children?.()} </a>src/lib/components/ui/navigation-menu/NavigationMenuList.svelte0.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface NavigationMenuListProps extends HTMLAttributes<HTMLUListElement> { ref?: HTMLUListElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' let { class: className, children, ref = $bindable(null), ...restProps }: NavigationMenuListProps = $props() </script> <ul bind:this={ref} data-uipkge="" data-slot="navigation-menu-list" class={cn('group flex flex-1 list-none items-center justify-center gap-1', className)} {...restProps} > {@render children?.()} </ul>src/lib/components/ui/navigation-menu/NavigationMenuTrigger.svelte3.2 kB
<script lang="ts" module> import type { HTMLButtonAttributes } from 'svelte/elements' export interface NavigationMenuTriggerProps extends HTMLButtonAttributes { disabled?: boolean ref?: HTMLButtonElement | null } </script> <script lang="ts"> import { tick } from 'svelte' import { ChevronDown } from '@lucide/svelte' import { cn } from '$lib/utils' import { getNavigationMenuItemContext, getNavigationMenuRootContext } from './NavigationMenuContext' import { navigationMenuTriggerStyle } from './navigation-menu.variants' let { class: className, disabled, children, ref = $bindable(null), onclick, onkeydown, onmouseenter, onmouseleave, ...restProps }: NavigationMenuTriggerProps = $props() const root = getNavigationMenuRootContext() const item = getNavigationMenuItemContext() let el: HTMLButtonElement | null = $state(null) $effect(() => { ref = el item?.setTriggerEl(el) }) $effect(() => { if (item && root) { const v = item.value return root.registerTrigger(v, () => el) } }) const isOpen = $derived(item?.isOpen() ?? false) function focusFirstInContent() { tick().then(() => { root ?.getContentEl() ?.querySelector<HTMLElement>('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])') ?.focus() }) } </script> <button bind:this={el} type="button" id={item?.triggerId} aria-expanded={isOpen} aria-controls={item?.contentId} aria-disabled={disabled || undefined} data-uipkge="" data-slot="navigation-menu-trigger" data-state={isOpen ? 'open' : 'closed'} data-disabled={disabled ? '' : undefined} {disabled} class={cn(navigationMenuTriggerStyle(), 'group', className)} onclick={(e) => { if (disabled) return if (item && root) root.setValue(isOpen ? undefined : item.value) onclick?.(e) }} onmouseenter={(e) => { if (!disabled && item && root) root.openWithDelay(item.value) onmouseenter?.(e) }} onmouseleave={(e) => { root?.cancelDelayedOpen() onmouseleave?.(e) }} onkeydown={(e) => { if (disabled) { onkeydown?.(e) return } if (e.key === 'ArrowDown') { e.preventDefault() if (item && root) { if (!isOpen) root.setValue(item.value) focusFirstInContent() } } else if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') { e.preventDefault() if (item && root) { const next = root.focusSiblingTrigger(item.value, e.key === 'ArrowRight' ? 1 : -1) if (next && root.getValue() !== undefined) root.setValue(next) } } else if (e.key === 'Home') { e.preventDefault() if (item && root) root.focusSiblingTrigger(item.value, 'first') } else if (e.key === 'End') { e.preventDefault() if (item && root) root.focusSiblingTrigger(item.value, 'last') } else if (e.key === 'Escape') { if (isOpen && root) { e.preventDefault() root.setValue(undefined) } } onkeydown?.(e) }} {...restProps} > {@render children?.()} <ChevronDown class="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180" aria-hidden="true" /> </button>src/lib/components/ui/navigation-menu/NavigationMenuViewport.svelte3.6 kB
<script lang="ts" module> import type { HTMLAttributes } from 'svelte/elements' export interface NavigationMenuViewportProps extends HTMLAttributes<HTMLDivElement> { ref?: HTMLDivElement | null } </script> <script lang="ts"> import { cn } from '$lib/utils' import { getNavigationMenuRootContext } from './NavigationMenuContext' import { navigationMenuContentVariants } from './navigation-menu-content.variants' let { class: className, children, ref = $bindable(null), onkeydown, ...restProps }: NavigationMenuViewportProps = $props() const root = getNavigationMenuRootContext() let viewportEl: HTMLDivElement | null = $state(null) let panelEl: HTMLDivElement | null = $state(null) $effect(() => { ref = viewportEl root?.setContentEl(panelEl) }) const value = $derived(root?.getValue()) const active = $derived(value !== undefined ? root?.getContent(value) : undefined) const motion = $derived(value !== undefined && root ? root.getMotion(value) : undefined) const isOpen = $derived(value !== undefined && active?.snippet !== undefined) const LONG_CONTENT_CLASS = 'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:motion-safe:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:motion-safe:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:motion-safe:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:motion-safe:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:motion-safe:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:motion-safe:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none' </script> <div class="absolute top-full left-0 isolate z-50 flex justify-center"> <div bind:this={viewportEl} data-uipkge="" data-slot="navigation-menu-viewport" data-state={isOpen ? 'open' : 'closed'} class={cn( // NOTE: reka auto-measures `--reka-navigation-menu-viewport-*` geometry; // the hand-rolled twin sizes to content instead. 'origin-top-center bg-popover text-popover-foreground data-[state=open]:motion-safe:animate-in data-[state=closed]:motion-safe:animate-out data-[state=closed]:motion-safe:zoom-out-95 data-[state=open]:motion-safe:zoom-in-90 relative mt-1.5 h-auto w-full overflow-hidden rounded-md border shadow md:w-max', !isOpen && 'hidden', className, )} onkeydown={(e) => { if (e.key === 'Escape') { e.preventDefault() root?.setValue(undefined, true) } onkeydown?.(e) }} {...restProps} > {#if isOpen && active && value !== undefined} <div bind:this={panelEl} id={active.contentId} aria-labelledby={active.triggerId} data-uipkge="" data-slot="navigation-menu-content" data-state="open" data-motion={motion} class={cn(navigationMenuContentVariants(), LONG_CONTENT_CLASS, active.className)} > {@render active.snippet?.()} </div> {/if} {@render children?.()} </div> </div>src/lib/components/ui/navigation-menu/navigation-menu.variants.ts0.9 kB
import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than the package * `index.ts`) so consuming Vue SFCs can import without creating a circular * dependency through the index. See card.variants.ts for the canonical * example + the SSR symptom that motivated the split. */ export const navigationMenuTriggerStyle = cva( 'group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1', )src/lib/components/ui/navigation-menu/index.ts1.2 kB
export { default as NavigationMenu, type NavigationMenuProps } from './NavigationMenu.svelte' export { default as NavigationMenuContent, type NavigationMenuContentProps } from './NavigationMenuContent.svelte' export { default as NavigationMenuIndicator, type NavigationMenuIndicatorProps } from './NavigationMenuIndicator.svelte' export { default as NavigationMenuItem, type NavigationMenuItemProps } from './NavigationMenuItem.svelte' export { default as NavigationMenuLink, type NavigationMenuLinkProps } from './NavigationMenuLink.svelte' export { default as NavigationMenuList, type NavigationMenuListProps } from './NavigationMenuList.svelte' export { default as NavigationMenuTrigger, type NavigationMenuTriggerProps } from './NavigationMenuTrigger.svelte' export { default as NavigationMenuViewport, type NavigationMenuViewportProps } from './NavigationMenuViewport.svelte' // Re-export variant API from the sibling files (kept separate to avoid the // Component <-> index circular import that broke dev SSR for Card). export { navigationMenuTriggerStyle } from './navigation-menu.variants' export { navigationMenuContentVariants } from './navigation-menu-content.variants'
Raw manifest:https://uipkge.dev/r/svelte/navigation-menu.json