Navigation

Nav menu

Horizontal site navigation whose items open rich panels of links with icons and descriptions, a mega menu.

Built on
<nav>Popover APIpopovertargetaria-expandedCSS anchor positioning
Client JavaScript
Yes, for state
Works without React
React only (needs JavaScript)
Source, gzipped
4.4 kB
Built from
IconPopover
Code
import { NavMenu, NavMenuItem, NavMenuLink, NavMenuSection } from "@/components/equip/nav-menu";

export default function Demo() {
  return (
    <NavMenu>
      <NavMenuItem label="Products">
        <NavMenuSection title="Build">
          <NavMenuLink href="#hosting" icon="cloud" title="Hosting" description="Deploy from Git in seconds." />
          <NavMenuLink href="#databases" icon="database" title="Databases" description="Postgres that scales to zero." />
        </NavMenuSection>
        <NavMenuSection title="Observe">
          <NavMenuLink href="#logs" icon="terminal" title="Logs" description="Search every request." />
          <NavMenuLink href="#alerts" icon="bell" title="Alerts" description="Know before your users do." />
        </NavMenuSection>
      </NavMenuItem>
      <NavMenuItem label="Pricing" href="#pricing" />
    </NavMenu>
  );
}

Examples

Product site

Hover a trigger and its panel opens after a short rest; click pins it, Escape closes it and returns focus to the trigger, and Down moves into it.

Code
import { NavMenu, NavMenuItem, NavMenuLink, NavMenuSection } from "@/components/equip/nav-menu";

export default function Example() {
  return (
    <NavMenu aria-label="Site">
      <NavMenuItem label="Platform">
        <NavMenuSection title="Ship">
          <NavMenuLink href="#previews" icon="git-branch" title="Preview deployments" description="A live link for every pull request." />
          <NavMenuLink href="#edge" icon="globe" title="Edge network" description="Served from 40 regions." />
          <NavMenuLink href="#rollbacks" icon="history" title="Instant rollback" description="Go back to any earlier build." />
        </NavMenuSection>
        <NavMenuSection title="Store">
          <NavMenuLink href="#postgres" icon="database" title="Postgres" description="Branches for every preview." />
          <NavMenuLink href="#storage" icon="hard-drive" title="Object storage" description="Files close to your functions." />
          <NavMenuLink href="#cache" icon="bolt" title="Key value cache" description="Reads in under a millisecond." />
        </NavMenuSection>
      </NavMenuItem>
      <NavMenuItem label="Resources">
        <NavMenuSection>
          <NavMenuLink href="#docs" icon="book-open" title="Documentation" />
          <NavMenuLink href="#guides" icon="compass" title="Guides" />
          <NavMenuLink href="#changelog" icon="newspaper" title="Changelog" />
        </NavMenuSection>
      </NavMenuItem>
      <NavMenuItem label="Customers" href="#customers" />
      <NavMenuItem label="Pricing" href="#pricing" current />
    </NavMenu>
  );
}

Marking where you are

current on the trigger shows the page is inside its panel, and current on the link marks the page itself with aria-current.

Code
import { NavMenu, NavMenuItem, NavMenuLink, NavMenuSection } from "@/components/equip/nav-menu";

export default function Example() {
  return (
    <NavMenu aria-label="Help centre">
      <NavMenuItem label="Account" current>
        <NavMenuSection title="Account">
          <NavMenuLink href="#profile" icon="user" title="Profile" description="Name, photo and contact details." />
          <NavMenuLink href="#security" icon="shield" title="Security" description="Passwords, passkeys and sessions." current />
          <NavMenuLink href="#notifications" icon="bell" title="Notifications" description="What we send and where." />
        </NavMenuSection>
      </NavMenuItem>
      <NavMenuItem label="Billing">
        <NavMenuSection title="Billing">
          <NavMenuLink href="#plans" icon="credit-card" title="Plans" description="Compare plans and change yours." />
          <NavMenuLink href="#invoices" icon="receipt" title="Invoices" description="Download past invoices." />
        </NavMenuSection>
      </NavMenuItem>
      <NavMenuItem label="Contact" href="#contact" />
    </NavMenu>
  );
}

Stacked rows

