Toast Notification Micro-interaction

Four triggers spawn stacked toasts bottom-right: each slides in from x 40 with a back.out overshoot while earlier toasts shift up via Flip, an icon draws itself with DrawSVG, a hairline counts down 4s (pausing on hover) and dismissal re-flows the stack.

Loading

AI prompt

Recreate this effect with any AI tool

Paste into Claude, Cursor, v0, Lovable or Bolt. 1207 words of exact values — rewritten live as you tune the Playground.

prompt.md · Toast Notification Micro-interaction
### EFFECT: Toast Notification Micro-interaction
**What it does (visual):** A small dark "app window" (title bar with three dots, a serif heading "Fire a toast", a paragraph and four outlined pill buttons: Success, Info, Warning, Error) sits centred on the page. Pressing a button makes a notification card appear in the bottom-right corner: it slides in from 40px to the right while fading up from transparent and growing from 96% to 100% with a gentle overshoot (0.5s, `back.out(1.4)`). Any toasts already on screen glide upward to make room (Flip, 0.5s). Inside the new toast a 24px icon draws itself: first the circular ring strokes around, then the mark (a check, an "i", a "!" or a cross) draws in 0.12s later, both in the toast's tone colour — `#d4ff3a` for success, neutral for info, amber for warning, red for error. The title and message sit beside the icon, a small "×" sits top-right, and a 1px hairline in the tone colour runs across the bottom edge, shrinking from full width to zero over exactly 4 seconds. Hovering a toast freezes the hairline; leaving resumes it. When the hairline reaches zero — or when the toast is clicked — the card slides 40px right and fades out in 0.35s (accelerating) and the remaining toasts settle down into the gap (Flip, 0.4s). At most four toasts stack; a fifth dismisses the oldest. On first view three demo toasts fire automatically at 0.6s, 1.5s and 2.4s (demo salvo enabled: true).

**Tech stack:**
- GSAP 3.13+ with Flip and DrawSVGPlugin (both free since 3.13).
- CDN: `gsap.min.js`, `Flip.min.js`, `DrawSVGPlugin.min.js` from `https://cdn.jsdelivr.net/npm/gsap@3/dist/`.
- npm: `import gsap from "gsap"; import { Flip } from "gsap/Flip"; import { DrawSVGPlugin } from "gsap/DrawSVGPlugin"; gsap.registerPlugin(Flip, DrawSVGPlugin);`
- Fonts: a sans for toast text (demo: Inter Tight), a mono for labels, a serif for the page heading.

**HTML structure:**
```html
<section class="tmi">
  <div class="tmi__app">
    <header class="tmi__bar" aria-hidden="true"><span class="tmi__dot"></span><span class="tmi__dot"></span><span class="tmi__dot"></span><span class="tmi__bar-title">Ledger · Orders</span></header>
    <div class="tmi__body">
      <h1 class="tmi__h">Fire a toast</h1>
      <div class="tmi__triggers">
        <button class="tmi__trigger" type="button" data-type="success">Success</button>
        <button class="tmi__trigger" type="button" data-type="info">Info</button>
        <button class="tmi__trigger" type="button" data-type="warning">Warning</button>
        <button class="tmi__trigger" type="button" data-type="error">Error</button>
      </div>
    </div>
  </div>
  <div class="tmi__stack" role="region" aria-live="polite" aria-label="Notifications"></div>
  <template id="tmi-toast">
    <div class="tmi__toast" role="status">
      <svg class="tmi__icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
        <circle class="tmi__icon-ring" cx="12" cy="12" r="10.5"/>
        <path class="tmi__icon-mark" d=""/>
      </svg>
      <div class="tmi__text"><p class="tmi__toast-title"></p><p class="tmi__toast-msg"></p></div>
      <button class="tmi__close" type="button" aria-label="Dismiss notification">×</button>
      <span class="tmi__progress" aria-hidden="true"></span>
    </div>
  </template>
</section>
```
Mark paths (24×24 box): success `M7.5 12.5l3 3 6-7`, info `M12 11v5.5M12 7.5v.5`, warning `M12 7.5v5.5M12 16v.5`, error `M8.5 8.5l7 7M15.5 8.5l-7 7`.

