Segmented Control
SegmentedControlは、関連する選択肢をグループ化し、1つを選択することで表示内容や状態を切り替えるコンポーネントです。
<script lang="ts">
import SegmentedControl from '$lib/components/ui/atoms/SegmentedControl.svelte';
let segmentedControls = [
{ id: 1, label: 'タブテキスト' },
{ id: 2, label: 'タブテキスト' },
{ id: 3, label: 'タブテキスト' },
];
</script>
<SegmentedControl {segmentedControls} />
プロパティ
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>
使い方
<script lang="ts">
import SegmentedControl from '$lib/components/ui/atoms/SegmentedControl.svelte';
let segmentedControls = [
{ id: 1, label: 'タブテキスト' },
{ id: 2, label: 'タブテキスト' },
{ id: 3, label: 'タブテキスト' },
];
</script>
<SegmentedControl {segmentedControls} />
サンプル
Default
特に操作が行われていない、デフォルトの状態です。
<script lang="ts">
import SegmentedControl from '$lib/components/ui/atoms/SegmentedControl.svelte';
let segmentedControls = [
{ id: 1, label: 'タブテキスト' },
{ id: 2, label: 'タブテキスト' },
{ id: 3, label: 'タブテキスト' },
];
</script>
<SegmentedControl {segmentedControls} />
Value
Valueを指定することで、任意のセグメントを選択状態にすることも可能です。
<script lang="ts">
import SegmentedControl from '$lib/components/ui/atoms/SegmentedControl.svelte';
let item1 = [
{ id: 1, label: 'タブテキスト' },
{ id: 2, label: 'タブテキスト', value: true },
{ id: 3, label: 'タブテキスト' },
];
let item2 = [
{ id: 1, label: 'タブテキスト' },
{ id: 2, label: 'タブテキスト' },
{ id: 3, label: 'タブテキスト' },
];
let value = $state(item2[1].id);
</script>
<div class="flex flex-col gap-4">
<SegmentedControl segmentedControls={item1} />
<SegmentedControl segmentedControls={item2} bind:value />
</div>
OnChange
コールバック関数で値を受け取ることができます。
<script lang="ts">
import DebugConsole from '$lib/components/ui/atoms/DebugConsole.svelte';
import SegmentedControl from '$lib/components/ui/atoms/SegmentedControl.svelte';
let segmentedControls = [
{ id: 1, label: 'タブテキスト' },
{ id: 2, label: 'タブテキスト' },
{ id: 3, label: 'タブテキスト' },
];
let selectedValue = $state(segmentedControls[0]);
function onChange(value) {
selectedValue = value;
}
</script>
<div class="flex flex-col gap-4">
<SegmentedControl {segmentedControls} {onChange} />
<DebugConsole data={{ selectedValue }} />
</div>
Snippet
Snippetも渡すことができます。
<script lang="ts">
import SegmentedControl from '$lib/components/ui/atoms/SegmentedControl.svelte';
import { Expand, Monitor, Smartphone } from '@lucide/svelte';
let item = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
];
</script>
<div class="flex flex-col gap-4">
<SegmentedControl segmentedControls={item}>
{#snippet segmentContent(item)}
{#if item.id === 1}
<Monitor class="shrink-0" size="1rem" />
タブテキスト
{/if}
{#if item.id === 2}
<Smartphone class="shrink-0" size="1rem" />
タブテキスト
{/if}
{#if item.id === 3}
<Expand class="shrink-0" size="1rem" />
タブテキスト
{/if}
{/snippet}
</SegmentedControl>
</div>
Disabled
利用不可の状態です。
<script lang="ts">
import SegmentedControl from '$lib/components/ui/atoms/SegmentedControl.svelte';
let segmentedControls = [
{ id: 1, label: 'タブテキスト', disabled: false },
{ id: 2, label: 'タブテキスト', disabled: true },
{ id: 3, label: 'タブテキスト', disabled: false },
];
</script>
<SegmentedControl {segmentedControls} />