orientation="vertical" stacks the items as full-width rows for a drawer, "auto" does so only inside a container under 640px, and panelWidth keeps this panel to one column.

Code
import { NavMenu, NavMenuItem, NavMenuLink, NavMenuSection } from "@/components/equip/nav-menu";

export default function Example() {
  return (
    <div style={{ width: 240 }}>
      <NavMenu aria-label="Docs" orientation="vertical" panelWidth="18rem">
        <NavMenuItem label="Guides">
          <NavMenuSection title="Start">
            <NavMenuLink href="#install" icon="download" title="Install" description="Add the CLI and sign in." />
            <NavMenuLink href="#first-deploy" icon="upload" title="First deploy" description="From a repository to a live link." />
            <NavMenuLink href="#domains" icon="globe" title="Custom domains" description="Point your DNS and get a certificate." />
          </NavMenuSection>
        </NavMenuItem>
        <NavMenuItem label="API reference" href="#api" current />
        <NavMenuItem label="Changelog" href="#changelog" />
      </NavMenu>
    </div>
  );
}

Props

PropTypeDefaultWhat it does
aria-labelstring"Main"NavMenu: names the landmark.
openDelaynumber120NavMenu: milliseconds the pointer rests on a trigger before its panel opens.
closeDelaynumber280NavMenu: milliseconds after the pointer leaves before the panel closes.
orientation"horizontal" | "vertical" | "auto""horizontal"NavMenu: a bar of items, or full-width rows for a drawer or folded menu. auto is a bar until its nearest size container, such as a TopBar, is under 640px wide, then rows.
panelWidthstring | number"720px"NavMenu: the widest a panel grows, as a CSS length or pixels. Never wider than the viewport. Sets --eq-nav-menu-panel-width.
...restComponentProps<"nav">noneNavMenu: every native nav attribute.
NavMenuItem labelReactNodenoneThe trigger's or link's text.
NavMenuItem hrefstringnoneMakes the item a plain link. Leave it out and give children for a panel.
NavMenuItem currentbooleanfalseOn a link, aria-current="page". On a trigger, marks that its panel holds the current page.
NavMenuItem childrenReactNodenoneThe panel content, usually NavMenuSection columns.
NavMenuSection titleReactNodenoneA small heading over a column, also the name of its list.
NavMenuLinkhref, title, description?, icon?, current?noneA rich link in a panel. icon takes an Icon name or your own element; other anchor attributes go to the link.

Anatomy

root.eq-nav-menuThe nav landmark. data-orientation holds the layout.
list.eq-nav-menu-list
item.eq-nav-menu-item
trigger.eq-nav-menu-triggerA button with aria-expanded and popovertarget. data-current when its panel holds the current page.
chevron.eq-nav-menu-chevronTurns over when the panel opens.
link.eq-nav-menu-linkA top-level item that is a plain link.
panel.eq-nav-menu-panelA Popover surface in the top layer, anchored under its trigger.
columns.eq-nav-menu-columnsLays the sections out in 15rem columns that wrap.
section.eq-nav-menu-section
heading.eq-nav-menu-heading
section list.eq-nav-menu-section-list
entry.eq-nav-menu-entryThe li around a card.
card.eq-nav-menu-cardThe rich link.
card icon.eq-nav-menu-card-icon
card text.eq-nav-menu-card-text
card title.eq-nav-menu-card-title
card description.eq-nav-menu-card-description

Keyboard

TabMoves through the triggers and links; with a panel open, into its links and then on to the next item.
EnterSpaceOn a trigger, opens or closes its panel.
ArrowDownOn a trigger, opens the panel and focuses its first link; in a panel, moves to the next link.
ArrowUpIn a panel, moves to the previous link, then back to the trigger.
ArrowRightArrowLeftMoves between the top-level triggers and links.
HomeEndMoves to the first or last top-level item.
EscapeCloses the panel and returns focus to its trigger.