**CSS requirements:**
- `.tmi__stack`: `position: fixed; right: 20px; bottom: 20px; z-index: 20; display: flex; flex-direction: column; align-items: stretch; gap: 10px; width: min(360px, calc(100vw - 40px)); pointer-events: none;` (children re-enable `pointer-events: auto`). Because the column is anchored to the bottom, appending a toast pushes the earlier ones *up*.
- `.tmi__toast`: `--tone: var(--text); position: relative; display: grid; grid-template-columns: 24px 1fr auto; gap: 12px; align-items: start; padding: 14px 12px 16px 14px; border: 1px solid var(--line, rgba(255,255,255,.08)); border-radius: 12px; background: var(--surface, #121212); box-shadow: 0 24px 60px -24px rgba(0,0,0,.65); overflow: hidden; cursor: pointer; will-change: transform, opacity;`
- Tones: `[data-type="success"] { --tone: var(--accent, #d4ff3a) }`, `[data-type="info"] { --tone: var(--text) }`, `[data-type="warning"] { --tone: #F2B544 }`, `[data-type="error"] { --tone: #FF5C5C }`.
- `.tmi__icon-ring, .tmi__icon-mark`: `fill: none; stroke: var(--tone); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round;`
- `.tmi__toast-title`: `.9rem`, weight 500; `.tmi__toast-msg`: `.8rem`, `color: var(--muted)`, `margin-top: 3px`.
- `.tmi__close`: 26px circle, muted, lightens on hover; visible focus ring.
- `.tmi__progress`: `position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--tone); transform-origin: left center;`
- `.tmi__app`: `width: min(100%, 760px); border: 1px solid var(--line); border-radius: 14px; background: var(--surface); overflow: hidden;` — triggers are outlined pills that invert on hover.
- Under 768px: `.tmi__stack { right: 12px; bottom: 12px; width: calc(100vw - 24px) }`.

**Animation spec (exact values):**
`spawn(type)`:
1. If `stack.children.length >= 4` → `dismiss(stack.firstElementChild)`.
2. Clone the template, set `data-type`, title, message and the mark's `d`.
3. `const state = Flip.getState(stack.children)` → `stack.appendChild(el)` → `Flip.from(state, { duration: 0.5, ease: 'power3.out' })` (existing toasts shift up; the new element is not in the state so Flip ignores it).
4. Entrance: `gsap.fromTo(el, { x: 40, opacity: 0, scale: 0.96 }, { x: 0, opacity: 1, scale: 1, duration: 0.5, ease: 'back.out(1.4)' })`.
5. Icon: `gsap.fromTo([ring, mark], { drawSVG: '0%' }, { drawSVG: '100%', duration: 0.6, ease: 'power2.out', stagger: 0.12, delay: 0.15 })`.
6. Timer: `el._timer = gsap.fromTo(bar, { scaleX: 1 }, { scaleX: 0, duration: 4, ease: 'none', delay: 0.3, onComplete: () => dismiss(el) })`.
7. Listeners: `pointerenter → el._timer.pause()`, `pointerleave → el._timer.resume()`, `click → dismiss(el)`.
`dismiss(el)`:
1. Guard with `el._leaving`; `el._timer.kill()`; `el.style.pointerEvents = 'none'`.
2. `gsap.to(el, { x: 40, opacity: 0, duration: 0.35, ease: 'power2.in', onComplete })` where `onComplete` does: `const state = Flip.getState([...stack.children].filter(c => c !== el)); el.remove(); Flip.from(state, { duration: 0.4, ease: 'power3.out' })`.
Demo salvo (if true): `gsap.delayedCall(0.6, () => spawn('success'))`, `1.5 → 'info'`, `2.4 → 'warning'`.

**Interaction:** Trigger buttons spawn by `data-type`. Hovering a toast pauses its 4s timer (the hairline visibly stops); leaving resumes. Clicking anywhere on a toast (including the ×) dismisses it immediately with the exit tween. The stack never exceeds four.

**Responsive behavior:** The stack is full-width minus 24px under 768px; toasts keep the same grid. No `gsap.matchMedia()` branches are needed — hover-to-pause simply does not fire on touch, where the timer runs to completion (tap dismisses).

