2026.05.27 リリースしました

確認する

Global Navigation

GlobalNavigationは、Webサイトやアプリケーション全体を通じて常に表示される主要なナビゲーションメニューです。

プロパティ

GlobalNavigationは、以下のプロパティをサポートしています。

名前 型 デフォルト値 説明
menus GlobalNavigationItem[] メニューとして表示する値の配列
direction string left メニューとして表示する値の配列です。left, right のいずれかを選択できます。
currentIndex number 現在の階層を指定するnumberです。
children Snippet<[GlobalNavigationItem, number]> サブメニューの内容を表示するスニペットです。
startContent Snippet<[GlobalNavigationItem, number]> 各メニューの左側に置けるコンテンツです。

GlobalNavigationItem

GlobalNavigationItemは、各メニュー項目を表すオブジェクトです。

名前 型 デフォルト値 説明
id any メニューのidです。
label string メニューのラベルです。
link GlobalNavigationLink メニューのリンク先です。
disabled boolean 操作できるかどうか。
hasSubMenu boolean 表示するコンテンツがあるかどうか。

GlobalNavigationLink

GlobalNavigationLinkは、リンク情報を表すオブジェクトです。

名前 型 デフォルト値 説明
href string 遷移するリンク先です。
blank boolean 別タブで開くかどうか。

インストールの手順

以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。

modules/GlobalNavigation.svelte
        <!--
@component
## 概要
- Webサイトやアプリケーション全体を通じて常に表示される主要なナビゲーションメニューです

## 機能
- 渡した配列をメニューとして表示できる

## Props
- menus: メニューとして表示する値の配列
- direction: メニューを表示する方向

## Usage
```svelte
  <GlobalNavigation {menus} />
```
-->

<script module lang="ts">
  import type { Snippet } from 'svelte';
  import type { ClassValue } from 'svelte/elements';
  import { cva, type VariantProps } from 'class-variance-authority';

  export const globalNavigationVariants = cva('relative flex items-center size-full gap-1', {
    variants: {
      /** メニューをどちらに寄せるか */
      direction: {
        left: ['justify-start'],
        right: ['justify-end'],
      },
    },
    defaultVariants: {
      direction: 'left',
    },
  });

  export const globalNavigationItemVariants = cva('relative block place-content-center h-full group cursor-pointer focus-visible:outline-none', {
    variants: {
      /** 操作できるかどうか */
      current: {
        true: [
          'text-primary before:absolute before:inset-x-0 before:bottom-0 before:block before:content-[""] before:w-[calc(100%-1rem)] before:h-0.5 before:bg-primary before:rounded-t-[1px] before:mx-auto',
        ],
        false: [],
      },
      /** 操作できるかどうか */
      disabled: {
        true: ['opacity-50 pointer-events-none'],
        false: [],
      },
    },
  });

  export const menuContainerVariants = cva('absolute top-full left-0 grid w-full gap-3 p-6 bg-surface border border-border rounded-md shadow-lg text-sm origin-top');

  export interface GlobalNavigationProps extends GlobalNavigationVariants {
    /** メニューとして表示したい要素の配列 */
    menus: GlobalNavigationItem[];
    /** 現在の階層かどうか */
    currentIndex?: any;
    class?: ClassValue;
    /** サブメニューコンテンツ */
    children?: Snippet<[GlobalNavigationItem, number]>;
    /** メニュー左側のコンテンツ */
    startContent?: Snippet<[GlobalNavigationItem, number]>;
  }

  export interface GlobalNavigationItem {
    /** メニューのid */
    id?: any;
    /** メニューのラベル */
    label: string;
    /** 遷移先の指定 */
    link?: GlobalNavigationLink;
    /** 操作できるかどうか */
    disabled?: boolean;
    /** コンテンツがあるか */
    hasSubMenu?: boolean;
  }

  export interface GlobalNavigationLink {
    /** 遷移するリンク先 */
    href: string;
    /** 別タブで開くかどうか */
    blank?: boolean;
  }

  export type GlobalNavigationVariants = VariantProps<typeof globalNavigationVariants>;
</script>

