VNL Works
VNL Works
VNL Works
← Components
Download ZIPRegistry JSON
Package: tailwind-motion-kit
ui

Toast

成功/失敗/情報の通知。状態遷移に強い。

toastnotificationcss+js
Customize

トークン: --mk-toast-width / --mk-toast-gap

A11y

role="status" を使用(実装側)。

Install

“壊れにくさ”優先のため、依存は増やさずに ファイルをコピーして使う 形式です。

Files

まずは下記ファイルをプロジェクトに配置してください。

required-files.md
- tailwind-motion-kit/css/00_tokens.css (css)
- tailwind-motion-kit/css/13_toast.css (css)
- tailwind-motion-kit/js/toast.js (js)
Option A

まずは Bundle を読み込んで、動作を確認するのがおすすめです。

globals.css
/* Bundle: まずはこれが一番簡単 */
@import "./tailwind-motion-kit/css/99_bundle.css";
Option B

必要な CSS だけ読み込みたい場合はこちら。

globals.css
/* 1) CSS: グローバルCSSに追記 */
@import "./tailwind-motion-kit/css/00_tokens.css";
@import "./tailwind-motion-kit/css/13_toast.css";
JavaScript

動きやUI制御が必要な場合のみ、初期化を追加します。

init.js
import { createToastManager } from "./tailwind-motion-kit/js/toast.js";
const toast = createToastManager({ position: "top-right" });
toast.show({ title: "Saved", message: "変更を保存しました", tone: "success" });

Snippets

Copy & paste
usage.js
import { createToastManager } from "./tailwind-motion-kit/js/toast.js";
const toast = createToastManager({ position: "top-right" });
toast.show({ title: "Saved", message: "変更を保存しました", tone: "success" });

Files

この部品が参照する実ファイルです。

tailwind-motion-kit/css/00_tokens.css
css
tailwind-motion-kit/css/00_tokens.css
/*!
 * Tailwind Motion Kit (mk) - Tokens
 * --------------------------------
 * 目的: 速度・距離・イージング等の“モーション言語”を統一し、
 *       パーツを増やしても品位が崩れない状態を作ります。
 *
 * 使い方:
 *  - :root の変数を上書きするだけで全体の動きを一括調整できます。
 *  - 個別要素は style="" か data-mk-* 属性で上書きできます(JSがある場合)。
 */

