Navigation Menu
A navigation menu component commonly used for site-wide navigation. Menus open on hover with configurable delay, support animated content transitions between items, and position content using a shared viewport.
NavigationMenu.Root wraps everything in a <nav> with <ul role="menubar">. Each NavigationMenu.Menu groups a NavigationMenu.Trigger and NavigationMenu.Content. Content is portalled into a NavigationMenu.Viewport for smooth animated transitions between menus. The data-motion attribute on content (from-start, from-end, to-start, to-end) enables directional animation.
View source
import { NavigationMenu } from "@danielfrg/solid-ui/navigation-menu"
import styles from "./index.module.css"
export function DemoNavigationMenuHero() {
return (
<NavigationMenu.Root class={styles.root}>
<NavigationMenu.Menu>
<NavigationMenu.Trigger class={styles.trigger}>
Learn{" "}
<NavigationMenu.Icon aria-hidden="true" class={styles["trigger-indicator"]}>
<ChevronDownIcon />
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Portal>
<NavigationMenu.Content class={`${styles.content} ${styles["content-1"]}`}>
<NavigationMenu.Item class={styles["item-callout"]} href="https://github.com">
<NavigationMenu.ItemLabel class={styles["item-label"]}>GitHub</NavigationMenu.ItemLabel>
<NavigationMenu.ItemDescription class={styles["item-description"]}>
Where the world builds software.
</NavigationMenu.ItemDescription>
</NavigationMenu.Item>
<NavigationMenu.Item class={styles.item} href="#quick-start">
<NavigationMenu.ItemLabel class={styles["item-label"]}>Quick Start</NavigationMenu.ItemLabel>
<NavigationMenu.ItemDescription class={styles["item-description"]}>
Install and assemble your first component.
</NavigationMenu.ItemDescription>
</NavigationMenu.Item>
<NavigationMenu.Item class={styles.item} href="#accessibility">
<NavigationMenu.ItemLabel class={styles["item-label"]}>Accessibility</NavigationMenu.ItemLabel>
<NavigationMenu.ItemDescription class={styles["item-description"]}>
Learn how we build accessible components.
</NavigationMenu.ItemDescription>
</NavigationMenu.Item>
<NavigationMenu.Item class={styles.item} href="#styling">
<NavigationMenu.ItemLabel class={styles["item-label"]}>Styling</NavigationMenu.ItemLabel>
<NavigationMenu.ItemDescription class={styles["item-description"]}>
Unstyled and compatible with any styling solution.
</NavigationMenu.ItemDescription>
</NavigationMenu.Item>
</NavigationMenu.Content>
</NavigationMenu.Portal>
</NavigationMenu.Menu>
<NavigationMenu.Menu>
<NavigationMenu.Trigger class={styles.trigger}>
Overview{" "}
<NavigationMenu.Icon class={styles["trigger-indicator"]}>
<ChevronDownIcon />
</NavigationMenu.Icon>
</NavigationMenu.Trigger>
<NavigationMenu.Portal>
<NavigationMenu.Content class={`${styles.content} ${styles["content-2"]}`}>
<NavigationMenu.Item class={styles.item} href="#introduction">
<NavigationMenu.ItemLabel class={styles["item-label"]}>Introduction</NavigationMenu.ItemLabel>
<NavigationMenu.ItemDescription class={styles["item-description"]}>
Build high-quality, accessible design systems and web apps.
</NavigationMenu.ItemDescription>
</NavigationMenu.Item>
<NavigationMenu.Item class={styles.item} href="#getting-started">
<NavigationMenu.ItemLabel class={styles["item-label"]}>Getting Started</NavigationMenu.ItemLabel>
<NavigationMenu.ItemDescription class={styles["item-description"]}>
A quick tutorial to get you up and running.
</NavigationMenu.ItemDescription>
</NavigationMenu.Item>
<NavigationMenu.Item class={styles.item} href="#animation">
<NavigationMenu.ItemLabel class={styles["item-label"]}>Animation</NavigationMenu.ItemLabel>
<NavigationMenu.ItemDescription class={styles["item-description"]}>
Use CSS keyframes or any animation library of your choice.
</NavigationMenu.ItemDescription>
</NavigationMenu.Item>
<NavigationMenu.Item class={styles.item} href="#composition">
<NavigationMenu.ItemLabel class={styles["item-label"]}>Composition</NavigationMenu.ItemLabel>
<NavigationMenu.ItemDescription class={styles["item-description"]}>
Customize behavior or integrate existing libraries.
</NavigationMenu.ItemDescription>
</NavigationMenu.Item>
</NavigationMenu.Content>
</NavigationMenu.Portal>
</NavigationMenu.Menu>
<NavigationMenu.Trigger class={styles.trigger} as="a" href="https://github.com" target="_blank">
GitHub
</NavigationMenu.Trigger>
<NavigationMenu.Viewport class={styles.viewport} />
</NavigationMenu.Root>
)
}
function ChevronDownIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none">
<path
d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
/>
</svg>
)
}.root {
display: flex;
justify-content: center;
align-items: center;
padding: 4px;
background-color: var(--color-gray-50);
width: max-content;
border-radius: 6px;
list-style: none;
}
.trigger {
appearance: none;
display: inline-flex;
justify-content: center;
align-items: center;
width: auto;
outline: none;
padding: 12px 16px;
background-color: var(--color-gray-50);
color: var(--color-gray-900);
font-size: 0.9375rem;
gap: 6px;
line-height: 0;
transition: 250ms background-color;
border-radius: 4px;
border: none;
text-decoration: none;
font-weight: 500;
font-family: inherit;
cursor: pointer;
}
.trigger[data-highlighted="true"] {
background-color: var(--color-gray-100);
}
.trigger-indicator {
position: relative;
margin: -7.5px -4px;
height: 15px;
width: 15px;
transition: transform 250ms ease;
}
.trigger[data-expanded] .trigger-indicator {
transform: rotateX(180deg);
}
.viewport {
display: flex;
justify-content: center;
align-items: center;
width: var(--kb-navigation-menu-viewport-width);
height: var(--kb-navigation-menu-viewport-height);
z-index: 1000;
background-color: white;
border: 1px solid var(--color-gray-200);
box-shadow:
0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
opacity: 0;
overflow-x: clip;
overflow-y: visible;
transform-origin: var(--kb-menu-content-transform-origin);
transition:
width 250ms ease,
height 250ms ease;
animation: viewport-hide 250ms ease-in forwards;
pointer-events: none;
}
.viewport[data-expanded] {
border-radius: 6px;
animation: viewport-show 250ms ease-out;
opacity: 1;
pointer-events: auto;
}
.item-callout {
box-sizing: border-box;
display: flex;
justify-content: flex-end;
flex-direction: column;
width: 100%;
height: 100%;
background: linear-gradient(135deg, var(--color-gray-900) 0%, var(--color-gray-600) 100%);
border-radius: 6px;
padding: 25px;
text-decoration: none;
outline: none;
user-select: none;
}
.item-callout:focus {
outline: 2px solid var(--color-blue);
}
.item-callout .item-label {
margin-top: 16px;
font-size: 1.25rem;
color: white;
}
.item-callout .item-description {
color: var(--color-gray-200);
}
.item-label {
font-size: 1rem;
font-weight: 500;
color: var(--color-gray-900);
line-height: 1.2;
}
.item-description {
font-size: 0.875rem;
color: var(--color-gray-500);
line-height: 1.4;
}
.item {
display: block;
outline: none;
text-decoration: none;
user-select: none;
padding: 12px;
border-radius: 6px;
font-size: 15px;
line-height: 1;
}
.item:hover,
.item:focus {
background-color: var(--color-gray-100);
}
.content {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
outline: none;
display: grid;
padding: 22px;
column-gap: 10px;
grid-template-rows: repeat(3, 1fr);
grid-auto-flow: column;
animation-duration: 250ms;
animation-timing-function: ease;
animation-fill-mode: forwards;
pointer-events: none;
}
.content[data-expanded] {
pointer-events: auto;
}
.content[data-motion="from-start"] {
animation-name: enter-from-left;
}
.content[data-motion="from-end"] {
animation-name: enter-from-right;
}
.content[data-motion="to-start"] {
animation-name: exit-to-left;
}
.content[data-motion="to-end"] {
animation-name: exit-to-right;
}
.content-1 {
width: min(500px, 90dvw);
grid-template-columns: 0.75fr 1fr;
}
.content-1 > :first-child {
grid-row: span 3;
}
.content-2 {
width: min(600px, 90dvw);
grid-template-columns: 1fr 1fr;
}
@keyframes viewport-show {
from {
opacity: 0;
transform: rotateX(-20deg) scale(0.96);
}
to {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
}
@keyframes viewport-hide {
from {
opacity: 1;
transform: rotateX(0deg) scale(1);
}
to {
opacity: 0;
transform: rotateX(-10deg) scale(0.96);
}
}
@keyframes enter-from-right {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes enter-from-left {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes exit-to-right {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes exit-to-left {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}