<script lang="ts">
  import ChevronUp from '@lucide/svelte/icons/chevron-up';
  import ExternalLink from '@lucide/svelte/icons/external-link';
  import { scale } from 'svelte/transition';

  let { menus, currentIndex, direction = 'left', class: className, children, startContent }: GlobalNavigationProps = $props();

  let subMenuElement = $state<HTMLElement>();
  let openMenuTriggerElement = $state<HTMLElement>();
  let openMenuItem = $state.raw<GlobalNavigationItem>();

  let globalNavigationVariantsClass = $derived(globalNavigationVariants({ direction }));

  /**
   * サブメニュー領域外をクリックした際にメニューを閉じるハンドラ。
   * @param event ドキュメント全体で発生したクリックイベント
   */
  function handleClickOutside(event: MouseEvent) {
    if (!(event.target instanceof Node)) return;
    if (subMenuElement && subMenuElement.contains(event.target)) {
      return;
    }
    if (openMenuTriggerElement && openMenuTriggerElement.contains(event.target)) {
      return;
    }
    closeMenu();
  }

  /** サブメニューを閉じる */
  function closeMenu() {
    openMenuItem = undefined;
    openMenuTriggerElement = undefined;
    subMenuElement = undefined;
  }

  /** Escapeキーでサブメニューを閉じる */
  function handleKeyDown(event: KeyboardEvent) {
    if (event.key === 'Escape') {
      openMenuItem = undefined;
    }
  }

  /** サブメニューを開く */
  function handleClickMenu(item: GlobalNavigationItem, event: MouseEvent) {
    if (item.hasSubMenu) {
      if (openMenuItem === item) {
        closeMenu();
        return;
      }

      if (event.currentTarget instanceof HTMLElement) {
        openMenuTriggerElement = event.currentTarget;
      }
      else {
        openMenuTriggerElement = undefined;
      }

      openMenuItem = item;
      return;
    }

    closeMenu();
  }
</script>

<svelte:document onkeydown={handleKeyDown} onclick={handleClickOutside} />

<nav class={[className]} data-rabee-ui="global-navigation">
  <ul class={globalNavigationVariantsClass}>
    {#each menus as item, index}
      <li class="relative h-full">
        {#if item.hasSubMenu}
          <button class={globalNavigationItemVariants({ current: index === currentIndex, disabled: item.disabled })} onclick={(e) => handleClickMenu(item, e)} disabled={item.disabled}>
            {@render menuContent()}
          </button>
        {:else}
          <a class={globalNavigationItemVariants({ current: index === currentIndex, disabled: item.disabled })} href={item.link?.href} target={item.link?.blank ? '_blank' : '_self'} tabindex={item.disabled ? -1 : 0} aria-disabled={item.disabled}>
            {@render menuContent()}
          </a>
        {/if}
        {#snippet menuContent()}
          <div class={['flex items-center gap-1 px-3 py-2 rounded-md text-sm outline-ring transition-[background-color] group-focus-visible:outline-2 group-focus-visible:outline-offset-2', openMenuItem === item ? 'bg-primary/10 group-hover:bg-primary/20' : 'group-hover:bg-accent/90 active:bg-accent/90']}>
            {#if startContent}
              {@render startContent(item, index)}
            {/if}
            {item.label}
            {#if item.hasSubMenu}
              <ChevronUp class={['transition-transform', openMenuItem === item ? 'rotate-0' : '-rotate-180']} size="1rem" />
            {:else if item.link?.blank}
              <ExternalLink size="1rem" />
            {/if}
          </div>
        {/snippet}
      </li>
      {#if openMenuItem === item && item.hasSubMenu}
        <div class={menuContainerVariants()} bind:this={subMenuElement} transition:scale={{ start: 0.98, opacity: 0, duration: 100 }}>
          {@render children?.(item, index)}
        </div>
      {/if}
    {/each}
  </ul>
</nav>

      

使い方


サンプル

Default

特に操作が行われていない、デフォルトの状態です。

Direction

メニューを寄せる方向を指定できます。

Current

現在表示中のページやコンテンツの位置を示します。

Disabled

操作できない状態です。

List

リストを表示させることもできます。

With Icon

メニューとアイコンを組み合わせた例です。