Building a Toast Component

2025/12/03 23:51

Building a Toast Component

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:


Summary

Sonner(2023年リリース)は、多くのハイプロファイルプロジェクトや shadcn/ui フレームワークで定番のトーストライブラリとなっています。週あたり 7 百万件以上 の npm ダウンロードを記録し、Cursor、X、Vercel などの企業でも採用されています。

主な強み:

  • シンプルな API:
    sonner
    から
    toast
    をインポートしてどこでも呼び出せます。フックやコンテキストは不要です。プロミスベースのヘルパー(
    toast.promise
    )により、ローディング・成功・エラー状態を扱えます。
  • 開発者体験: インタラクティブなドキュメントとすぐに使える例が採用障壁を下げています。
  • 視覚的魅力 & UX:
    • スムーズなスタッキングアニメーション は CSS トランジション(キー フレームではなく)で構築され、割り込み可能な動きを実現します。
    • スワイプ解除 はモバイル/デスクトップの両方で機能し、速度閾値 0.11 または距離で判定します。
    • ホバー拡張: トースト領域にカーソルを合わせるとすべてのトーストが展開されます。
      <Toaster />
      expand
      プロパティで制御できます。
    • 小さな改善点: ドキュメントが非表示になった際はタイマーを一時停止、ホバーギャップは疑似要素で維持、ドラッグ中のポインタキャプチャを保持し、摩擦でスワイプ動作を滑らかにします。

内部的には Sonner は Observer パターン

ToastState
<Toaster />
setToasts
を通じて購読)を使用して状態管理を効率化しています。

このライブラリの成功は、開発者フレンドリーな API、洗練されたアニメーション、および競合他社と差別化する思慮深い UX ディテールに起因します。今後の計画としては、インタラクティブドキュメントを拡充し、プロミスベースのヘルパーを増やすとともにアクセシビリティ機能を洗練させることが挙げられます。


本文

2023 年に立ち上げた Toast ライブラリ「Sonner」

2023 年、私は Sonner と呼ばれる Toast ライブラリを作ることにしました。現在では npm から週に 700 万件以上がダウンロードされており、Cursor、X、Vercel などの企業で採用されています。また、shadcn/ui のデフォルト Toast コンポーネントでもあります。

開発時点では「Toast 市場」は既に混雑していました。そこで Sonner を際立たせるポイントは何だったのでしょうか? なぜ人々は確立された代替品よりも Sonner を選ぶのですか?


名前

機能を表す名前(react-toast、react-snackbar、react-notifications など)は安っぽく感じられます。私は「もっとユニークでエレガントなもの」を求めていました。

フランス語で通知に関係する単語を調べた結果、「Sonner」(サンネール)という言葉がぴったりでした。

sonner /sɔ.ne/
動詞(自動詞) – 古フランス語 soner から、ラテン語 sonāre に由来

  1. 鳴る、音を立てる 2. 鐘を鳴らす
    Sonner la cloche(鐘を鳴らす)
    Sonner à la porte(ドアベルを鳴らす)

発見性と明確さは犠牲にしますが、エレガントで違和感のない名前だと感じました。


アニメーション

Sonner が即座に注目された理由は、そのスタッキングアニメーションです。企業では既に使われていた技術ですが、オープンソース化されていませんでした。この動きが自然であることを確認できたとき、すぐに採用しました。

最初は CSS キーフレームを使用していましたが、中断不可だったため、複数 Toast を追加すると古いものが新しい位置へジャンプし、スムーズな遷移になりませんでした。

解決策

CSS トランジション(中断可能)と小さな

useEffect
フックを組み合わせて、初回レンダリング後に
mounted
を設定します。

React.useEffect(() => {
  setMounted(true);
}, []);

<li data-mounted={mounted}>
  .sonner-toast { transition: transform 400ms ease; }
  [data-mounted="true"]   { transform: translateY(0); }
  [data-mounted="false"]  { transform: translateY(100%); }
</li>

後に

@starting-style
CSS アットルールでさらに簡潔化できます。

Toast のスタッキング

スタッキング効果を作るには、各 Toast のインデックスに応じてギャップを掛けた値を Y 座標にします。Toast は絶対位置で配置され、深さ感のため少し縮小されています。

[data-sonner-toast][data-expanded="false"][data-front="false"] {
  --scale: var(--toasts-before) * 0.05 + 1;
  --y: translateY(calc(var(--lift-amount) * var(--toasts-before)))
       scale(calc((-1 * var(--toasts-before) * 0.05) + 1));
}

Toast の高さが異なる場合は、スタックモードで前面 Toast と同じ高さに揃えます。


スワイプでの解除

