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 w-full gap-1', {
    variants: {
      /** メニューをどちらに寄せるか */
      direction: {
        left: ['justify-start'],
        right: ['justify-end'],
      },
    },
    defaultVariants: {
      direction: 'left',
    },
  });

  export const globalNavigationItemVariants = cva('relative block py-2.5 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-base-container-default border border-base-stroke-default 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, ExternalLink } from '@lucide/svelte';
  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 {MouseEvent} e - ドキュメント全体で発生したクリックイベント
   */
  function onClickOutside(e: MouseEvent) {
    if (!(e.target instanceof Node)) return;
    if (subMenuElement && subMenuElement.contains(e.target)) {
      return;
    }
    if (openMenuTriggerElement && openMenuTriggerElement.contains(e.target)) {
      return;
    }
    closeMenu();
  }

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

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

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

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

      openMenuItem = item;
      return;
    }

    closeMenu();
  }
</script>

<svelte:document onkeydown={onKeyDown} onclick={onClickOutside} />

<nav class={[className]}>
  <ul class={globalNavigationVariantsClass}>
    {#each menus as item, index}
      <li class="relative">
        {#if item.hasSubMenu}
          <button class={globalNavigationItemVariants({ current: index === currentIndex, disabled: item.disabled })} onclick={(e) => onClickMenu(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'}>
            {@render menuContent()}
          </a>
        {/if}
        {#snippet menuContent()}
          <div class={['flex items-center gap-1 px-3 py-2 rounded-md text-sm outline-primary transition-[background-color] group-focus-visible:outline-[0.125rem] group-focus-visible:outline-offset-[0.125rem]', openMenuItem === item ? 'bg-primary/10 group-hover:bg-primary/20' : 'group-hover:bg-base-container-accent/90 active:bg-base-container-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

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