Menubar

A horizontal menubar component that contains multiple dropdown menus with keyboard navigation, submenus, checkbox items, and radio groups.

The API follows Base-UI naming conventions: Menubar.Root renders the top-level role="menubar" container, each Menubar.Menu is an individual dropdown menu, Menubar.Trigger opens it, and Menubar.Popup holds the content. Submenus use Menubar.SubmenuRoot, Menubar.SubmenuTrigger, and Menubar.SubmenuPopup.

View source
import { Menubar } from "@danielfrg/solid-ui/menubar"
import styles from "./index.module.css"

export function DemoMenubarHero() {
  return (
    <Menubar.Root class={styles.menubar}>
      <Menubar.Menu>
        <Menubar.Trigger class={styles.trigger}>File</Menubar.Trigger>
        <Menubar.Portal>
          <Menubar.Popup class={styles.content}>
            <Menubar.Item class={styles.item} onSelect={() => console.log("New")}>
              New
            </Menubar.Item>
            <Menubar.Item class={styles.item} onSelect={() => console.log("Open")}>
              Open
            </Menubar.Item>
            <Menubar.Item class={styles.item} onSelect={() => console.log("Save")}>
              Save
            </Menubar.Item>
            <Menubar.SubmenuRoot>
              <Menubar.SubmenuTrigger class={styles.item}>
                Export
                <ChevronRightIcon />
              </Menubar.SubmenuTrigger>
              <Menubar.Portal>
                <Menubar.SubmenuPopup class={styles.content}>
                  <Menubar.Item class={styles.item} onSelect={() => console.log("PDF")}>
                    PDF
                  </Menubar.Item>
                  <Menubar.Item class={styles.item} onSelect={() => console.log("PNG")}>
                    PNG
                  </Menubar.Item>
                  <Menubar.Item class={styles.item} onSelect={() => console.log("SVG")}>
                    SVG
                  </Menubar.Item>
                </Menubar.SubmenuPopup>
              </Menubar.Portal>
            </Menubar.SubmenuRoot>
            <Menubar.Separator class={styles.separator} />
            <Menubar.Item class={styles.item} onSelect={() => console.log("Print")}>
              Print
            </Menubar.Item>
          </Menubar.Popup>
        </Menubar.Portal>
      </Menubar.Menu>

      <Menubar.Menu>
        <Menubar.Trigger class={styles.trigger}>Edit</Menubar.Trigger>
        <Menubar.Portal>
          <Menubar.Popup class={styles.content}>
            <Menubar.Item class={styles.item} onSelect={() => console.log("Cut")}>
              Cut
            </Menubar.Item>
            <Menubar.Item class={styles.item} onSelect={() => console.log("Copy")}>
              Copy
            </Menubar.Item>
            <Menubar.Item class={styles.item} onSelect={() => console.log("Paste")}>
              Paste
            </Menubar.Item>
          </Menubar.Popup>
        </Menubar.Portal>
      </Menubar.Menu>

      <Menubar.Menu>
        <Menubar.Trigger class={styles.trigger}>View</Menubar.Trigger>
        <Menubar.Portal>
          <Menubar.Popup class={styles.content}>
            <Menubar.Item class={styles.item} onSelect={() => console.log("Zoom In")}>
              Zoom In
            </Menubar.Item>
            <Menubar.Item class={styles.item} onSelect={() => console.log("Zoom Out")}>
              Zoom Out
            </Menubar.Item>
            <Menubar.SubmenuRoot>
              <Menubar.SubmenuTrigger class={styles.item}>
                Layout
                <ChevronRightIcon />
              </Menubar.SubmenuTrigger>
              <Menubar.Portal>
                <Menubar.SubmenuPopup class={styles.content}>
                  <Menubar.Item class={styles.item} onSelect={() => console.log("Single Page")}>
                    Single Page
                  </Menubar.Item>
                  <Menubar.Item class={styles.item} onSelect={() => console.log("Two Pages")}>
                    Two Pages
                  </Menubar.Item>
                  <Menubar.Item class={styles.item} onSelect={() => console.log("Continuous")}>
                    Continuous
                  </Menubar.Item>
                </Menubar.SubmenuPopup>
              </Menubar.Portal>
            </Menubar.SubmenuRoot>
            <Menubar.Separator class={styles.separator} />
            <Menubar.Item class={styles.item} onSelect={() => console.log("Full Screen")}>
              Full Screen
            </Menubar.Item>
          </Menubar.Popup>
        </Menubar.Portal>
      </Menubar.Menu>
    </Menubar.Root>
  )
}

function ChevronRightIcon() {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" fill="none">
      <path
        d="M6 12L10 8L6 4"
        stroke="currentColor"
        stroke-width="1.5"
        stroke-linecap="round"
        stroke-linejoin="round"
      />
    </svg>
  )
}
.menubar {
  display: flex;
  background-color: var(--color-gray-50);
  border: 1px solid var(--color-gray-200);
  border-radius: 0.375rem;
  padding: 0.125rem;
}

.trigger {
  box-sizing: border-box;
  background: none;
  padding: 0 0.75rem;
  outline: 0;
  border: 0;
  color: var(--color-gray-600);
  border-radius: 0.25rem;
  user-select: none;
  height: 2rem;
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: default;
}

.trigger[data-highlighted] {
  background-color: var(--color-gray-100);
  outline: none;
}

.trigger:focus-visible {
  background-color: var(--color-gray-100);
  outline: none;
}

.trigger[data-disabled] {
  opacity: 0.5;
}

.content {
  box-sizing: border-box;
  padding-block: 0.25rem;
  border-radius: 0.375rem;
  background-color: var(--color-gray-50);
  color: var(--color-gray-900);
  z-index: 50;
  min-width: 10rem;
  animation: contentHide 150ms ease forwards;
}

.content[data-expanded] {
  animation: contentShow 150ms ease;
}

@media (prefers-color-scheme: light) {
  .content {
    outline: 1px solid var(--color-gray-200);
    box-shadow:
      0 10px 15px -3px var(--color-gray-200),
      0 4px 6px -4px var(--color-gray-200);
  }
}

@media (prefers-color-scheme: dark) {
  .content {
    outline: 1px solid var(--color-gray-300);
    outline-offset: -1px;
  }
}

.item {
  outline: 0;
  cursor: default;
  user-select: none;
  padding: 0.5rem 1rem;
  display: flex;
  font-size: 0.875rem;
  line-height: 1rem;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  position: relative;
}

.item[data-highlighted] {
  z-index: 0;
  color: var(--color-gray-50);
}

.item[data-highlighted]::before {
  content: "";
  z-index: -1;
  position: absolute;
  inset-block: 0;
  inset-inline: 0.25rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-900);
}

.item[data-disabled] {
  opacity: 0.5;
}

.separator {
  margin: 0.375rem 1rem;
  height: 1px;
  background-color: var(--color-gray-200);
}

@keyframes contentShow {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes contentHide {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
: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%);
  }
}