:root {
  /* --- イージング(加減速) --- */
  --mk-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --mk-ease-in: cubic-bezier(0.32, 0, 0.67, 0);
  --mk-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  /* “少しだけ遊び”が欲しい時用(多用は上品さを損ねます) */
  --mk-ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);

  /* --- 時間 --- */
  --mk-duration-fast: 160ms;
  --mk-duration-medium: 280ms;
  --mk-duration-slow: 600ms;
  --mk-duration-slower: 900ms;

  /* コンポーネントが参照する“共通デフォルト” */
  --mk-duration: var(--mk-duration-medium);
  --mk-ease: var(--mk-ease-out);
  --mk-delay: 0ms;

  /* --- 移動量 --- */
  --mk-distance-1: 6px;
  --mk-distance-2: 12px;
  --mk-distance-3: 20px;
  --mk-distance: var(--mk-distance-2);

  /* --- ぼかし --- */
  --mk-blur-0: 0px;
  --mk-blur-1: 6px;
  --mk-blur-2: 10px;
  --mk-blur: var(--mk-blur-1);

  /* --- 影(浮遊感) --- */
  --mk-shadow-lift: 0 12px 30px rgba(0, 0, 0, 0.14);
  --mk-shadow-soft: 0 10px 22px rgba(0, 0, 0, 0.10);

  /* --- アンダーライン --- */
  --mk-underline-thickness: 2px;
  --mk-underline-offset: 0.2em;

  /* --- フォーカスリング --- */
  --mk-ring-color: rgba(59, 130, 246, 0.35);
  --mk-ring-size: 3px;

  /* --- スケルトン(ロード中) --- */
  --mk-skeleton-a: rgba(0, 0, 0, 0.06);
  --mk-skeleton-b: rgba(0, 0, 0, 0.12);
  --mk-skeleton-speed: 1250ms;

  /* --- スピナー --- */
  --mk-spinner-size: 1.2em;
  --mk-spinner-border: 2px;

  /* --- テキスト分割表示 --- */
  --mk-char-step: 25ms; /* 1文字ごとの遅延 */


  /* --- ダイアログ / ドロワー --- */
  --mk-dialog-duration: var(--mk-duration-slow);
  --mk-dialog-ease: var(--mk-ease-out);
  --mk-dialog-backdrop-opacity: 0.45;
  --mk-dialog-offset: var(--mk-distance-3);
  --mk-dialog-scale-from: 0.995;
  --mk-dialog-radius: 18px;

  /* --- 折りたたみ(アコーディオン) --- */
  --mk-collapse-duration: var(--mk-duration-medium);
  --mk-collapse-ease: var(--mk-ease);

  /* --- Toast / 通知 --- */
  --mk-toast-gap: 10px;
  --mk-toast-width: min(360px, calc(100vw - 24px));
  --mk-toast-duration: var(--mk-duration-medium);
  --mk-toast-ease: var(--mk-ease-out);

  /* --- スクロール進捗(トップバー) --- */
  --mk-scroll-progress-top: 0px;
  --mk-scroll-progress-height: 3px;
  --mk-scroll-progress-ease: linear;

  /* --- ナビ下線インジケータ --- */
  --mk-nav-underline-height: 2px;
  --mk-nav-underline-radius: 999px;
  --mk-nav-duration: var(--mk-duration-medium);
  --mk-nav-ease: var(--mk-ease-out);


  /* --- UI state(idle/loading/success/error) --- */
  --mk-state-hold-success: 900ms;
  --mk-state-hold-error: 1400ms;


  /* --- Tabs --- */
  --mk-tabs-gap: 10px;
  --mk-tabs-radius: 999px;
  --mk-tabs-duration: var(--mk-duration-medium);
  --mk-tabs-ease: var(--mk-ease-out);
  --mk-tabs-underline-height: 2px;
  --mk-tabs-underline-color: currentColor;

  /* --- Tooltip --- */
  --mk-tooltip-bg: rgba(0, 0, 0, 0.82);
  --mk-tooltip-fg: #fff;
  --mk-tooltip-radius: 10px;
  --mk-tooltip-padding: 10px 12px;
  --mk-tooltip-gap: 8px;
  --mk-tooltip-shadow: 0 18px 60px rgba(0,0,0,0.35);
  --mk-tooltip-max-width: 260px;

  /* --- Stepper --- */
  --mk-stepper-gap: 14px;
  --mk-step-size: 28px;
  --mk-step-line: rgba(255, 255, 255, 0.14);
  --mk-step-done: rgba(59, 130, 246, 1);
  --mk-step-current: rgba(255, 255, 255, 0.92);
  --mk-step-todo: rgba(255, 255, 255, 0.5);

}

/* motion減衰設定のあるユーザーには、動きを最小化して“結果”を出します */
@media (prefers-reduced-motion: reduce) {
  :root {
    --mk-duration-fast: 0ms;
    --mk-duration-medium: 0ms;
    --mk-duration-slow: 0ms;
    --mk-duration-slower: 0ms;
    --mk-duration: 0ms;
    --mk-delay: 0ms;
  }
}
tailwind-motion-kit/css/13_toast.css
css
tailwind-motion-kit/css/13_toast.css
/*!
 * Tailwind Motion Kit (mk) - Toast
 * -------------------------------
 * 目的: “処理中 / 完了 / 失敗” を静かに伝える通知。
 * Nyano の mint / swap / redeem など状態遷移のある画面と相性が良いです。
 *
 * JS:
 *   import { createToastManager } from "./js/toast.js";
 *   const toast = createToastManager();
 *   toast.show({ title: "Saved", message: "..." });
 *
 * マークアップ(自動生成が基本):
 * <div class="mk-toast-viewport" data-mk-toast-viewport data-mk-position="top-right"></div>
 */