Notes

  • It follows the APG disclosure navigation pattern: triggers are buttons with aria-expanded and panels hold plain links, not role="menu", which would switch screen readers into application mode.
  • Hover opens a panel only for a mouse and only after openDelay, and leaving waits closeDelay, so passing across the bar does not flash panels.
  • A click on a panel that hover just opened keeps it open instead of closing it.
  • Panels are auto popovers in the top layer, so an overflow: hidden ancestor cannot clip them, opening one closes any other, and a click outside closes it.
  • Each panel follows its trigger in the DOM, so Tab reaches its links straight after the trigger.
  • Focus leaving an item and its panel closes the panel.
  • On a touch screen hover does nothing; a tap opens the panel.
  • auto measures the nearest ancestor that is a size container, so inside a TopBar it stacks exactly when the bar folds; with no such ancestor it stays a bar.
  • The keys are the same in both orientations: ArrowDown on a trigger still opens its panel, so stacked items are reached with Tab or ArrowLeft and ArrowRight.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/nav-menu.tsx322 lines
"use client";

import {
  createContext,
  useContext,
  useEffect,
  useId,
  useRef,
  useState,
  type ComponentProps,
  type CSSProperties,
  type FocusEvent,
  type KeyboardEvent,
  type MouseEvent,
  type PointerEvent,
  type ReactElement,
  type ReactNode,
  type ToggleEvent,
} from "react";
import { Icon, type IconName } from "./icon";
import { Popover } from "./popover";
import "./nav-menu.css";

type Timing = { openDelay: number; closeDelay: number };

const NavMenuContext = createContext<Timing>({ openDelay: 120, closeDelay: 280 });

const ITEM = ":scope > li > :is(.eq-nav-menu-trigger, .eq-nav-menu-link)";

export type NavMenuProps = ComponentProps<"nav"> & {
  /** Milliseconds the pointer rests on a trigger before its panel opens. */
  openDelay?: number;
  /** Milliseconds after the pointer leaves before the panel closes, so a diagonal move does not lose it. */
  closeDelay?: number;
  /**
   * horizontal is a bar of items; vertical stacks them as full-width rows, for a folded menu
   * panel or a drawer. auto is horizontal until its nearest size container, such as a TopBar,
   * is under 640px wide, then vertical.
   */
  orientation?: "horizontal" | "vertical" | "auto";
  /** The widest a panel grows, as a CSS length or pixels. Never wider than the viewport. */
  panelWidth?: string | number;
};

export function NavMenu({
  openDelay = 120,
  closeDelay = 280,
  orientation = "horizontal",
  panelWidth,
  className,
  style,
  children,
  "aria-label": label = "Main",
  ...rest
}: NavMenuProps) {
  // Left, Right, Home and End move between the top-level items, as the APG allows for disclosure navigation.
  function handleKeyDown(event: KeyboardEvent<HTMLUListElement>) {
    if (event.altKey || event.ctrlKey || event.metaKey) return;
    const list = event.currentTarget;
    const items = Array.from(list.querySelectorAll<HTMLElement>(ITEM));
    const index = items.indexOf(event.target as HTMLElement);
    if (index === -1) return;
    const forward = getComputedStyle(list).direction === "rtl" ? "ArrowLeft" : "ArrowRight";
    const back = forward === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
    let next: number;
    if (event.key === forward) next = Math.min(index + 1, items.length - 1);
    else if (event.key === back) next = Math.max(index - 1, 0);
    else if (event.key === "Home") next = 0;
    else if (event.key === "End") next = items.length - 1;
    else return;
    event.preventDefault();
    items[next].focus();
  }

  return (
    <NavMenuContext.Provider value={{ openDelay, closeDelay }}>
      <nav
        {...rest}
        aria-label={label}
        className={["eq-nav-menu", className].filter(Boolean).join(" ")}
        data-orientation={orientation}
        style={
          panelWidth === undefined
            ? style
            : ({
                "--eq-nav-menu-panel-width": typeof panelWidth === "number" ? `${panelWidth}px` : panelWidth,
                ...style,
              } as CSSProperties)
        }
      >
        <ul className="eq-nav-menu-list" onKeyDown={handleKeyDown}>
          {children}
        </ul>
      </nav>
    </NavMenuContext.Provider>
  );
}

export type NavMenuItemProps = Omit<ComponentProps<"li">, "children"> & {
  /** The visible text of the trigger or link. */
  label: ReactNode;
  /** Makes the item a plain link. Leave it out and give children to make it open a panel. */
  href?: string;
  /** Marks a link item, or the trigger whose panel holds the current page. */
  current?: boolean;
  /** The panel: NavMenuSection columns, or anything else. */
  children?: ReactNode;
};

