Toast
Toastは、ユーザーに一時的なメッセージを通知するコンポーネントです。
アクション部分はsnippet action()で差し替えられ、6方向の表示位置に対応しています。
位置に応じてスライド方向が変化するアニメーションで表示されます。
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Toast from '$lib/components/ui/items/Toast.svelte';
import { untrack } from 'svelte';
let toasts: { show: boolean; label: string; description: string; key: Symbol }[] = $state([]);
async function addToast({ label, description }) {
const toast = { show: true, label, description, key: Symbol() };
untrack(() => {
toasts.push(toast);
});
}
$effect(() => {
const temp = toasts.filter((t) => t.show);
if (temp.length !== toasts.length) {
toasts = temp;
}
});
</script>
<Button onclick={() => addToast({ label: 'ラベル', description: 'ここに補足文が入ります' })}>Show toast</Button>
{#each toasts as toast, i (toast.key)}
<div class="fixed top-8 right-4 z-50 origin-top transition-all {toasts.length - i > 3 ? 'hidden' : ''}" style="transform: scale({Math.max(0.3, 1 - (toasts.length - i) / 10)}) translateY({-((toasts.length - i) * 10)}px);">
<Toast bind:open={toast.show} duration={3000} transitionParams={{ duration: 350, x: 20 }} hideCloseButton>
<div class="text-left">
<div class="text-base-foreground-default text-lg font-semibold mb-2">{toast.label}</div>
<div class="text-base-foreground-muted">{toast.description}</div>
</div>
</Toast>
</div>
{/each}
プロパティ
Toastは、以下のプロパティをサポートしています。
| 名前 | 型 | デフォルト値 | 説明 |
|---|---|---|---|
position |
string |
top-center |
表示位置を指定できます。top-right, top-left, bottom-right, bottom-left, top-center, bottom-center のいずれかを選択できます。 |
duration |
number |
3000 |
自動で閉じるまでの時間をミリ秒で指定します。 |
keepOpen |
boolean |
false |
trueを指定することでトーストを自動で閉じなくします。 |
hideCloseButton |
boolean |
false |
trueを指定すると閉じるボタンを非表示にします。 |
transitionParams |
FlyParams |
{duration: 200, x: 0, y: 0} |
アニメーションの時間・水平方向移動距離・垂直方向移動距離を指定できます。 |
closeButtonClass |
string |
閉じるアイコンのクラスを指定できます。 | |
open |
boolean |
true |
トーストの開閉状態を制御します。 |
onClose |
() => void |
トーストが閉じられたときに呼ばれるハンドラです。 |
インストールの手順
以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。
items/Toast.svelte
<!--
@component
## 概要
- 一時的なメッセージを表示するためのコンポーネントです
## 機能
- 表示位置を6方向から選択できます
- 指定した時間で自動的に閉じる、もしくは手動で閉じられます
- スロットでアクションボタンを差し込めます
## Props
- position: トーストの表示位置を指定します
- duration: 自動で閉じるまでの時間を指定します
- keepOpen: trueを指定することで自動で閉じなくできます
- hideCloseButton: trueを指定することで閉じるボタンを非表示にすることができます
- transitionParams: アニメーションの時間・水平方向の移動量・垂直方向の移動量を指定できます
- closeButtonClass: 閉じるボタンのクラスを指定します
## Usage
```svelte
<Toast duration={3000}>保存しました</Toast>
```
-->
<script module lang="ts">
import type { Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements';
import type { FlyParams } from 'svelte/transition';
import { cva, type VariantProps } from 'class-variance-authority';
export const toastVariants = cva('flex items-center gap-2 p-6 bg-base-container-default border border-base-stroke-default rounded-md shadow-lg', {
variants: {
position: {
'top-right': 'fixed top-4 right-4 z-50',
'top-left': 'fixed top-4 left-4 z-50',
'bottom-right': 'fixed right-4 bottom-4 z-50',
'bottom-left': 'fixed bottom-4 left-4 z-50',
'top-center': 'fixed inset-x-4 top-4 z-50 mx-auto',
'bottom-center': 'fixed inset-x-4 bottom-4 z-50 mx-auto',
},
},
});
export type ToastVariants = VariantProps<typeof toastVariants>;
export interface ToastProps extends ToastVariants {
/** クラス */
class?: ClassValue;
/** 自動で閉じるまでの時間(ミリ秒) */
duration?: number;
/** trueを指定で自動で閉じなくする */
keepOpen?: boolean;
/** trueを指定で閉じるボタンを非表示 */
hideCloseButton?: boolean;
/** 開閉状態 */
open?: boolean;
/** アニメーションのprops */
transitionParams?: FlyParams;
/** 閉じるボタンのクラス */
closeButtonClass?: ClassValue;
/** 本文 */
children?: Snippet<[]>;
/** アクションボタン用スロット */
action?: Snippet<[]>;
/** 閉じたときのハンドラ */
onClose?: () => void;
}
</script>
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import { X } from '@lucide/svelte';
import { fly } from 'svelte/transition';
let { class: className, position, duration = 3000, keepOpen = false, hideCloseButton = false, open = $bindable(false), transitionParams = { duration: 200, x: 0, y: 0 }, closeButtonClass, children, action, onClose }: ToastProps = $props();
let timerId: ReturnType<typeof setTimeout> | null = null;
let toastVariantClass = $derived(toastVariants({ position, class: className }));
$effect(() => {
if (open && !keepOpen) {
timerId = setTimeout(close, duration);
}
return () => {
if (timerId) clearTimeout(timerId);
};
});
function close() {
if (timerId) clearTimeout(timerId);
open = false;
onClose?.();
}
</script>
<div transition:fly={transitionParams} class={toastVariantClass}>
<div class="flex-1 min-w-0">
{@render children?.()}
</div>
{#if action || !hideCloseButton}
<div class="flex items-center shrink-0 gap-2">
{@render action?.()}
{#if !hideCloseButton}
<Button class={closeButtonClass} size="small" variant="secondary" isSquare tone="ghost" onclick={close} aria-label="close">
<X size="1rem" />
</Button>
{/if}
</div>
{/if}
</div>
依存コンポーネント
Toastを使うときは、以下のコンポーネントもダウンロードが必要です。
使い方
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Toast from '$lib/components/ui/items/Toast.svelte';
import { untrack } from 'svelte';
let toasts: { show: boolean; label: string; description: string; key: Symbol }[] = $state([]);
async function addToast({ label, description }) {
const toast = { show: true, label, description, key: Symbol() };
untrack(() => {
toasts.push(toast);
});
}
$effect(() => {
const temp = toasts.filter((t) => t.show);
if (temp.length !== toasts.length) {
toasts = temp;
}
});
</script>
<Button onclick={() => addToast({ label: 'ラベル', description: 'ここに補足文が入ります' })}>Show toast</Button>
{#each toasts as toast, i (toast.key)}
<div class="fixed top-8 right-4 z-50 origin-top transition-all {toasts.length - i > 3 ? 'hidden' : ''}" style="transform: scale({Math.max(0.3, 1 - (toasts.length - i) / 10)}) translateY({-((toasts.length - i) * 10)}px);">
<Toast bind:open={toast.show} duration={3000} transitionParams={{ duration: 350, x: 20 }} hideCloseButton>
<div class="text-left">
<div class="text-base-foreground-default text-lg font-semibold mb-2">{toast.label}</div>
<div class="text-base-foreground-muted">{toast.description}</div>
</div>
</Toast>
</div>
{/each}
サンプル
Auto Close
durationに指定した時間が経過後に自動でToastを閉じます。
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Toast from '$lib/components/ui/items/Toast.svelte';
import { untrack } from 'svelte';
let toasts: { show: boolean; label: string; description: string; key: Symbol }[] = $state([]);
async function addToast({ label, description }) {
const toast = { show: true, label, description, key: Symbol() };
untrack(() => {
toasts.push(toast);
});
}
$effect(() => {
const temp = toasts.filter((t) => t.show);
if (temp.length !== toasts.length) {
toasts = temp;
}
});
</script>
<Button onclick={() => addToast({ label: 'ラベル', description: 'ここに補足文が入ります' })}>Show toast</Button>
{#each toasts as toast, i (toast.key)}
<div class="fixed top-8 right-4 z-50 origin-top transition-all {toasts.length - i > 3 ? 'hidden' : ''}" style="transform: scale({Math.max(0.3, 1 - (toasts.length - i) / 10)}) translateY({-((toasts.length - i) * 10)}px);">
<Toast bind:open={toast.show} duration={3000} transitionParams={{ duration: 350, x: 20 }} hideCloseButton>
<div class="text-left">
<div class="text-base-foreground-default text-lg font-semibold mb-2">{toast.label}</div>
<div class="text-base-foreground-muted">{toast.description}</div>
</div>
</Toast>
</div>
{/each}
Auto Close with Action
自動で閉じるほか、 閉じるボタンによって閉じることができます。
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Toast from '$lib/components/ui/items/Toast.svelte';
let open = $state(false);
</script>
<Button onclick={() => (open = true)}>Show toast</Button>
{#if open}
<Toast bind:open duration={3000} transitionParams={{ duration: 200, x: 20 }} position="top-right">
<div class="flex align-center gap-4">
<div class="text-left">
<div class="text-base-foreground-default text-lg font-semibold mb-2">ラベル</div>
<div class="text-base-foreground-muted">ここに補足文が入ります</div>
</div>
<Button class="self-center" size="small" variant="primary" tone="ghost">ボタン</Button>
</div>
</Toast>
{/if}
Persistent
keepOpen を true にすると、操作されるまで表示され続けます。
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Toast from '$lib/components/ui/items/Toast.svelte';
let open = $state(false);
</script>
<Button onclick={() => (open = true)}>Show toast</Button>
{#if open}
<Toast class="md:max-w-92" bind:open position="bottom-center" keepOpen>
<div class="flex flex-none justify-between align-center">
<div class="text-left">
<div class="text-base-foreground-default text-lg font-semibold mb-2">ラベル</div>
<div class="text-base-foreground-muted">ここに補足文が入ります</div>
</div>
<Button class="flex-none self-center" size="small" variant="primary" tone="ghost" onclick={() => (open = false)}>ボタン</Button>
</div>
</Toast>
{/if}
Positions
6方向の表示位置に対応しています。
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Toast from '$lib/components/ui/items/Toast.svelte';
let topRight = $state(false);
let bottomRight = $state(false);
let topLeft = $state(false);
let bottomLeft = $state(false);
let topCenter = $state(false);
let bottomCenter = $state(false);
</script>
<div class="flex flex-wrap gap-2">
<Button onclick={() => (topRight = true)}>top-right</Button>
<Button onclick={() => (bottomRight = true)}>bottom-right</Button>
<Button onclick={() => (topLeft = true)}>top-left</Button>
<Button onclick={() => (bottomLeft = true)}>bottom-left</Button>
<Button onclick={() => (topCenter = true)}>top-center</Button>
<Button onclick={() => (bottomCenter = true)}>bottom-center</Button>
</div>
{#if topRight}
<Toast bind:open={topRight} position="top-right" duration={2000} transitionParams={{ duration: 200, x: 20 }} hideCloseButton>
top-right
</Toast>
{/if}
{#if bottomRight}
<Toast bind:open={bottomRight} position="bottom-right" duration={2000} transitionParams={{ duration: 200, x: 20 }} hideCloseButton>
bottom-right
</Toast>
{/if}
{#if topLeft}
<Toast bind:open={topLeft} position="top-left" duration={2000} transitionParams={{ duration: 200, x: -20 }} hideCloseButton>
top-left
</Toast>
{/if}
{#if bottomLeft}
<Toast bind:open={bottomLeft} position="bottom-left" duration={2000} transitionParams={{ duration: 200, x: -20 }} hideCloseButton>
bottom-left
</Toast>
{/if}
{#if topCenter}
<Toast class="w-fit" bind:open={topCenter} position="top-center" duration={2000} transitionParams={{ duration: 200, y: -20 }} hideCloseButton>
top-center
</Toast>
{/if}
{#if bottomCenter}
<Toast class="w-fit" bind:open={bottomCenter} position="bottom-center" duration={2000} transitionParams={{ duration: 200, y: 20 }} hideCloseButton>
bottom-center
</Toast>
{/if}