はじめに
トースト通知の実装、地味に面倒じゃないですか。
自前で作ると状態管理やアニメーション、複数表示の管理など考えることが多いし、かといって既存ライブラリは設定項目が多すぎたり、デザインがイマイチだったり。
そんな中で見つけたのがSonner。GitHubで11,700スター以上、毎月2,500万回以上のnpmダウンロード。数字だけ見ても「使われている」のは明らかですね。
正直なところ、最初は「またトーストライブラリか」と思っていたんですよ。でも実際に使ってみたら、「これでいいじゃん」という気持ちになりました。シンプルさと見た目の良さ、このバランスが絶妙なんです。
Sonnerとは
SonnerはEmil Kowalskiさんが開発した「An opinionated toast component for React」。日本語で言えば「こだわりのあるReact用トーストコンポーネント」といったところでしょうか。
「opinionated」というのがポイントで、「このUIがベストだ」という思想がしっかりある。だからカスタマイズ項目は最小限で、でも出来上がりは美しい。
現在の最新バージョンは2.0.7で、MITライセンスのオープンソース。89人以上のコントリビューターが開発に参加していて、活発にメンテナンスされています。
特徴・メリット
1. とにかく軽い
バンドルサイズは約13.9kB(minified + gzipped)。トースト機能としては十分軽量ですね。
コスパ的に、この軽さでこの機能性は正義です。
2. APIがシンプル
これ、意外と重要なんですけど、Sonnerの基本的な使い方は本当にシンプル。
import { toast } from 'sonner'
toast('保存しました')
これだけ。学習コストがほぼゼロなんですよ。
3. デザインが美しい
個人的には、これが一番の魅力だと思います。アニメーションが滑らかで、デフォルトのスタイリングがモダン。何も設定しなくても「いい感じ」に見える。
30代になって思うのは、デザインセンスがないエンジニアでもそれなりの見た目になるライブラリは貴重だということ。
4. 複数種類のトーストに対応
- Default(デフォルト)
- Success(成功)
- Error(エラー)
- Warning(警告)
- Info(情報)
- Promise(非同期処理用)
- Custom(カスタム)
用途に応じて使い分けられる。特にPromiseトーストは、API呼び出し時の「Loading → Success/Error」の流れを簡単に実装できて便利。
5. 位置の自由度
6方向の配置が可能:
- top-left、top-center、top-right
- bottom-left、bottom-center、bottom-right
サービスのUIに合わせて配置を変えられるのは地味にありがたい。
6. スワイプで閉じられる
モバイル対応もしっかりしていて、スワイプで閉じる機能が標準装備。位置に応じてスワイプ方向も自動で変わるという親切設計。
インストール方法
前提条件
Reactプロジェクト(React 18以上推奨)がセットアップされている必要があります。Next.jsでも問題なく動きます。
インストール
npm install sonner
これだけ。時短になる。
基本的な使い方
セットアップ
まず、アプリケーションのルートに<Toaster />コンポーネントを配置します。
// app/layout.tsx (Next.js App Router)
import { Toaster } from 'sonner'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ja">
<body>
{children}
<Toaster />
</body>
</html>
)
}
Providerでラップする必要もないし、設定ファイルを書く必要もない。シンプル。
基本的なトースト
import { toast } from 'sonner'
function SaveButton() {
const handleSave = () => {
// 保存処理
toast('保存しました')
}
return <button onClick={handleSave}>保存</button>
}
トーストの種類
// 成功
toast.success('ファイルをアップロードしました')
// エラー
toast.error('保存に失敗しました')
// 警告
toast.warning('保存されていない変更があります')
// 情報
toast.info('新しい更新があります')
// 説明付き
toast.success('保存完了', {
description: 'すべての変更が保存されました',
})
直感的でしょ。メソッド名がそのまま意味を表している。
Promiseトースト
これが個人的には一番よく使う機能。API呼び出しの状態をそのまま表示できる。
const handleSubmit = async () => {
toast.promise(
fetch('/api/save', {
method: 'POST',
body: JSON.stringify(data),
}),
{
loading: '保存中...',
success: '保存しました',
error: '保存に失敗しました',
}
)
}
Promiseの状態に応じて自動でトーストが切り替わる。これ、自前で実装しようとすると結構面倒なんですよ。
アクション付きトースト
toast('ファイルを削除しました', {
action: {
label: '元に戻す',
onClick: () => restoreFile(),
},
})
Undo機能の実装がこれだけで済む。
カスタムトースト
JSXをそのまま渡せるので、独自のUIも作れます。
toast.custom((t) => (
<div className="flex items-center gap-2 bg-white p-4 rounded-lg shadow">
<img src="/icon.png" alt="" className="w-8 h-8" />
<div>
<p className="font-bold">新着メッセージ</p>
<p className="text-sm text-gray-500">田中さんからメッセージが届きました</p>
</div>
</div>
))
実践的なユースケース
フォーム送信時のフィードバック
'use client'
import { toast } from 'sonner'
import { useState } from 'react'
export function ContactForm() {
const [isSubmitting, setIsSubmitting] = useState(false)
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
setIsSubmitting(true)
const formData = new FormData(e.currentTarget)
toast.promise(
fetch('/api/contact', {
method: 'POST',
body: formData,
}).then((res) => {
if (!res.ok) throw new Error('送信に失敗しました')
return res.json()
}),
{
loading: '送信中...',
success: 'お問い合わせを受け付けました',
error: (err) => err.message,
}
)
setIsSubmitting(false)
}
return (
<form onSubmit={handleSubmit}>
{/* フォームフィールド */}
<button type="submit" disabled={isSubmitting}>
送信
</button>
</form>
)
}
認証状態の通知
import { toast } from 'sonner'
export function useAuth() {
const login = async (email: string, password: string) => {
try {
const result = await signIn({ email, password })
toast.success(`ようこそ、${result.user.name}さん`)
return result
} catch (error) {
toast.error('ログインに失敗しました', {
description: 'メールアドレスまたはパスワードが正しくありません',
})
throw error
}
}
const logout = async () => {
await signOut()
toast.info('ログアウトしました')
}
return { login, logout }
}
クリップボードコピー
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
toast.success('コピーしました')
} catch {
toast.error('コピーに失敗しました')
}
}
設定変更の通知
const updateSettings = async (settings: Settings) => {
const toastId = toast.loading('設定を更新中...')
try {
await saveSettings(settings)
toast.success('設定を更新しました', { id: toastId })
} catch {
toast.error('設定の更新に失敗しました', { id: toastId })
}
}
同じIDを指定することで、既存のトーストを更新できる。これも地味に便利。
Toasterのカスタマイズ
<Toaster
position="top-right"
richColors
closeButton
expand={true}
visibleToasts={5}
toastOptions={{
duration: 4000,
style: {
background: '#333',
color: '#fff',
},
}}
/>
richColors: 種類に応じた色付けcloseButton: 閉じるボタンを表示expand: 複数トーストを展開表示visibleToasts: 同時表示数の制限
Next.js App Routerでの注意点
Server ComponentsとClient Componentsの境界に注意が必要です。
// これはダメ(Server Componentでtoastを呼べない)
export default function Page() {
toast('Hello') // エラー
return <div>...</div>
}
// こうする
'use client'
import { toast } from 'sonner'
export default function Page() {
return (
<button onClick={() => toast('Hello')}>
Show Toast
</button>
)
}
toast()はクライアントサイドでのみ動作するので、必ずClient Componentから呼び出す必要があります。
まとめ
Sonnerを導入して感じた変化:
- セットアップ: 1分で完了。Toasterを置くだけ
- 学習コスト: ほぼゼロ。
toast()を呼ぶだけ - デザイン: デフォルトで美しい。カスタマイズ不要
- Promise対応: 非同期処理の状態表示が簡単
- バンドルサイズ: 約13.9kBで十分軽量
正直なところ、トースト通知の実装でSonner以外を選ぶ理由はあまりないと思います。シンプルさ、美しさ、機能性のバランスが絶妙。
特に「とりあえず動くトースト通知がほしい」という場面ではSonner一択ですね。設定に悩む時間がゼロになるのは、QOL上がります。
まだ試していない人は、次のプロジェクトで使ってみてください。トースト通知の実装で消耗する時間がなくなりますよ。