**Accessibility:** `.tmi__stack` is an `aria-live="polite"` region present in the DOM from load; each toast is `role="status"` so its text is announced once when inserted. The close button has `aria-label="Dismiss notification"` and a focus ring; the icon SVG is `aria-hidden`. Under `prefers-reduced-motion: reduce`: no Flip, no entrance/exit tweens — toasts are `gsap.set` to their final state and removed instantly; the icon is drawn at 100% immediately; only the hairline timer still runs (it is informational, not decorative).

**Performance rules:** Only `transform` and `opacity` are tweened on toasts (`x`, `scale`, `scaleX`); DrawSVG touches `stroke-dasharray/offset` on two tiny paths. `will-change: transform, opacity` is on toasts only. Everything lives in `gsap.context()`; on unmount `ctx.revert()` and `stack.replaceChildren()` clear timers and DOM. The host pauses `gsap.globalTimeline` off-screen, which also pauses every running timer.

**Customization knobs:**
- `MAX_TOASTS` (4) and `LIFETIME` (4s).
- `ENTER` (`0.5s back.out(1.4)`), slide distance (40px, shared by the entrance and exit) and `EXIT` (`0.35s power2.in`); Flip durations (0.5 / 0.4).
- Tones per type via `--tone` (success: #d4ff3a); icon stroke width (1.4).
- Demo salvo on/off (true).
- Stack position (`right/bottom: 20px`), width (360px) and `gap` (10px).
- Replace `appendChild` with `prepend` and anchor the stack to the top to get a top-right stack that pushes downward.

**Acceptance criteria:**
- [ ] A new toast enters from x 40 with a visible overshoot and existing toasts move up smoothly (no jump).
- [ ] The icon ring draws first and the mark follows 0.12s later; both in the toast's tone colour.
- [ ] The hairline takes exactly 4s to reach zero, pauses while hovered and resumes on leave; the toast is removed when it hits zero.
- [ ] Clicking a toast exits it in 0.35s and the stack re-flows; a fifth toast dismisses the oldest.
- [ ] Screen readers announce each toast; reduced motion shows toasts in place with no sliding.
- [ ] No console errors; no double-dismiss when clicking a leaving toast.

Source

Full source code

<section class="tmi">
  <div class="tmi__app">
    <header class="tmi__bar" aria-hidden="true"><span class="tmi__dot"></span><span class="tmi__dot"></span><span class="tmi__dot"></span><span class="tmi__bar-title">Ledger · Orders</span></header>
    <div class="tmi__body">
      <p class="tmi__eyebrow">Button · Notification stack</p>
      <h1 class="tmi__h">Fire a toast</h1>
      <p class="tmi__p">Each notification slides in from the right with a soft overshoot, the pile re-flows with Flip, an icon draws itself in and a hairline counts down four seconds — hover to pause it.</p>
      <div class="tmi__triggers">
        <button class="tmi__trigger" type="button" data-type="success">Success</button>
        <button class="tmi__trigger" type="button" data-type="info">Info</button>
        <button class="tmi__trigger" type="button" data-type="warning">Warning</button>
        <button class="tmi__trigger" type="button" data-type="error">Error</button>
      </div>
    </div>
  </div>

  <div class="tmi__stack" role="region" aria-live="polite" aria-label="Notifications"></div>

  <template id="tmi-toast">
    <div class="tmi__toast" role="status">
      <svg class="tmi__icon" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
        <circle class="tmi__icon-ring" cx="12" cy="12" r="10.5"/>
        <path class="tmi__icon-mark" d=""/>
      </svg>
      <div class="tmi__text">
        <p class="tmi__toast-title"></p>
        <p class="tmi__toast-msg"></p>
      </div>
      <button class="tmi__close" type="button" aria-label="Dismiss notification">
        <svg viewBox="0 0 12 12" width="12" height="12" aria-hidden="true"><path d="M2 2l8 8M10 2l-8 8" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>
      </button>
      <span class="tmi__progress" aria-hidden="true"></span>
    </div>
  </template>
</section>