Global Navigation
GlobalNavigationは、Webサイトやアプリケーション全体を通じて常に表示される主要なナビゲーションメニューです。
<script lang="ts">
import GlobalNavigation from '$lib/components/ui/modules/GlobalNavigation.svelte';
let menus = [
{
id: 'menu1',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-3', blank: false },
},
{
id: 'menu3',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-3', blank: false },
},
{
label: 'メニュー',
link: { href: '#heading-sub-3', blank: false },
},
];
</script>
<div class="size-full">
<GlobalNavigation class="min-w-114" {menus}>
{#snippet children(item)}
{#if item.id === 'menu1'}
ここにメニュー1の要素が入ります。
{/if}
{#if item.id === 'menu3'}
ここにメニュー3の要素が入ります。
{/if}
{/snippet}
</GlobalNavigation>
</div>
プロパティ
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>
使い方
<script lang="ts">
import GlobalNavigation from '$lib/components/ui/modules/GlobalNavigation.svelte';
let menus = [
{
id: 'menu1',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-3', blank: false },
},
{
id: 'menu3',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-3', blank: false },
},
{
label: 'メニュー',
link: { href: '#heading-sub-3', blank: false },
},
];
</script>
<div class="size-full">
<GlobalNavigation class="min-w-114" {menus}>
{#snippet children(item)}
{#if item.id === 'menu1'}
ここにメニュー1の要素が入ります。
{/if}
{#if item.id === 'menu3'}
ここにメニュー3の要素が入ります。
{/if}
{/snippet}
</GlobalNavigation>
</div>
サンプル
Default
特に操作が行われていない、デフォルトの状態です。
<script lang="ts">
import GlobalNavigation from '$lib/components/ui/modules/GlobalNavigation.svelte';
let menus = [
{
id: 'menu1',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-3', blank: false },
},
{
id: 'menu3',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-3', blank: false },
},
{
label: 'メニュー',
link: { href: '#heading-sub-3', blank: false },
},
];
</script>
<div class="size-full">
<GlobalNavigation class="min-w-114" {menus}>
{#snippet children(item)}
{#if item.id === 'menu1'}
ここにメニュー1の要素が入ります。
{/if}
{#if item.id === 'menu3'}
ここにメニュー3の要素が入ります。
{/if}
{/snippet}
</GlobalNavigation>
</div>
Direction
メニューを寄せる方向を指定できます。
<script lang="ts">
import GlobalNavigation from '$lib/components/ui/modules/GlobalNavigation.svelte';
let menus = [
{
id: 'menu1',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-4', blank: false },
},
{
id: 'menu3',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-4', blank: false },
},
{
label: 'メニュー',
link: { href: '#heading-sub-4', blank: false },
},
];
</script>
<div class="size-full">
<GlobalNavigation class="min-w-114" direction="right" {menus}>
{#snippet children(item)}
{#if item.id === 'menu1'}
ここにメニュー1の要素が入ります。
{/if}
{#if item.id === 'menu3'}
ここにメニュー3の要素が入ります。
{/if}
{/snippet}
</GlobalNavigation>
</div>
Current
現在表示中のページやコンテンツの位置を示します。
<script lang="ts">
import GlobalNavigation from '$lib/components/ui/modules/GlobalNavigation.svelte';
let menus = [
{
id: 'menu1',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-5', blank: false },
},
{
id: 'menu3',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-5', blank: false },
},
{
label: 'メニュー',
link: { href: '#heading-sub-5', blank: false },
},
];
</script>
<div class="size-full">
<GlobalNavigation class="min-w-114" {menus} currentIndex={1}>
{#snippet children(item)}
{#if item.id === 'menu1'}
ここにメニュー1の要素が入ります。
{/if}
{#if item.id === 'menu3'}
ここにメニュー3の要素が入ります。
{/if}
{/snippet}
</GlobalNavigation>
</div>
Disabled
操作できない状態です。
<script lang="ts">
import GlobalNavigation from '$lib/components/ui/modules/GlobalNavigation.svelte';
let menus = [
{
id: 'menu1',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-6', blank: false },
disabled: true,
},
{
id: 'menu3',
label: 'メニュー',
hasSubMenu: true,
disabled: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-6', blank: false },
},
{
label: 'メニュー',
link: { href: '#heading-sub-6', blank: false },
},
];
</script>
<div class="size-full">
<GlobalNavigation class="min-w-114" {menus}>
{#snippet children(item)}
{#if item.id === 'menu1'}
ここにメニュー1の要素が入ります。
{/if}
{#if item.id === 'menu3'}
ここにメニュー3の要素が入ります。
{/if}
{/snippet}
</GlobalNavigation>
</div>
List
リストを表示させることもできます。
<script lang="ts">
import GlobalNavigation from '$lib/components/ui/modules/GlobalNavigation.svelte';
let menus = [
{
id: 'menu1',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-7', blank: false },
},
{
id: 'menu3',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-7', blank: false },
},
{
label: 'メニュー',
link: { href: '#heading-sub-7', blank: false },
},
];
let list1 = [
{
items: [
{
label: 'タイトル',
description: 'ここに補足文が入ります。',
link: { href: '#heading-sub-7', blank: false },
},
{
label: 'タイトル',
description:
'ここに補足文が入ります。ここに補足文が入ります。ここに補足文が入ります。',
link: { href: '#heading-sub-7', blank: false },
},
],
},
{
items: [
{
label: 'タイトル',
description: 'ここに補足文が入ります。',
link: { href: '#heading-sub-7', blank: false },
},
{
label: 'タイトル',
description: 'ここに補足文が入ります。',
link: { href: '#heading-sub-7', blank: false },
},
],
},
];
let list3 = [
{
items: [
{
label: 'タイトル',
description: 'ここに補足文が入ります。',
link: { href: '#heading-sub-7', blank: false },
},
{
label: 'タイトル',
description: 'ここに補足文が入ります。',
link: { href: '#heading-sub-7', blank: false },
},
],
},
{
items: [
{
label: 'タイトル',
description:
'ここに補足文が入ります。ここに補足文が入ります。ここに補足文が入ります。',
link: { href: '#heading-sub-7', blank: false },
},
{
label: 'タイトル',
description: 'ここに補足文が入ります。',
link: { href: '#heading-sub-7', blank: false },
},
],
},
];
const menuMap = {
0: list1,
2: list3,
};
</script>
<div class="size-full">
<GlobalNavigation class="min-w-114" {menus}>
{#snippet children(_, index)}
{@const list = menuMap[index]}
<div class="grid w-full gap-x-6 gap-y-2" style={`grid-template-columns: repeat(${list.length}, minmax(0,1fr)); grid-template-rows: repeat(${list.length}, auto);`}>
{#each list as column}
<ul class="grid grid-rows-subgrid gap-3 row-span-full">
{#each column.items as item}
<li>
<a class="flex flex-col h-fit gap-1 p-3 rounded-md transition-colors hover:bg-base-container-accent/90 focus-visible:transition-none" href={item.link.href}>
<p class="font-semibold leading-normal text-sm">
{item.label}
</p>
{#if item.description}
<p class="leading-normal text-base-foreground-muted text-sm">
{item.description}
</p>
{/if}
</a>
</li>
{/each}
</ul>
{/each}
</div>
{/snippet}
</GlobalNavigation>
</div>
With Icon
メニューとアイコンを組み合わせた例です。
<script lang="ts">
import GlobalNavigation from '$lib/components/ui/modules/GlobalNavigation.svelte';
import { Circle } from '@lucide/svelte';
let menus = [
{
id: 'menu1',
label: 'メニュー',
hasSubMenu: true,
},
{
id: 'menu2',
label: 'メニュー',
link: { href: '#heading-sub-8', blank: true },
},
{
label: 'メニュー',
link: { href: '#heading-sub-8', blank: false },
},
{
id: 'menu4',
label: 'メニュー',
hasSubMenu: true,
},
{
label: 'メニュー',
link: { href: '#heading-sub-8', blank: false },
},
];
</script>
<div class="size-full">
<GlobalNavigation class="min-w-114" {menus}>
{#snippet startContent(item)}
{#if item.id === 'menu1' || item.id === 'menu2' || item.id === 'menu4'}
<Circle size="1rem" />
{/if}
{/snippet}
{#snippet children(item)}
{#if item.id === 'menu1'}
ここにメニュー1の要素が入ります。
{/if}
{#if item.id === 'menu4'}
ここにメニュー4の要素が入ります。
{/if}
{/snippet}
</GlobalNavigation>
</div>