Segmented Control

SegmentedControlは、関連する選択肢をグループ化し、1つを選択することで表示内容や状態を切り替えるコンポーネントです。

プロパティ

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

名前 デフォルト値 説明
segmentedControls SegmentedControlItem[] セグメント項目の配列を渡します。
value number | string 現在選択されているセグメントのValueを指します。
onChange (segmentedControl: SegmentedControlItem) => void 選択されたときのコールバック関数。
segmentContent Snippet<[SegmentedControlItem]> セグメントをカスタム描画するコンテンツ

SegmentedControlItem

SegmentedControlItemは、セグメント内の情報を表すオブジェクトです。

名前 デフォルト値 説明
id number | string セグメント項目のidです。
label string セグメントの項目名です。
disabled boolean false セグメントを無効化します。操作できません。
value boolean 現在選択されているセグメントのValueを指します。

インストールの手順

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

atoms/SegmentedControl.svelte
        <!--
@component
## 概要
- セグメント形式のナビゲーションUIを提供するコンポーネントです

## 機能
- segmentedControlsに指定した配列からセグメントを動的に生成します

## Props
- segmentedControls: セグメント項目の配列
- value: 現在選択されているセグメントのvalue

## Usage
```svelte
<SegmentedControl {segmentedControls} />
```
-->

<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 segmentedControlVariants = cva('relative px-3 py-2 rounded-xs font-medium leading-none text-sm overflow-hidden outline-primary transition-colors cursor-pointer focus-visible:z-10 focus-visible:outline-[0.125rem] focus-visible:outline-offset-[0.125rem]', {
    variants: {
      /** 現在のセグメントかどうか */
      selected: {
        true: ['shadow-xs text-base-foreground-default hover:text-base-foreground-accent'],
        false: ['text-base-foreground-muted hover:text-base-foreground-accent'],
      },
      /** 操作できるかどうか */
      disabled: {
        true: ['opacity-50 pointer-events-none'],
        false: [],
      },
    },
  });

  export type SegmentedControlVariants = VariantProps<typeof segmentedControlVariants>;

  export interface SegmentedControlProps extends SegmentedControlVariants {
    /** クラス */
    class?: ClassValue;
    /** セグメント項目の配列 */
    segmentedControls: SegmentedControlItem[];
    /** セグメントが選択されたときのハンドラ */
    onChange?: (segmentedControl: SegmentedControlItem) => void;
    /** 選択されているセグメントのvalue */
    value?: number | string;
    /** セグメントをカスタム描画したいときの snippet */
    segmentContent?: Snippet<[SegmentedControlItem]>;
  }

  export interface SegmentedControlItem {
    /** セグメント項目のID */
    id: number | string;
    /** セグメントの項目名 */
    label?: string;
    /** 操作できるかどうか */
    disabled?: boolean;
    /** 選択されているセグメント */
    value?: boolean;
  }
</script>

<script lang="ts">
  let { class: className, segmentedControls, onChange, value = $bindable(), segmentContent }: SegmentedControlProps = $props();

  let selectedIndex = $derived.by(() =>
    segmentedControls.findIndex((SegmentedControl) => SegmentedControl.value === true || SegmentedControl.id === value),
  );

  $effect(() => {
    // 初期状態で value: true が1つもない, value と id が一致しない場合、id:1 を true にする
    if (
      !segmentedControls.some((SegmentedControl) => SegmentedControl.value === true || SegmentedControl.id === value)
    ) {
      segmentedControls = segmentedControls.map((SegmentedControl) => ({
        ...SegmentedControl,
        value: SegmentedControl.id === 1,
      }));
    }
  });

  function onChangeSegmentedControl(segmentedControl: SegmentedControlItem) {
    if (segmentedControl.disabled) return;
    segmentedControls = segmentedControls.map((SegmentedControl) => ({
      ...SegmentedControl,
      value: SegmentedControl.id === segmentedControl.id,
    }));

    value = segmentedControl.id;
    let selected_value = segmentedControls.find((SegmentedControl) => SegmentedControl.id === segmentedControl.id);

    onChange?.(selected_value ?? segmentedControl);
  }
</script>

<div class={[className, 'flex items-center']}>
  <div class="relative flex p-1 bg-base-container-muted rounded-md" style="--segments-length: {segmentedControls.length}; --selected-segment-index: {selectedIndex};">
    <div class="absolute top-1 left-1 z-0 bg-base-container-default rounded-xs shadow-xs transition-transform duration-300 segment-highlight hover:bg-base-container-accent/90"></div>
    {#each segmentedControls as segmentedControl}
      {@const is_selected = value ? segmentedControl.id === value : segmentedControl.value === true}
      <button class={['flex flex-1 items-center justify-center gap-2', segmentedControlVariants({ selected: is_selected, disabled: segmentedControl.disabled })]} type="button" disabled={segmentedControl.disabled || is_selected} onclick={() => onChangeSegmentedControl(segmentedControl)}>
        {#if segmentContent}
          {@render segmentContent(segmentedControl)}
        {:else}
          {segmentedControl.label}
        {/if}
      </button>
    {/each}
  </div>
</div>

<style>
  .segment-highlight {
    width: calc((100% - 0.5rem) / var(--segments-length));
    height: calc(100% - 0.5rem);
    transform: translateX(calc(100% * var(--selected-segment-index)));
  }
</style>

      

使い方


サンプル

Default

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

Value

Valueを指定することで、任意のセグメントを選択状態にすることも可能です。

OnChange

コールバック関数で値を受け取ることができます。

Snippet

Snippetも渡すことができます。

Disabled

利用不可の状態です。