Skip to content

Navbar

A responsive navigation bar with desktop and mobile layouts, plus sticky and floating modes. It includes a dedicated mobile menu slot and supports a floating-to-full-width transition when floating and sticky are used together.

Features

  • 📱 Responsive by default: desktop and mobile layouts with configurable media query breakpoint
  • 📌 Sticky mode: pins the navbar to the top of the viewport
  • 🫧 Floating mode: detached top navbar with rounded panel styling
  • 🔄 Floating + Sticky transition: starts floating, becomes full-width sticky when scrolling past scroll-threshold
  • 🎛️ Imperative API: open/close/toggle mobile menu from code
  • Accessible semantics: nav landmark label, expanded state, keyboard-close on Escape
  • 🧩 Composable slots: logo, start, default, end, and mobile-menu

Source Code

View Source Code (Navbar)
ts
import {
  computed,
  createContext,
  define,
  html,
  inject,
  listen,
  onMounted,
  provide,
  signal,
  type ReadonlySignal,
  watch,
} from '@vielzeug/craftit';
import { resizeObserver } from '@vielzeug/craftit/observers';

import type { ElevationLevel, RoundedSize, ThemeColor, VisualVariant } from '../../types';

import '../../content/icon/icon';
import {
  coarsePointerMixin,
  colorThemeMixin,
  elevationMixin,
  frostVariantMixin,
  reducedMotionMixin,
  roundedVariantMixin,
} from '../../styles';
import navbarStyles from './navbar.css?inline';

type NavbarMode = 'floating' | 'sticky';

const hasElementContent = (el: Element): boolean => {
  if (!(el instanceof HTMLElement)) return true;

  if (el.tagName.includes('-')) return true;

  if (el.childElementCount > 0) return true;

  return (el.textContent ?? '').trim().length > 0;
};

const parseMaxWidthPx = (query: string | undefined): number | undefined => {
  const value = String(query ?? '').trim();

  if (!value) return undefined;

  const match = /max-width\s*:\s*([0-9]+(?:\.[0-9]+)?)px/i.exec(value);

  if (!match) return undefined;

  const parsed = Number.parseFloat(match[1]);

  return Number.isFinite(parsed) ? parsed : undefined;
};

const resolveContainerElement = (el: HTMLElement): HTMLElement | null => {
  let container = el.parentElement;

  while (container?.tagName.toLowerCase() === 'bit-grid-item') {
    container = container.parentElement;
  }

  return container;
};

const readContainerWidth = (el: HTMLElement): number => {
  const parentWidth = resolveContainerElement(el)?.clientWidth ?? 0;

  if (parentWidth > 0) return parentWidth;

  return el.offsetWidth;
};

const findScrollContainer = (start: HTMLElement): HTMLElement | null => {
  let current: HTMLElement | null = start.parentElement;

  while (current) {
    const styles = window.getComputedStyle(current);
    const overflowY = styles.overflowY;

    if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') {
      return current;
    }

    current = current.parentElement;
  }

  return null;
};

/** Context provided by `bit-navbar` to `bit-navbar-item` children. */
export type NavbarContext = {
  isMobile: ReadonlySignal<boolean>;
  mobileMenuOpen: ReadonlySignal<boolean>;
};

/** Injection key for navbar context. */
export const NAVBAR_CTX = createContext<NavbarContext>('NavbarContext');

/** Navbar component events */
export type BitNavbarEvents = {
  'mobile-menu-change': { open: boolean };
};

/** Navbar component element interface */
export type NavbarElement = HTMLElement &
  BitNavbarProps & {
    closeMobileMenu(): void;
    openMobileMenu(): void;
    toggleMobileMenu(): void;
  };

type MobileSidebarElement = HTMLElement & {
  closeMobile?: () => void;
  openMobile?: () => void;
  toggleMobile?: () => void;
};

/** Navbar component properties */
export type BitNavbarProps = {
  /** CSS media query used for mobile layout switching */
  breakpoint?: string;
  /** Theme color */
  color?: ThemeColor;
  /** Evaluate mobile breakpoint against container width only when possible. */
  'container-breakpoints'?: boolean;
  /** Shadow elevation level (0-5) */
  elevation?: `${ElevationLevel}`;
  /** Start as detached floating navbar */
  floating?: boolean;
  /** Accessible nav landmark label */
  label?: string;
  /** CSS selector for an external sidebar toggled by the mobile button */
  'mobile-sidebar'?: string;
  /** Border radius size */
  rounded?: RoundedSize;
  /** Scroll threshold in px used by floating+sticky transition */
  'scroll-threshold'?: number;
  /** Stick to top of viewport */
  sticky?: boolean;
  /** Visual style variant */
  variant?: Exclude<VisualVariant, 'ghost' | 'text'>;
};