.mk-toast-viewport {
  position: fixed;
  z-index: 70;

  display: flex;
  flex-direction: column;
  gap: var(--mk-toast-gap);

  width: var(--mk-toast-width);
  pointer-events: none;
}

/* placement */
.mk-toast-viewport[data-mk-position="top-right"]    { top: 14px; right: 14px; align-items: flex-end; }
.mk-toast-viewport[data-mk-position="top-left"]     { top: 14px; left: 14px; align-items: flex-start; }
.mk-toast-viewport[data-mk-position="bottom-right"] { bottom: 14px; right: 14px; align-items: flex-end; }
.mk-toast-viewport[data-mk-position="bottom-left"]  { bottom: 14px; left: 14px; align-items: flex-start; }
.mk-toast-viewport[data-mk-position="top-center"]   { top: 14px; left: 50%; transform: translateX(-50%); align-items: center; }
.mk-toast-viewport[data-mk-position="bottom-center"]{ bottom: 14px; left: 50%; transform: translateX(-50%); align-items: center; }

.mk-toast {
  pointer-events: auto;

  background: var(--mk-toast-bg, rgba(17, 17, 17, 0.92));
  color: var(--mk-toast-fg, #fff);
  border: 1px solid var(--mk-toast-border, rgba(255, 255, 255, 0.14));
  border-radius: 14px;
  box-shadow: var(--mk-shadow-soft);

  padding: 12px 14px;
  width: 100%;
}

.mk-toast-title {
  font-weight: 600;
  font-size: 0.95rem;
  line-height: 1.2;
  margin: 0 0 4px;
}

.mk-toast-message {
  font-size: 0.9rem;
  line-height: 1.35;
  margin: 0;
  opacity: 0.92;
}

.mk-toast-close {
  appearance: none;
  border: 0;
  background: transparent;
  color: inherit;
  cursor: pointer;
  opacity: 0.8;
}
.mk-toast-close:hover { opacity: 1; }

/* motion */
.mk-toast {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  transition:
    opacity var(--mk-toast-duration) var(--mk-toast-ease),
    transform var(--mk-toast-duration) var(--mk-toast-ease);
}

.mk-toast[data-mk-state="open"] {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

.mk-toast[data-mk-state="closing"] {
  opacity: 0;
  transform: translate3d(0, -6px, 0);
}

/* tone(好みに応じて上書き可能) */
.mk-toast[data-mk-tone="success"] { --mk-toast-border: rgba(34, 197, 94, 0.45); }
.mk-toast[data-mk-tone="error"]   { --mk-toast-border: rgba(239, 68, 68, 0.45); }
.mk-toast[data-mk-tone="info"]    { --mk-toast-border: rgba(59, 130, 246, 0.45); }

/* reduce motion */
@media (prefers-reduced-motion: reduce) {
  .mk-toast {
    transition: none !important;
    transform: none !important;
  }
}
tailwind-motion-kit/js/toast.js
js
tailwind-motion-kit/js/toast.js
// Tailwind Motion Kit (mk) - Toast
// --------------------------------
// 依存なしの軽量トースト。
// createToastManager().show({ title, message, tone, timeout }) で利用します。

function prefersReducedMotion() {
  return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}

function parseTimeMs(v) {
  const s = String(v || "").trim();
  if (!s) return 0;
  if (s.endsWith("ms")) return Number(s.replace("ms", "")) || 0;
  if (s.endsWith("s")) return (Number(s.replace("s", "")) * 1000) || 0;
  const n = Number(s);
  return Number.isFinite(n) ? n : 0;
}

function maxTransitionMs(el) {
  if (!el) return 0;
  const cs = getComputedStyle(el);
  const ds = cs.transitionDuration.split(",").map((x) => parseTimeMs(x));
  const ls = cs.transitionDelay.split(",").map((x) => parseTimeMs(x));
  const n = Math.max(ds.length, ls.length, 1);
  let max = 0;
  for (let i = 0; i < n; i++) {
    const d = ds[i] ?? ds[ds.length - 1] ?? 0;
    const l = ls[i] ?? ls[ls.length - 1] ?? 0;
    max = Math.max(max, d + l);
  }
  return max;
}

function ensureViewport({
  selector = "[data-mk-toast-viewport]",
  position = "top-right",
  className = "mk-toast-viewport",
} = {}) {
  let viewport = document.querySelector(selector);
  if (viewport) return viewport;

  viewport = document.createElement("div");
  viewport.className = className;
  viewport.setAttribute("data-mk-toast-viewport", "");
  viewport.setAttribute("data-mk-position", position);
  viewport.setAttribute("aria-live", "polite");
  viewport.setAttribute("aria-relevant", "additions");
  document.body.appendChild(viewport);

  return viewport;
}

function buildToast({ title, message, tone }) {
  const el = document.createElement("div");
  el.className = "mk-toast";
  el.setAttribute("role", "status");
  el.setAttribute("data-mk-tone", tone || "neutral");
  el.setAttribute("data-mk-state", "closed");

  const header = document.createElement("div");
  header.style.display = "flex";
  header.style.alignItems = "start";
  header.style.gap = "10px";

  const body = document.createElement("div");
  body.style.flex = "1 1 auto";
  body.style.minWidth = "0";

  if (title) {
    const h = document.createElement("p");
    h.className = "mk-toast-title";
    h.textContent = title;
    body.appendChild(h);
  }

  const p = document.createElement("p");
  p.className = "mk-toast-message";
  p.textContent = message || "";
  body.appendChild(p);

  const closeBtn = document.createElement("button");
  closeBtn.className = "mk-toast-close";
  closeBtn.type = "button";
  closeBtn.setAttribute("aria-label", "Close");
  closeBtn.textContent = "×";

  header.appendChild(body);
  header.appendChild(closeBtn);
  el.appendChild(header);

  return { el, closeBtn };
}

export function createToastManager({
  position = "top-right",
  selector = "[data-mk-toast-viewport]",
} = {}) {
  const reduce = prefersReducedMotion();
  const viewport = ensureViewport({ selector, position });

  function closeToast(toastEl) {
    if (!toastEl) return;

    const finalize = () => {
      toastEl.remove();
    };

    if (reduce) {
      finalize();
      return;
    }

    toastEl.setAttribute("data-mk-state", "closing");
    const t = Math.max(maxTransitionMs(toastEl), 80);
    window.setTimeout(finalize, t + 20);
  }

  function show({
    title,
    message = "",
    tone = "neutral",
    timeout = 3500,
  } = {}) {
    const { el, closeBtn } = buildToast({ title, message, tone });

    viewport.appendChild(el);

    const open = () => el.setAttribute("data-mk-state", "open");
    if (reduce) open();
    else requestAnimationFrame(open);

    const onClose = (e) => {
      e.preventDefault();
      closeToast(el);
    };
    closeBtn.addEventListener("click", onClose);

    let timer = null;
    if (timeout !== 0 && timeout != null) {
      timer = window.setTimeout(() => closeToast(el), timeout);
    }

    return {
      el,
      close() { if (timer) window.clearTimeout(timer); closeToast(el); },
    };
  }

  return { viewport, show };
}

let defaultManager = null;

export function toast(messageOrOptions, maybeOptions = {}) {
  if (!defaultManager) defaultManager = createToastManager();
  if (typeof messageOrOptions === "string") {
    return defaultManager.show({ message: messageOrOptions, ...maybeOptions });
  }
  return defaultManager.show(messageOrOptions || {});
}
Preview
Viewport (preview)
Toast

これはプレビュー通知です。

Tips
  • • デザイン側は CSS 変数(tokens)で一括調整できます。
  • • 動きは “短く・小さく・同じ癖” を優先すると品位が保てます。
  • • prefers-reduced-motion を必ず考慮してください。