もう一つの特徴は、モバイルやデスクトップでも便利なスワイプ解除です。速度ベース(モーメンタム)で動作し、短い距離でも素早くフリックすると解除できます。

const onMove = (event) => {
  const yPosition = event.clientY - pointerStartRef.current.y;
  toastRef.current.style.setProperty("--swipe-amount", `${yPosition}px`);
};

const timeTaken = new Date().getTime() - dragStartTime.current.getTime();
const velocity   = Math.abs(swipeAmount) / timeTaken;

if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
  removeToast(toast);
}

Toast の展開

スタックモードでは、Toast エリアにカーソルを合わせるとすべての Toast が拡大します。

const toastsHeightBefore = React.useMemo(() => {
  return heights.reduce((prev, curr, reducerIndex) => {
    if (reducerIndex >= heightIndex) return prev;
    return prev + curr.height;
  }, 0);
}, [heights, heightIndex]);

const offset = React.useMemo(
  () => heightIndex * GAP + toastsHeightBefore,
  [heightIndex, toastsHeightBefore],
);

<Toaster />
expand
プロパティを渡すことで、デフォルトで有効にできます。


開発者体験

コンポーネントは意図的にシンプルです。フックやコンテキストは不要で、

<Toaster />
を一度挿入し、どこからでも
toast()
を呼び出せます。

function Toaster() {
  const [toasts, setToasts] = React.useState([]);

  React.useEffect(() => {
    return ToastState.subscribe((toast) => {
      setToasts(toasts => [...toasts, toast]);
    });
  }, []);

  return (
    <ol>
      {toasts.map((toast, index) => (
        <Toast key={toast.id} toast={toast} />
      ))}
    </ol>
  );
}
import { toast } from "sonner";

toast("My toast");

Promise API は高く評価されています。

toast.promise(promise, {
  loading: 'Loading...',
  success: 'Success!',
  error:   'Error!'
});

react-hot-toast
に触発されつつ、状態管理にはオブザーバーパターンを採用しています。


小さなディテールが大きく違う

  • タブが隠れたときに一時停止

    useIsDocumentHidden
    フックでタイマーを止めます。

    export const useIsDocumentHidden = () => {
      const [isDocumentHidden, setIsDocumentHidden] = React.useState(document.hidden);
      
      React.useEffect(() => {
        function handleVisibilityChange() { setIsDocumentHidden(document.hidden); }
        document.addEventListener("visibilitychange", handleVisibilityChange);
        return () => document.removeEventListener("visibilitychange", handleVisibilityChange);
      }, []);
    
      return isDocumentHidden;
    };
    
  • 一貫したホバー状態

    ::after
    擬似要素で Toast 間の隙間を埋めます。

  • ドラッグ中のポインタキャプチャ:ポインタが範囲外に出ても Toast が反応し続けます。

  • 上方向ドラッグ時の摩擦:Toast は減速して自然に停止します。

これら見えない部分が組み合わさり、滑らかで直感的な体験を実現しています。


Sonner の成功理由

  1. 開発者体験 – フックやコンテキスト不要。
    <Toaster />
    +
    toast()
    だけで完結。
  2. ビジュアルの魅力 – 美しいデフォルトと洗練されたアニメーションが、混雑した市場で際立ちます。

Sonner のようなコンポーネントを作りたい方は、私のアニメーションコース「animations.dev」もぜひご覧ください。

同じ日のほかのニュース

一覧に戻る →

2025/12/08 2:18

I failed to recreate the 1996 Space Jam website with Claude

## Japanese Translation: ## 要約 著者は、Claude AI を使って 1996 年の Warner Bros の「Space Jam」ランディングページをスクリーンショットとアセットフォルダから再構築しようとしました。元のサイトは 200 KB 未満の単一 HTML ファイルで、絶対位置決め、テーブルレイアウト、およびタイル状の星空 GIF 背景に依存しています。 **プロセスと所見** 1. **初期試行:** Claude は概算レイアウトを生成しましたが、惑星軌道を誤った位置に配置しました。軌道パターンは認識できたものの、それを再現することには失敗しました。 2. **構造化プロンプト:** 著者は Claude に「知覚分析」「空間解釈」「再構築計画」の各セクションで理由を説明させ、正確なピクセル座標を要求しましたが、Claude はそれらを提供できませんでした。 3. **カスタムツール:** 精度向上のために 50 px → 5 px のグリッドオーバーレイ、ラベル付き座標参照点、色差比較、スクリーンショットサイドバイサイドビューア、およびスクリーンショットを 6 区域に分割するスクリプトを構築しました。 4. **結果:** Claude の調整は目標から 5–10 px 内に留まりましたが、正しい軌道半径(約 350–400 px)には決して収束しませんでした。内部レイアウトが生成されると、その後のフィードバックは元のスクリーンショットではなく、この誤ったモデルに基づいて行われました。 5. **トークナイズ仮説:** 著者は Claude が 16×16 パッチで画像をトークナイズしているため、細かい視覚的粒度が欠如し、セマンティック理解はあるもののピクセル精度が低いと考えました。 6. **ズームインテスト:** 200 % に拡大したスクリーンショットを提供して、大きなパッチで解像度が向上するか確認しましたが、Claude は依然として比例スケーリング指示に従いませんでした。 **結論** このタスクは未解決のままです。実験は Claude の空間推論限界をベンチマークとし、ピクセル単位で正確な画像再構築におけるモデルの現在の制約を示しています。

