Menu
Menuは、複数の選択肢や操作を一覧で表示し、ユーザーが項目を選択できるコンポーネントです。
主にアプリケーションのナビゲーションや、操作メニューとして利用されます。
項目数や押下時の処理を柔軟に設定できます。
<script lang="ts">
import Menu from '$lib/components/ui/atoms/Menu.svelte';
let current = 'menu1';
const menus = [
{
id: 'menu1',
label: 'メニュー1',
options: [
{ id: 'menu1-1', label: 'メニュー' },
{ id: 'menu1-2', label: 'メニュー' },
{ id: 'menu1-3', label: 'メニュー' },
],
},
{
id: 'menu2',
label: 'メニュー2',
},
{
id: 'menu3',
label: 'メニュー3',
options: [
{ id: 'menu3-1', label: 'メニュー' },
{ id: 'menu3-2', label: 'メニュー' },
{ id: 'menu3-3', label: 'メニュー' },
],
},
];
</script>
<div class="h-full">
{#each menus as menu}
<Menu class="w-52" item={menu} {current} />
{/each}
</div>
プロパティ
Menuは、以下のプロパティをサポートしています。
| 名前 | 型 | デフォルト値 | 説明 |
|---|---|---|---|
item |
MenuItem |
メニューの設定(項目やラベルなど)を渡します。 | |
current |
string |
現在選択されているメニューの値を指定します。 |
MenuItem
| プロパティ名 | 型 | 説明 |
|---|---|---|
id |
string |
メニュー項目の一意なID |
label |
string |
メニューのラベル |
options |
MenuOptionProps[] |
サブメニューの設定(項目やラベルなど)を渡します |
disabled |
boolean |
押下できるかどうか |
onClick |
(item: MenuItem) => void |
押下時に発火するコールバック関数 |
MenuOptionProps
| プロパティ名 | 型 | 説明 |
|---|---|---|
id |
string |
メニュー項目の一意なID |
label |
string |
メニューのラベル |
disabled |
boolean |
押下できるかどうか |
onClick |
(item: MenuOptionProps) => void |
押下時に発火するコールバック関数 |
インストールの手順
以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。
atoms/Menu.svelte
<!--
@component
## 概要
- 複数の選択肢や操作を一覧で表示し、ユーザーが項目を選択できるメニューコンポーネントです
- サブメニューや無効化、押下時の処理など柔軟な設定が可能です
## 機能
- メニュー項目の表示・選択
- サブメニュー(階層化)対応
- メニュー選択時の処理発火
- 現在選択中の項目のハイライト
## Props
- class: 追加のクラスを指定できます
- item: メニューの設定(項目やラベルなど)を渡します
- current: 現在選択されているメニューの値
## Usage
```svelte
<Menu item={item} current={current} />
```
-->
<script module lang="ts">
import type { ClassValue } from 'svelte/elements';
import { cva } from 'class-variance-authority';
export let menuOptionVariants = cva('relative w-full py-3 pr-9 pl-3 rounded-sm text-left text-sm list-none outline-primary transition hover:bg-base-container-accent/90 focus-visible:outline-[0.125rem] focus-visible:outline-offset-[0.125rem] focus-visible:outline-primary', {
variants: {
isCurrent: {
true: ['text-primary'],
false: ['text-base-foreground-default'],
},
disabled: {
true: ['opacity-50 pointer-events-none focus-visible:outline-none'],
false: ['cursor-pointer'],
},
},
defaultVariants: {
isCurrent: false,
disabled: false,
},
});
export let childrenOptionVariants = cva('py-3 pr-2 pl-8 rounded-sm text-left text-sm list-none outline-primary transition hover:bg-base-container-accent/90 focus-visible:outline-[0.125rem] focus-visible:outline-offset-[0.125rem] focus-visible:outline-primary', {
variants: {
disabled: {
true: ['opacity-50 pointer-events-none'],
false: ['cursor-pointer'],
},
},
defaultVariants: {
disabled: false,
},
});
export interface MenuProps {
/** 追加のクラス */
class?: ClassValue;
/** メニューの設定 */
item: MenuItem;
/** 現在選択されているメニューの値 */
current?: string;
}
export interface MenuItem {
id: string;
/** メニューのラベル */
label: string;
/** メニューとして表示する値 */
options?: MenuOptionProps[];
/** 押下できるかどうか */
disabled?: boolean;
/** 押下時に発火させるコールバック関数 */
onClick?: (item: MenuItem) => void;
}
export interface MenuOptionProps {
id: string;
/** メニュー内オプションのラベル */
label: string;
/** 押下できるかどうか */
disabled?: boolean;
/** 押下時に発火させるコールバック関数 */
onClick?: (item: MenuOptionProps) => void;
}
</script>
<script lang="ts">
import { ChevronDown } from '@lucide/svelte';
import { slide } from 'svelte/transition';
let { class: className, item, current = '' }: MenuProps = $props();
let isCurrent = $derived(current === item.id);
let isOpen = $state(false);
let menuOptionVariantClass = $derived(menuOptionVariants({ isCurrent, disabled: item.disabled }));
/** メニュー押下時の処理 */
function onClickMenu() {
item.onClick?.(item);
}
/** サブメニュー押下時の処理 */
function onClickOption(callback, option: MenuOptionProps) {
callback?.(option);
}
/** 開閉フラグの切り替え処理 */
function toggleOpen() {
isOpen = !isOpen;
}
</script>
<div class={[className]}>
<button class={menuOptionVariantClass} type="button" onclick={!item.options && item.onClick ? onClickMenu : toggleOpen} disabled={item.disabled}>
{#if current === item.id}
<div class="absolute top-1/2 left-0 w-0.5 h-1/2 bg-primary rounded-r-[1px] -translate-y-1/2"></div>
{/if}
<span>{item.label}</span>
{#if item.options}
<ChevronDown class={['absolute inset-y-0 right-3 transition duration-250 my-auto', { 'rotate-180': isOpen }]} size="1rem" />
{/if}
</button>
{#if item.options && isOpen}
<div class="flex flex-col w-full p-1 overflow-hidden translate-3d" transition:slide={{ duration: 250 }}>
{#each item.options || [] as option}
<button class={childrenOptionVariants({ disabled: option.disabled })} type="button" onclick={() => onClickOption(option.onClick, option)} disabled={option.disabled}>
{option.label}
</button>
{/each}
</div>
{/if}
</div>
使い方
<script lang="ts">
import Menu from '$lib/components/ui/atoms/Menu.svelte';
let current = 'menu1';
const menus = [
{
id: 'menu1',
label: 'メニュー1',
options: [
{ id: 'menu1-1', label: 'メニュー' },
{ id: 'menu1-2', label: 'メニュー' },
{ id: 'menu1-3', label: 'メニュー' },
],
},
{
id: 'menu2',
label: 'メニュー2',
},
{
id: 'menu3',
label: 'メニュー3',
options: [
{ id: 'menu3-1', label: 'メニュー' },
{ id: 'menu3-2', label: 'メニュー' },
{ id: 'menu3-3', label: 'メニュー' },
],
},
];
</script>
<div class="h-full">
{#each menus as menu}
<Menu class="w-52" item={menu} {current} />
{/each}
</div>
サンプル
Default
特に操作が行われていない、デフォルトの状態です。
<script lang="ts">
import Menu from '$lib/components/ui/atoms/Menu.svelte';
let current = 'menu1';
const menus = [
{
id: 'menu1',
label: 'メニュー1',
options: [
{ id: 'menu1-1', label: 'メニュー' },
{ id: 'menu1-2', label: 'メニュー' },
{ id: 'menu1-3', label: 'メニュー' },
],
},
{
id: 'menu2',
label: 'メニュー2',
},
{
id: 'menu3',
label: 'メニュー3',
options: [
{ id: 'menu3-1', label: 'メニュー' },
{ id: 'menu3-2', label: 'メニュー' },
{ id: 'menu3-3', label: 'メニュー' },
],
},
];
</script>
<div class="h-full">
{#each menus as menu}
<Menu class="w-52" item={menu} {current} />
{/each}
</div>
Disabled
メニュー項目が無効化されている状態です。
<script lang="ts">
import Menu from '$lib/components/ui/atoms/Menu.svelte';
const menus = [
{
id: 'menu1',
label: 'メニュー1',
disabled: true,
},
{
id: 'menu2',
label: 'メニュー2',
options: [
{ id: 'menu2-1', label: 'メニュー' },
{ id: 'menu2-2', label: 'メニュー', disabled: true },
{ id: 'menu2-3', label: 'メニュー' },
],
},
];
</script>
<div class="h-full">
{#each menus as menu}
<Menu class="w-52" item={menu} />
{/each}
</div>
onClick
メニュー項目がクリックされたときに、コールバック関数で値を受け取ることができます。
<script lang="ts">
import DebugConsole from '$lib/components/ui/atoms/DebugConsole.svelte';
import Menu from '$lib/components/ui/atoms/Menu.svelte';
let selectedId = '';
const menus = [
{
id: 'menu1',
label: 'メニュー1',
onClick: (item) => onClickMenu(item),
},
{
id: 'menu2',
label: 'メニュー2',
options: [
{ id: 'menu2-1', label: '選択肢1', onClick: (item) => onClickMenu(item) },
{ id: 'menu2-2', label: '選択肢2', onClick: (item) => onClickMenu(item) },
{ id: 'menu2-3', label: '選択肢3', onClick: (item) => onClickMenu(item) },
],
},
];
/** 押下時に値をセット */
function onClickMenu(item) {
selectedId = item.id;
}
</script>
<div class="flex flex-col size-full">
<div class="flex flex-col items-center size-full">
{#each menus as menu}
<Menu class="w-52" item={menu} />
{/each}
</div>
<DebugConsole class="mt-4" data={selectedId} />
</div>