/** Navbar item properties */
export type BitNavbarItemProps = {
  /** Whether this item represents the current page */
  active?: boolean;
  /** Whether this item is disabled */
  disabled?: boolean;
  /** Link target URL. Renders an anchor when provided. */
  href?: string;
  /** Relationship of linked URL (anchor-only) */
  rel?: string;
  /** Browsing context for link (anchor-only) */
  target?: string;
};

/**
 * `bit-navbar` — Responsive navigation with sticky/floating modes and a mobile overflow panel.
 *
 * @element bit-navbar
 *
 * @attr {string} label - Accessible nav landmark label
 * @attr {boolean} sticky - Makes navbar sticky at top
 * @attr {boolean} floating - Makes navbar detached and floating
 * @attr {number} scroll-threshold - Scroll threshold for floating+sticky transition
 * @attr {string} breakpoint - CSS media query used for mobile mode
 * @attr {boolean} container-breakpoints - Evaluates parseable max-width breakpoints against container width
 * @attr {string} variant - Visual variant
 * @attr {string} color - Theme color
 * @attr {string} rounded - Border radius token
 * @attr {string} elevation - Shadow elevation level
 *
 * @fires mobile-menu-change - Emitted when mobile menu open state changes
 *
 * @slot logo - Brand/logo content rendered at the leading edge
 * @slot start - Content rendered in the left/start region
 * @slot - Center navigation content rendered between the start and end regions
 * @slot end - Content rendered in the right/end region
 * @slot mobile-menu - Controls and links rendered inside the mobile overflow panel
 *
 * @cssprop --blur-lg - Blur strength for floating navbar backdrops
 * @cssprop --blur-md - Blur strength for the mobile panel backdrop
 * @cssprop --border - Border token used by the navbar surface and separators
 * @cssprop --color-canvas - Base navbar and mobile panel background
 * @cssprop --color-contrast-100 - Hover/active background for navbar chrome
 * @cssprop --color-contrast-200 - Divider and border contrast color
 * @cssprop --color-contrast-400 - Muted text color for secondary navbar content
 * @cssprop --color-contrast-50 - Soft background for mobile and floating states
 * @cssprop --color-contrast-500 - Secondary text color in the navbar chrome
 * @cssprop --color-contrast-700 - Strong text color for navbar items
 * @cssprop --color-contrast-900 - Deep contrast color used in navbar shadows
 * @cssprop --color-primary - Accent color for active and highlighted navbar states
 * @part nav - Outer navigation landmark
 * @part bar - Main navbar row container
 * @part logo - Logo slot container
 * @part start - Leading action/content region
 * @part center - Center content region
 * @part end - Trailing action/content region
 * @part mobile-toggle - Mobile menu toggle button
 * @part mobile-menu - Mobile overflow panel
 * @part item-icon - Leading icon inside navbar items
 * @part item-label - Label text inside navbar items
 * @part item-end - Trailing content inside navbar items
 * @part item - Clickable navbar item root
 * @example
 * ```html
 * <bit-navbar></bit-navbar>
 * ```
 */