2025/12/08 7:18

How I block all online ads

## Japanese Translation: > **概要:** > 著者は、ウェブブラウザとモバイルアプリの両方で広告を排除するために長期的かつ多層的なアプローチを説明しています。彼は **Firefox + uBlock Origin** と最小限のフィルタリスト(組み込みのuBlockフィルタ、EasyList、AdGuard – Ads)と「広告でない不快要素」のためのカスタム非広告フィルタを使用します。 > DNS フィルタリングには **Pi‑hole(または AdGuard Home)** を Docker 上で $5 の DigitalOcean ドロップレットに稼働させ、WireGuard VPN の DNS サーバとして設定しています。トラフィックは **クラウドベースの VPN**(DigitalOcean、Hetzner、Azure、Google Cloud、または AWS)を経由し、プラットフォームが公的クラウド IP を検知して広告配信を減らします。 > この設定では **Cloudflare のキャプチャや HTTP エラー** が発生する場合があるため、著者は該当サイトで VPN を無効化しています。また、**Consent‑O‑Matic**(クッキーポップアップ)、**Buster**(キャプチャ)、**SponsorBlock**(動画広告)などのブラウザ拡張機能を推奨します。iOS では **Background App Refresh** をオフにするとデータ収集が減少し、Android では **ReVanced がアプリをパッチできますが、セキュリティリスクがあります** と指摘しています。 > 著者はこの統合戦略を 3 年以上使用しており、現在ほとんど広告を見ることはありません。プラットフォーム別の効果は異なります:YouTube は uBlock Origin + VPN(1週間〜1か月)が必要;Instagram は uBlock Origin のみで十分;Twitch は主に VPN に依存し、数日で効果が現れます;TikTok は両方のツールを使用しますが、数時間だけです。**AdMob** を利用するアプリも DNS ブロックの恩恵を受けます。 > 広告配信ネットワークは数日から数週間でパターンを観察し調整する可能性があるため、継続的な監視が必要です。著者は **Firebog** をブロックリストの良い情報源として引用し、正当なサイトを壊さないように許可リスト(allowlist)を維持する重要性を強調しています。

2025/12/07 23:37

Dollar-stores overcharge cash-strapped customers while promising low prices

## Japanese Translation: ドルジェネラルとファミリードラーは、棚に貼られたタグの価格よりも高い価格で顧客を頻繁に請求し、低所得層の買い物客に不釣り合いな過剰課金が広く発生しています。州検査と独立調査では、一部店舗でエラー率が88%に達するケースや、両チェーン全体で価格設定失敗が一貫して報告されています。 主な例としては、ノースカロライナ州ウィンザーのファミリードラーで23%のスキャンアイテムが過剰請求(同店の4回連続失敗)、オハイオ州ハミルトンのドルジェネラルで76%のエラー率(2022年10月)、ニュージャージー州バウンドブルックのファミリードラーで68%の不一致(2023年2月)があります。2022年1月以降、ドルジェネラルは4,300件以上、ファミリードラーは2,100件以上の価格失敗事例を記録しています。 アリゾナ州(60万ドル)、コロラド州(40万ドル)、ニュージャージー州・バーモント州・ウィスコンシン州・オハイオ州(最大100万ドル)など複数の州がチェーンと訴訟を和解し、連邦および州の司法長官は追加訴訟を提起しています。株主訴訟では、経営陣がシステム的問題を認識していたと主張されています。ニュージャージー州の連邦裁判所は、モバイルアプリ利用に関連する仲裁条項を理由にドルジェネラルに対する集団訴訟を停止し、消費者の救済手段を制限しました。 規制当局は現在の1検査あたり5,000ドル上限を超えるより厳格な執行や高い罰則を課すことができ、さらに州が調査を進めるにつれて追加の和解が生じる可能性があります。影響としては顧客信頼の低下、チェーンへの潜在的財務損失、評判へのダメージ、およびドルストア業界全体での価格設定と人員管理の強化への動きが挙げられます。