エクスクルーシブ
Toast Physics <mv-toast-physics>
本物の剛体として振る舞う通知です。角のトレイに落ちて重力で積み重なり、優先度が質量になります(エラーは重く幅も広く、ドスンと着地して他の通知を揺らし、軽いカードの下へ沈みます)。
仕組み
本物の剛体として振る舞う通知:隅のトレイに落ちて重力で積み重なり、優先度が質量になります(エラーは重く幅も広く、ドスンと着地して他のカードを揺らし、その後より軽いカードの下に沈みます)。カードをつかんで投げると閉じられ、期限が切れるとフェードアウトし、積み重なりがその隙間を埋めるように落ち着きます。API の考え方は toast() と同じです。
| カテゴリー | オーバーレイ |
|---|---|
| タイプ | Web Component(<mv-toast-physics>) |
| ステータス | 安定版 |
| Keywords | exclusive, toast, notification, physics, gravity, fling, rigid-body, priority, live-region, tactile |
When to use
- Notifications are part of a playful product identity and a tactile, physics-driven pile adds delight
- Error notifications should visibly outweigh info messages and stay in view longer
Avoid when
インストール
node scripts/add.mjs toast-physics --out ./src/marvelousMarvelous UI MCP サーバーを使う AI エージェント:install_components({ slugs: ["toast-physics"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
コピーされるファイル(依存関係を含む):tokens/tokens.css、core/base.css、core/dom.js、core/element.js、core/motion.js、core/physics.js、components/toast-physics/toast-physics.js、components/toast-physics/toast-physics.css。
使い方
基本となるマークアップです。ここから属性、data-*、CSS 変数でカスタマイズしてください:
<div id="tp-demo" style="display:grid;gap:1rem;justify-items:center;text-align:center">
<div style="display:grid;gap:.25rem">
<strong style="font-size:var(--mv-text-base)">Stone & Linen Studio · back office</strong>
<span style="font-size:var(--mv-text-sm);color:var(--mv-fg-muted)">Notifications drop into the tray at the bottom right. Errors weigh more.</span>
</div>
<div style="display:flex;flex-wrap:wrap;gap:.5rem;justify-content:center;max-width:36rem">
<button class="mv-button" data-variant="outline" type="button" data-t="success">Order shipped</button>
<button class="mv-button" data-variant="outline" type="button" data-t="info">New customer review</button>
<button class="mv-button" data-variant="outline" type="button" data-t="warning">Low stock</button>
<button class="mv-button" data-variant="destructive" type="button" data-t="error">Payment declined</button>
<button class="mv-button" data-variant="secondary" type="button" data-t="burst">Burst ×4</button>
</div>
<p style="margin:0;font-size:.8125rem;color:var(--mv-fg-muted)">Grab a card and <strong style="color:var(--mv-fg);font-weight:500">fling it</strong> to dismiss · <strong style="color:var(--mv-fg);font-weight:500">Alt + T</strong> then <strong style="color:var(--mv-fg);font-weight:500">Delete</strong> from the keyboard</p>
<mv-toast-physics position="bottom-right"></mv-toast-physics>
</div>
<script type="module">
// In an app: import { toast } from "./components/toast-physics/toast-physics.js";
const { toast } = await customElements.whenDefined("mv-toast-physics");
const n = () => 4800 + Math.floor(Math.random() * 400);
const demos = {
success: () => toast.success(`Order #${n()} shipped`, { description: "Parcelway · arriving Thursday, Sep 24" }),
info: () => toast.info("New customer review ★★★★★", { description: "“Gorgeous mugs, beautifully packed.” (Hannah K.)" }),
warning: () => toast.warning("Low stock: Stoneware Mug 12 oz", { description: "Only 3 left. Time to restock.", action: { label: "Reorder", onClick: () => toast.success("Purchase order sent to the studio") } }),
error: () => toast.error("Payment declined", { description: "Order #4827 · card expired, the customer has been notified.", action: { label: "Retry", onClick: () => {} } }),
burst: () => ["info", "success", "warning", "success"].forEach((k, i) => setTimeout(demos[k], i * 170)),
};
document.getElementById("tp-demo").addEventListener("click", (e) => {
const k = e.target.closest("[data-t]")?.dataset.t;
if (k) demos[k]();
});
// Arrival: two light cards, then an error that sinks beneath them.
setTimeout(() => toast.info("New customer review ★★★★★", { description: "“Gorgeous mugs, beautifully packed.” (Hannah K.)", duration: 16000 }), 150);
setTimeout(() => toast.success("Order #4821 shipped", { description: "Parcelway · arriving Thursday, Sep 24", duration: 16000 }), 450);
setTimeout(() => toast.error("Payment declined", { description: "Order #4827 · card expired, the customer has been notified.", duration: 20000 }), 1000);
</script>API
Attributes
| Name | タイプ | Default | Description |
|---|---|---|---|
position | bottom-right | bottom-left | bottom-center | top-right | top-left | top-center | bottom-right | Tray corner. At the top, gravity flips: cards rise and stack against the top edge. |
max | number | 5 | Number of cards in the tray; beyond that, the oldest of the lightest cards is removed. |
duration | number | 6000 | Default duration (ms); errors stay 1.6× longer. |
gravity | number | 1 | Gravity multiplier (1 ≈ 2200 px/s²). |
label | string | Notifications | Accessible name of the region. |
Properties
| Name | タイプ | Description |
|---|---|---|
toasts | object[] | Read-only: notifications in the tray, top to bottom. |
Methods
| Name | Description |
|---|---|
toast(message, options) → id | Exported from toast-physics.js (also MvToastPhysics.toast). options: type (default | success | info | warning | error), description, duration (ms or Infinity), id (same id = updated in place, with a little hop), action { label, onClick(e, { id, dismiss }) }, important (assertive announcement), closeLabel, toaster (selector), onDismiss, onAutoClose. Creates a <mv-toast-physics> if needed. |
toast.success / .info / .warning / .error(message, options) | Typed shortcuts. |
toast.dismiss(id?) | Dismisses one notification, or all of them. |
element.add(options) / element.dismiss(id?) | Same operations on a specific tray. |
Events
| Name | Description |
|---|---|
mv-toast | New notification. detail: { id, type }. |
mv-dismiss | detail: { id, reason } (timeout, fling, close, keyboard, action, overflow, dismiss). |
mv-settle | The pile is at rest. detail: { order } (ids top to bottom). |
CSS classes
| Name | Description |
|---|---|
mv-toast-physics-item / -icon / -content / -title / -description / -action / -close | Generated parts; data-type on the card, data-dragging while grabbed, data-sinking while a heavy card sinks. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-toast-physics-width | 22rem | Tray width (an error takes 100%, a warning 95%, the rest 90%). |
--mv-toast-physics-height | min(68vh, 34rem) | Tray height (drop height). |
--mv-toast-physics-offset | 1.25rem | Distance from the screen edges. |
--mv-toast-physics-bg | var(--mv-surface-raised) | Card background. |
Accessibility
A region named “Notifications (Alt+T)” in the top layer; each new card is announced in a polite live region (assertive for errors and important:true), separate from the list so reordering is never re-announced. Cards form a focusable list whose DOM order follows the visual order: Alt+T moves focus there, ↑/↓/Home/End navigate, Delete, Backspace or Escape dismiss the card and focus moves to its neighbor, then back to the original element. The close button shows on hover, on focus and always on touch. Timers pause on hover, on keyboard focus, while grabbed and when the tab is hidden (the duration fuse too). Reduced motion: a classic stacked list, heaviest at the bottom, with no physics or fling. Forced colors: cards keep their system border, the fuse is hidden. Moving the tray in the DOM keeps the cards and their remaining time. Known limits: a Node passed as message or description is moved into the card, not copied; the region label and the keyboard hint are in English (label attribute, closeLabel option); on a narrow screen the tray spans the width and covers the page corner while cards are shown.