export const NAVBAR_TAG = define<BitNavbarProps, BitNavbarEvents>('bit-navbar', {
  props: {
    breakpoint: '(max-width: 768px)',
    color: undefined,
    'container-breakpoints': false,
    elevation: undefined,
    floating: false,
    label: 'Main navigation',
    'mobile-sidebar': undefined,
    rounded: undefined,
    'scroll-threshold': 80,
    sticky: false,
    variant: undefined,
  },
  setup(props, { emit, host, slots }) {
    const hasLogo = () => slots.has('logo').value;
    const hasMobileMenu = () => slots.elements('mobile-menu').value.some(hasElementContent);
    const mobileSidebarTarget = signal<MobileSidebarElement | null>(null);
    const isExternalMobileMode = signal(false);
    const isExternalMobileOpen = signal(false);
    const isMobile = signal(false);
    const isMobileMenuOpen = signal(false);
    const isModeTransitioning = signal(false);
    const isScrolled = signal(false);
    const scrollBase = signal(0);
    const mediaMatches = signal(false);
    const sizeMatches = signal(false);
    const maxWidthPx = signal<number | undefined>(parseMaxWidthPx(props.breakpoint.value));
    const isPreviewMode = signal(false);

    provide(NAVBAR_CTX, {
      isMobile: computed(() => isMobile.value) as ReadonlySignal<boolean>,
      mobileMenuOpen: computed(() => isMobileMenuOpen.value) as ReadonlySignal<boolean>,
    });

    const computeMode = (): NavbarMode | undefined => {
      if (props.floating.value && props.sticky.value) {
        return isScrolled.value ? 'sticky' : 'floating';
      }

      if (props.floating.value) return 'floating';

      if (props.sticky.value) return 'sticky';

      return undefined;
    };

    const setMobileMenu = (next: boolean) => {
      const open = Boolean(next);

      if (mobileSidebarTarget.value && !hasMobileMenu()) {
        if (open) mobileSidebarTarget.value.openMobile?.();
        else mobileSidebarTarget.value.closeMobile?.();

        return;
      }

      if (open && !hasMobileMenu()) return;

      if (isMobileMenuOpen.value === open) return;

      isMobileMenuOpen.value = open;
      emit('mobile-menu-change', { open });
    };

    const closeMobileMenu = () => setMobileMenu(false);
    const openMobileMenu = () => setMobileMenu(true);
    const toggleMobileMenu = () => {
      if (mobileSidebarTarget.value && !hasMobileMenu()) {
        mobileSidebarTarget.value.toggleMobile?.();

        return;
      }

      setMobileMenu(!isMobileMenuOpen.value);
    };

    const syncMobileMode = () => {
      const next =
        (mobileSidebarTarget.value && !hasMobileMenu() ? isExternalMobileMode.value : false) ||
        mediaMatches.value ||
        sizeMatches.value;

      isMobile.value = next;

      if (!next) closeMobileMenu();
    };

    const readScrollOffset = (scrollContainer: HTMLElement | null): number => {
      if (scrollContainer) {
        return scrollContainer.scrollTop || 0;
      }

      return window.scrollY || document.documentElement.scrollTop || 0;
    };

    const resetScrollBase = (scrollContainer: HTMLElement | null) => {
      scrollBase.value = readScrollOffset(scrollContainer);
    };

    const updateScrolled = (scrollContainer: HTMLElement | null = null) => {
      if (!(props.floating.value && props.sticky.value)) {
        if (isScrolled.value) isScrolled.value = false;

        return;
      }

      const threshold = Math.max(0, Number(props['scroll-threshold'].value) || 0);
      const currentOffset = readScrollOffset(scrollContainer);
      const delta = currentOffset - scrollBase.value;

      isScrolled.value = delta > threshold;
    };

    host.bind({
      attr: {
        'data-mobile': () => (isMobile.value ? true : undefined),
        'data-mobile-open': () => (isMobile.value && isMobileMenuOpen.value ? true : undefined),
        'data-mode': () => computeMode() ?? null,
        'data-mode-transitioning': () => (isModeTransitioning.value ? true : undefined),
        'data-preview-mode': () => (isPreviewMode.value ? true : undefined),
        'data-scrolled': () => (props.floating.value && props.sticky.value && isScrolled.value ? true : undefined),
      },
    });

    onMounted(() => {
      const el = host.el as NavbarElement;
      const scrollContainer = findScrollContainer(host.el);

      el.closeMobileMenu = closeMobileMenu;
      el.openMobileMenu = openMobileMenu;
      el.toggleMobileMenu = toggleMobileMenu;

      const stopScroll = listen(
        window,
        'scroll',
        () => {
          updateScrolled(scrollContainer);
        },
        { passive: true },
      );
      const stopContainerScroll = scrollContainer
        ? listen(
            scrollContainer,
            'scroll',
            () => {
              updateScrolled(scrollContainer);
            },
            { passive: true },
          )
        : undefined;
      const stopEscape = listen(host.el, 'keydown', (event: KeyboardEvent) => {
        if (event.key !== 'Escape' || !isMobileMenuOpen.value) return;

        closeMobileMenu();
      });

      let mediaCleanup: (() => void) | undefined;
      let mobileSidebarCleanup: (() => void) | undefined;
      let modeTransitionTimeout: ReturnType<typeof setTimeout> | undefined;
      let previousMode: NavbarMode | undefined;

      const resolveMobileSidebarTarget = () => {
        mobileSidebarCleanup?.();
        mobileSidebarCleanup = undefined;
        mobileSidebarTarget.value = null;
        isExternalMobileMode.value = false;
        isExternalMobileOpen.value = false;

        const selector = String(props['mobile-sidebar'].value ?? '').trim();

        if (!selector) return;

        const root = host.el.getRootNode();
        const scopedTarget =
          root instanceof ShadowRoot || root instanceof Document
            ? (root.querySelector(selector) as MobileSidebarElement | null)
            : null;
        const target = scopedTarget ?? (document.querySelector(selector) as MobileSidebarElement | null);

        if (!target) return;

        const syncTargetState = () => {
          isExternalMobileMode.value = target.hasAttribute('data-bottom-nav');
          isExternalMobileOpen.value = target.hasAttribute('data-mobile-open');
          syncMobileMode();
        };

        const targetObserver = new MutationObserver(() => {
          syncTargetState();
        });

        syncTargetState();
        targetObserver.observe(target, {
          attributeFilter: ['data-bottom-nav', 'data-mobile-open'],
          attributes: true,
        });
        target.addEventListener('mobile-open-change', syncTargetState as EventListener);
        mobileSidebarCleanup = () => {
          targetObserver.disconnect();
          target.removeEventListener('mobile-open-change', syncTargetState as EventListener);
        };
        mobileSidebarTarget.value = target;
      };

      const triggerModeTransition = () => {
        isModeTransitioning.value = true;

        if (modeTransitionTimeout != null) clearTimeout(modeTransitionTimeout);

        modeTransitionTimeout = setTimeout(() => {
          isModeTransitioning.value = false;
          modeTransitionTimeout = undefined;
        }, 220);
      };

      const stopModeWatch = watch(
        computed(() => props.floating.value && props.sticky.value),
        (enabled) => {
          if (!enabled) {
            if (isScrolled.value) isScrolled.value = false;

            if (isModeTransitioning.value) isModeTransitioning.value = false;

            previousMode = undefined;

            if (modeTransitionTimeout != null) {
              clearTimeout(modeTransitionTimeout);
              modeTransitionTimeout = undefined;
            }

            return;
          }

          resetScrollBase(scrollContainer);
          updateScrolled(scrollContainer);

          const currentMode = computeMode();

          previousMode = currentMode;
        },
        { immediate: true },
      );

      const stopMobileSidebarWatch = watch(
        props['mobile-sidebar'],
        () => {
          resolveMobileSidebarTarget();
        },
        { immediate: true },
      );

      const stopModeTransitionWatch = watch(
        computed(() => computeMode()),
        (nextMode) => {
          const comboEnabled = props.floating.value && props.sticky.value;

          if (!comboEnabled) {
            previousMode = nextMode ?? undefined;

            return;
          }

          const next = nextMode ?? undefined;

          if (previousMode && next && previousMode !== next) {
            triggerModeTransition();
          }

          previousMode = next;
        },
        { immediate: true },
      );

      const stopMobileMenuSlotWatch = watch(
        computed(() => hasMobileMenu()),
        (hasContent) => {
          if (!hasContent && isMobileMenuOpen.value) {
            closeMobileMenu();
          }
        },
        { immediate: true },
      );

      watch(
        props.breakpoint,
        (query) => {
          mediaCleanup?.();
          mediaCleanup = undefined;

          const mediaQuery = String(query ?? '').trim();

          maxWidthPx.value = parseMaxWidthPx(mediaQuery);
          mediaMatches.value = false;

          const width = readContainerWidth(host.el);

          sizeMatches.value = width > 0 && maxWidthPx.value != null ? width <= maxWidthPx.value : false;
          syncMobileMode();

          if (props['container-breakpoints'].value && maxWidthPx.value != null) {
            return;
          }

          if (!mediaQuery) {
            return;
          }

          if (typeof window.matchMedia !== 'function') {
            return;
          }

          const mql = window.matchMedia(mediaQuery);
          const syncMedia = (next: boolean) => {
            mediaMatches.value = next;
            syncMobileMode();
          };
          const onChange = (event: MediaQueryListEvent) => {
            syncMedia(event.matches);
          };

          syncMedia(mql.matches);
          mql.addEventListener('change', onChange);

          mediaCleanup = () => {
            mql.removeEventListener('change', onChange);
          };
        },
        { immediate: true },
      );

      const stopResizeEffect =
        typeof ResizeObserver === 'function'
          ? (() => {
              const hostSize = resizeObserver(host.el);
              const wrapperEl = host.el.parentElement;
              const containerEl = resolveContainerElement(host.el);
              const wrapperSize = wrapperEl ? resizeObserver(wrapperEl) : undefined;
              const parentSize = containerEl && containerEl !== wrapperEl ? resizeObserver(containerEl) : undefined;

              return watch(
                computed(() => [
                  hostSize.value.width,
                  hostSize.value.height,
                  wrapperSize?.value.width,
                  wrapperSize?.value.height,
                  parentSize?.value.width,
                  parentSize?.value.height,
                ]),
                () => {
                  const width = readContainerWidth(host.el);
                  const parentWidth = resolveContainerElement(host.el)?.clientWidth ?? 0;

                  sizeMatches.value = width > 0 && maxWidthPx.value != null ? width <= maxWidthPx.value : false;
                  isPreviewMode.value = parentWidth > 0 && parentWidth < window.innerWidth;
                  syncMobileMode();
                },
              );
            })()
          : undefined;

      return () => {
        stopEscape?.();
        stopScroll?.();
        stopContainerScroll?.();
        stopModeWatch?.();
        stopModeTransitionWatch?.();
        stopMobileMenuSlotWatch?.();
        stopMobileSidebarWatch?.();
        mediaCleanup?.();
        mobileSidebarCleanup?.();
        stopResizeEffect?.();

        if (modeTransitionTimeout != null) {
          clearTimeout(modeTransitionTimeout);
          modeTransitionTimeout = undefined;
        }
      };
    });

    return () => html`
      <nav part="nav" aria-label="${props.label}">
        <div class="navbar" part="bar">
          <div class="navbar-logo" part="logo" ?hidden=${() => !hasLogo()}>
            <slot name="logo"></slot>
          </div>

          <div class="navbar-start" part="start">
            <slot name="start"></slot>
          </div>

          <div class="navbar-center" part="center">
            <slot></slot>
          </div>

          <div class="navbar-end" part="end">
            <slot name="end"></slot>
          </div>

          <button
            class="mobile-toggle"
            part="mobile-toggle"
            type="button"
            aria-label="${() =>
              hasMobileMenu()
                ? isMobileMenuOpen.value
                  ? 'Close navigation menu'
                  : 'Open navigation menu'
                : isExternalMobileOpen.value
                  ? 'Close navigation menu'
                  : 'Open navigation menu'}"
            aria-controls="mobile-menu-panel"
            aria-expanded="${() => String(hasMobileMenu() ? isMobileMenuOpen.value : isExternalMobileOpen.value)}"
            ?hidden=${() => !isMobile.value || (!hasMobileMenu() && !mobileSidebarTarget.value)}
            @click="${toggleMobileMenu}">
            <bit-icon
              name="${() => ((hasMobileMenu() ? isMobileMenuOpen.value : isExternalMobileOpen.value) ? 'x' : 'menu')}"
              size="18"
              stroke-width="2.5"
              aria-hidden="true"></bit-icon>
          </button>
        </div>

        <div
          id="mobile-menu-panel"
          class="mobile-menu-panel"
          part="mobile-menu"
          role="navigation"
          :aria-label="${() => `${props.label.value} mobile menu`}"
          ?hidden=${() => !isMobile.value || !isMobileMenuOpen.value || !hasMobileMenu()}>
          <slot name="mobile-menu"></slot>
        </div>
      </nav>
    `;
  },
  styles: [
    colorThemeMixin,
    elevationMixin,
    roundedVariantMixin,
    coarsePointerMixin,
    reducedMotionMixin,
    frostVariantMixin('.navbar'),
    navbarStyles,
  ],
});