export function NavMenuItem({
  label,
  href,
  current = false,
  className,
  children,
  onPointerEnter,
  onPointerLeave,
  onBlur,
  onKeyDown,
  ...rest
}: NavMenuItemProps) {
  const { openDelay, closeDelay } = useContext(NavMenuContext);
  const panelId = useId();
  const triggerRef = useRef<HTMLButtonElement>(null);
  const timer = useRef<number | undefined>(undefined);
  const byHover = useRef(false);
  const [open, setOpen] = useState(false);

  useEffect(() => () => window.clearTimeout(timer.current), []);

  const itemClass = ["eq-nav-menu-item", className].filter(Boolean).join(" ");

  if (children == null) {
    return (
      <li {...rest} className={itemClass}>
        <a className="eq-nav-menu-link" href={href} aria-current={current ? "page" : undefined}>
          {label}
        </a>
      </li>
    );
  }

  const panel = () => document.getElementById(panelId);
  const isOpen = () => panel()?.matches(":popover-open") ?? false;

  function show() {
    window.clearTimeout(timer.current);
    if (!isOpen()) panel()?.showPopover();
  }

  function hide() {
    window.clearTimeout(timer.current);
    if (isOpen()) panel()?.hidePopover();
  }

  function handlePointerEnter(event: PointerEvent<HTMLLIElement>) {
    onPointerEnter?.(event);
    if (event.pointerType !== "mouse") return;
    window.clearTimeout(timer.current);
    if (isOpen()) return;
    timer.current = window.setTimeout(() => {
      byHover.current = true;
      show();
    }, openDelay);
  }

  function handlePointerLeave(event: PointerEvent<HTMLLIElement>) {
    onPointerLeave?.(event);
    if (event.pointerType !== "mouse") return;
    window.clearTimeout(timer.current);
    if (byHover.current) timer.current = window.setTimeout(hide, closeDelay);
  }

  function handleClick(event: MouseEvent<HTMLButtonElement>) {
    window.clearTimeout(timer.current);
    // A click on a panel that hover just opened keeps it open rather than toggling it shut.
    if (isOpen() && byHover.current) event.preventDefault();
    byHover.current = false;
  }

  function handleBlur(event: FocusEvent<HTMLLIElement>) {
    onBlur?.(event);
    // Focus leaving the item and its panel closes the panel, as in the APG disclosure navigation.
    const next = event.relatedTarget;
    if (next instanceof Node && event.currentTarget.contains(next)) return;
    if (next !== null) hide();
  }

  function handleKeyDown(event: KeyboardEvent<HTMLLIElement>) {
    onKeyDown?.(event);
    if (event.defaultPrevented) return;
    const surface = panel();
    if (event.key === "Escape" && isOpen()) {
      // Handled here so focus always lands back on the trigger, wherever it was before.
      event.preventDefault();
      hide();
      triggerRef.current?.focus();
      return;
    }
    if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
    if (!surface) return;
    const links = Array.from(surface.querySelectorAll<HTMLElement>("a[href], button:not(:disabled)"));
    if (links.length === 0) return;
    if (event.target === triggerRef.current) {
      if (event.key !== "ArrowDown") return;
      event.preventDefault();
      byHover.current = false;
      show();
      links[0].focus();
      return;
    }
    const index = links.indexOf(event.target as HTMLElement);
    if (index === -1) return;
    event.preventDefault();
    if (event.key === "ArrowDown") links[Math.min(index + 1, links.length - 1)].focus();
    else if (index === 0) triggerRef.current?.focus();
    else links[index - 1].focus();
  }

  return (
    <li
      {...rest}
      className={itemClass}
      onPointerEnter={handlePointerEnter}
      onPointerLeave={handlePointerLeave}
      onBlur={handleBlur}
      onKeyDown={handleKeyDown}
    >
      <button
        ref={triggerRef}
        type="button"
        className="eq-nav-menu-trigger"
        popoverTarget={panelId}
        aria-expanded={open}
        aria-controls={panelId}
        data-current={current || undefined}
        onClick={handleClick}
      >
        {label}
        <svg
          className="eq-nav-menu-chevron"
          viewBox="0 0 16 16"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinecap="round"
          strokeLinejoin="round"
          aria-hidden="true"
        >
          <path d="M4 6l4 4 4-4" />
        </svg>
      </button>
      <Popover
        id={panelId}
        placement="bottom-start"
        className="eq-nav-menu-panel"
        onToggle={(event: ToggleEvent<HTMLDivElement>) => {
          const opened = event.newState === "open";
          setOpen(opened);
          if (!opened) byHover.current = false;
        }}
      >
        <div className="eq-nav-menu-columns">{children}</div>
      </Popover>
    </li>
  );
}

