Menu

Menuは、複数の選択肢や操作を一覧で表示し、ユーザーが項目を選択できるコンポーネントです。
主にアプリケーションのナビゲーションや、操作メニューとして利用されます。
項目数や押下時の処理を柔軟に設定できます。

プロパティ

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>

      

使い方


サンプル

Default

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

Disabled

メニュー項目が無効化されている状態です。

onClick

メニュー項目がクリックされたときに、コールバック関数で値を受け取ることができます。