/**
 * `bit-navbar-item` — Navigation item for navbar slots.
 *
 * Renders as an anchor when `href` is provided and item is not disabled.
 *
 * @element bit-navbar-item
 */
export const NAVBAR_ITEM_TAG = define<BitNavbarItemProps>('bit-navbar-item', {
  props: {
    active: false,
    disabled: false,
    href: undefined,
    rel: undefined,
    target: undefined,
  },
  setup(props, { host, slots }) {
    const hasIcon = () => slots.has('icon').value;
    const hasEnd = () => slots.has('end').value;
    const navbarCtx = inject(NAVBAR_CTX);

    host.bind({
      attr: {
        'navbar-mobile': () => (navbarCtx?.isMobile.value ? true : undefined),
        'navbar-mobile-open': () => (navbarCtx?.mobileMenuOpen.value ? true : undefined),
      },
    });

    const isLink = () => Boolean(props.href.value) && !props.disabled.value;

    const renderItemContent = () => html`
      <span class="item-icon" part="item-icon" ?hidden=${() => !hasIcon()} aria-hidden="true">
        <slot name="icon"></slot>
      </span>
      <span class="item-label" part="item-label"><slot></slot></span>
      <span class="item-end" part="item-end" ?hidden=${() => !hasEnd()}>
        <slot name="end"></slot>
      </span>
    `;

    return () => html`
      ${() => {
        if (isLink()) {
          return html`
            <a
              class="item"
              part="item"
              href="${props.href}"
              :rel="${props.rel}"
              :target="${props.target}"
              aria-current="${() => (props.active.value ? 'page' : null)}">
              ${renderItemContent()}
            </a>
          `;
        }

        if (props.disabled.value) {
          return html`<div class="item" part="item" tabindex="-1" aria-disabled="true">${renderItemContent()}</div>`;
        }

        return html`<button class="item" part="item" type="button">${renderItemContent()}</button>`;
      }}
    `;
  },
  styles: [coarsePointerMixin, navbarStyles],
});