export type NavMenuSectionProps = Omit<ComponentProps<"div">, "title"> & {
  /** A short heading over the column, also the list's name. */
  title?: ReactNode;
};

export function NavMenuSection({ title, className, children, ...rest }: NavMenuSectionProps) {
  const headingId = useId();
  return (
    <div {...rest} className={["eq-nav-menu-section", className].filter(Boolean).join(" ")}>
      {title != null && (
        <p id={headingId} className="eq-nav-menu-heading">
          {title}
        </p>
      )}
      <ul className="eq-nav-menu-section-list" aria-labelledby={title != null ? headingId : undefined}>
        {children}
      </ul>
    </div>
  );
}

export type NavMenuLinkProps = Omit<ComponentProps<"a">, "title"> & {
  href: string;
  title: ReactNode;
  /** One line under the title. */
  description?: ReactNode;
  /** An Icon name, or your own element. */
  icon?: IconName | ReactElement;
  /** Marks the page the person is on with aria-current="page". */
  current?: boolean;
};

export function NavMenuLink({ href, title, description, icon, current = false, className, ...rest }: NavMenuLinkProps) {
  return (
    <li className="eq-nav-menu-entry">
      <a
        {...rest}
        href={href}
        className={["eq-nav-menu-card", className].filter(Boolean).join(" ")}
        aria-current={current ? "page" : undefined}
      >
        {icon != null && (
          <span className="eq-nav-menu-card-icon" aria-hidden="true">
            {typeof icon === "string" ? <Icon name={icon} size="m" /> : icon}
          </span>
        )}
        <span className="eq-nav-menu-card-text">
          <span className="eq-nav-menu-card-title">{title}</span>
          {description != null && <span className="eq-nav-menu-card-description">{description}</span>}
        </span>
      </a>
    </li>
  );
}
components/equip/nav-menu.css223 lines
@layer equip {
  .eq-nav-menu {
    min-width: 0;
    color: var(--eq-ink);
    font: var(--eq-weight) var(--eq-text-s) / 1.3 var(--eq-font);
  }

  .eq-nav-menu-list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .eq-nav-menu-item {
    display: flex;
  }

  .eq-nav-menu-trigger,
  .eq-nav-menu-link {
    display: inline-flex;
    align-items: center;
    gap: var(--eq-space-1);
    height: var(--eq-h-s);
    padding-inline: var(--eq-space-2);
    border: 0;
    border-radius: var(--eq-radius-s);
    background: transparent;
    color: var(--eq-ink-2);
    font: inherit;
    font-weight: var(--eq-weight-strong);
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    transition:
      background-color var(--eq-dur) var(--eq-ease),
      color var(--eq-dur) var(--eq-ease),
      scale var(--eq-dur) var(--eq-ease);
  }

  .eq-nav-menu-link[aria-current="page"],
  .eq-nav-menu-trigger[data-current] {
    color: var(--eq-ink);
    background: var(--eq-tint-strong);
  }

  .eq-nav-menu-trigger[aria-expanded="true"] {
    color: var(--eq-ink);
    background: var(--eq-tint);
  }

  .eq-nav-menu-chevron {
    flex: none;
    width: 14px;
    height: 14px;
    color: var(--eq-ink-3);
    transition: rotate var(--eq-dur-move) var(--eq-spring);
  }

  .eq-nav-menu-trigger[aria-expanded="true"] .eq-nav-menu-chevron {
    rotate: 180deg;
  }

  @media (hover: hover) {
    .eq-nav-menu-trigger:hover,
    .eq-nav-menu-link:hover {
      background: var(--eq-tint);
      color: var(--eq-ink);
    }
  }

  .eq-nav-menu-trigger:active,
  .eq-nav-menu-link:active {
    scale: var(--eq-press);
  }

  .eq-nav-menu-trigger:focus-visible,
  .eq-nav-menu-link:focus-visible {
    outline: 2px solid var(--eq-focus);
    outline-offset: 2px;
  }

  /* ---- Vertical: full-width rows, for a folded menu panel or a drawer ---- */

  .eq-nav-menu[data-orientation="vertical"] > .eq-nav-menu-list {
    flex-direction: column;
    align-items: stretch;
  }

  .eq-nav-menu[data-orientation="vertical"] > .eq-nav-menu-list > .eq-nav-menu-item > :is(.eq-nav-menu-trigger, .eq-nav-menu-link) {
    flex: 1;
    justify-content: space-between;
    height: var(--eq-h-m);
    padding-inline: var(--eq-space-3);
    font-size: var(--eq-text-m);
  }

  /* auto: the same rows once the nearest size container (a TopBar folds at the same width) is narrow. */
  @container (width < 640px) {
    .eq-nav-menu[data-orientation="auto"] > .eq-nav-menu-list {
      flex-direction: column;
      align-items: stretch;
    }

    .eq-nav-menu[data-orientation="auto"] > .eq-nav-menu-list > .eq-nav-menu-item > :is(.eq-nav-menu-trigger, .eq-nav-menu-link) {
      flex: 1;
      justify-content: space-between;
      height: var(--eq-h-m);
      padding-inline: var(--eq-space-3);
      font-size: var(--eq-text-m);
    }
  }

  /* The Popover surface, widened for columns and given less padding so the cards set the rhythm. */
  .eq-popover.eq-nav-menu-panel {
    max-width: min(var(--eq-nav-menu-panel-width, 720px), calc(100vw - 2 * var(--eq-space-4)));
    padding: var(--eq-space-2);
  }

  /* Columns of 15rem that wrap when the viewport is narrower than all of them side by side. */
  .eq-nav-menu-columns {
    display: flex;
    flex-wrap: wrap;
    gap: var(--eq-space-2) var(--eq-space-3);
  }

  .eq-nav-menu-section {
    flex: 1 1 15rem;
    inline-size: 15rem;
    min-width: 0;
    max-width: 100%;
  }

  .eq-nav-menu-heading {
    margin: 0;
    padding: var(--eq-space-2) var(--eq-space-2) var(--eq-space-1);
    color: var(--eq-ink-3);
    font-size: var(--eq-text-xs);
    font-weight: var(--eq-weight-strong);
  }

  .eq-nav-menu-section-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .eq-nav-menu-entry {
    display: block;
  }

  .eq-nav-menu-card {
    display: flex;
    align-items: flex-start;
    gap: var(--eq-space-3);
    padding: var(--eq-space-2);
    border-radius: var(--eq-radius-m);
    color: var(--eq-ink);
    text-decoration: none;
    transition:
      background-color var(--eq-dur) var(--eq-ease),
      scale var(--eq-dur) var(--eq-ease);
  }

  .eq-nav-menu-card-icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 32px;
    height: 32px;
    border: 1px solid var(--eq-line);
    border-radius: var(--eq-radius-m);
    background: var(--eq-surface);
    color: var(--eq-ink);
  }

  .eq-nav-menu-card-icon > svg {
    width: 18px;
    height: 18px;
  }

  .eq-nav-menu-card-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
  }

  .eq-nav-menu-card-title {
    font-weight: var(--eq-weight-strong);
  }

  .eq-nav-menu-card-description {
    color: var(--eq-ink-2);
    font-size: var(--eq-text-xs);
    line-height: 1.4;
  }

  .eq-nav-menu-card[aria-current="page"] {
    background: var(--eq-tint-strong);
  }

  @media (hover: hover) {
    .eq-nav-menu-card:hover:not([aria-current="page"]) {
      background: var(--eq-tint);
    }
  }

  .eq-nav-menu-card:active {
    scale: var(--eq-press);
  }

  .eq-nav-menu-card:focus-visible {
    outline: 2px solid var(--eq-focus);
    outline-offset: -2px;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index