Basic Usage

PreviewCode
RTL

Position Modes

Sticky

PreviewCode
RTL

Floating

PreviewCode
RTL

Floating + Sticky Transition

When both are set, the navbar starts as floating and switches to full-width sticky once page scroll passes scroll-threshold.

PreviewCode
RTL

Recommended app-shell pattern:

  • Desktop: keep bit-sidebar visible and use the navbar for top-level context/actions.
  • Mobile: switch sidebar to bottom-nav mode and toggle its drawer from the navbar hamburger.
  • Use mobile-sidebar on bit-navbar and bottom-nav-at="(max-width: ... )" on bit-sidebar.
PreviewCode
RTL

bit-navbar-item

bit-navbar-item renders as an anchor when href is set, otherwise as a button.

PreviewCode
RTL

Imperative API

js
const navbar = document.querySelector('bit-navbar');

navbar.openMobileMenu();
navbar.closeMobileMenu();
navbar.toggleMobileMenu();

Events

js
const navbar = document.querySelector('bit-navbar');

navbar.addEventListener('mobile-menu-change', (e) => {
  console.log('Mobile menu open:', e.detail.open);
});

API Reference

bit-navbar Attributes

AttributeTypeDefaultDescription
labelstring'Main navigation'Accessible nav landmark label
stickybooleanfalseEnables sticky mode
floatingbooleanfalseEnables floating mode
scroll-thresholdnumber80Scroll px threshold for floating+sticky transition
breakpointstring'(max-width: 768px)'Media query used for mobile mode
container-breakpointsbooleanfalseEvaluates parseable max-width breakpoints against the navbar container width
variant'solid' | 'flat' | 'bordered' | 'outline' | 'glass' | 'frost'Surface style variant
colorThemeColorTheme color
roundedRoundedSizeBorder radius token
elevation'0' | '1' | '2' | '3' | '4' | '5'Elevation shadow level

bit-navbar Slots

SlotDescription
logoBrand mark/logo
startLeft-aligned desktop content
(default)Center desktop content
endRight-aligned desktop content
mobile-menuMobile-only expandable content panel

bit-navbar Events

EventDetailDescription
mobile-menu-change{ open: boolean }Fired when mobile menu open state changes

bit-navbar-item Attributes

AttributeTypeDefaultDescription
hrefstringLink URL (renders anchor when set)
activebooleanfalseMarks item as current page (aria-current="page")
disabledbooleanfalseDisables interaction
relstringLink relationship when href is set
targetstringLink target when href is set