Most toast notifications are boxes that slide in and slide out. They work fine. But I wanted to see what happens when you treat a notification as something softer. Something that feels like it grows into place rather than appearing.

Mochi is the result. A toast that uses an SVG gooey filter to merge two shapes into one organic form when it expands.

Mochi

A gooey toast notification

Mochi demo with one anchored toast and gooey expansion.

Why SVG gooey

The expand interaction is two rounded rectangles. A pill for the header and a wider body underneath. The obvious implementation is stacking two divs and animating the second one in.

That works, but the seam between the two shapes is always visible. They feel like separate layers.

The gooey filter changes that. It runs a Gaussian blur, then pushes the alpha channel hard with a color matrix so the blurred edges snap back to solid. Where the two shapes overlap, their blurred edges merge into a single continuous surface.

With gooey filter
Without gooey filter

Same geometry and different feel with gooey filter vs plain shape layers.

The difference is subtle in a screenshot but obvious in motion. The filtered version feels like one object changing shape. The plain version feels like two objects next to each other.

This has a real cost though. The filter amplifies any edge movement, so spacing and overlap between the pill and body need precise tuning. A 4px overlap with an 18px border radius at a 9-unit blur deviation is where it started looking right. Change any one of those and the merge either gaps or bleeds.

Motion decisions

The easing is a CSS spring approximated with a linear() function. 33 stops that overshoot to about 1.028 before settling. At 600ms it gives the expansion a slight bounce that plays well with the gooey filter. The filter already softens edges, so the overshoot reads as organic stretch rather than mechanical wobble.

Everything participates in the motion. The body scales up, but the pill also stretches downward to meet it. Without that, the expand feels like a panel appearing below a static header. With it, the whole shape feels like one object growing. The header content shifts down slightly and scales to 0.9 when open, reinforcing that sense of a single surface redistributing.

Reduced motion support drops the duration to 20ms. Not zero, because instant state changes with the gooey filter cause a visible flash as the blur recalculates. A near-instant transition avoids that artifact while still respecting the preference.

State model

One boolean drives everything

const [open, setOpen] = useState(false)

// pill stretches down to meet the body
const pillFullH = PILL_H + BODY_H - 8
const pillScaleY = open ? 1 : PILL_H / pillFullH

// body scales up from zero
transform: scaleY(${open ? 1 : 0})

// header shifts and shrinks
transform: translateY(${open ? 3 : 0}px) scale(${open ? 0.9 : 1})

One boolean. The pill, body, and header all derive their transforms from it.

One boolean controls expansion. The pill stretch, the body scale, the header shift, and the text opacity all read from it. Hover and the toggle button both write to the same value.

When the state model is this flat, you can retune the motion or swap the easing curve without worrying about state interactions. The gooey filter already introduces enough visual complexity. The code should not add more.

Wrap-up

Mochi is intentionally narrow. One toast, one position, one interaction. The point was not to build a toast library. It was to see what a notification can feel like when you treat every layer as a design decision.

The SVG filter, the overlap tuning, the easing curve, the state model. Each is a small choice. Together they determine whether the thing feels considered or thrown together.

The whole implementation is about 150 lines of React with zero dependencies beyond React itself. No animation library, no toast system. One boolean, one SVG filter, one CSS spring.

Use it

Copy this into your AI coding agent. It has the full implementation. The agent will adapt it to your codebase, your color tokens, your component conventions.

Mochi AI Prompt

Implement an expandable toast notification called "Mochi" in this project.
It uses an SVG gooey filter to merge two shapes (a pill header and a wider
body) into one organic form. Zero animation library dependencies. Pure CSS
transitions with a linear() spring. One React component, one useState boolean.

Read the project first. Understand the component patterns, the color/theme
system, the styling approach (Tailwind, CSS modules, styled-components, etc),
and whether there is an existing reduced-motion hook or media query pattern.
Adapt everything below to match the project's conventions.

---

THE SVG GOOEY FILTER

The organic merge effect comes from an SVG filter pipeline applied to a <g>
element that wraps two <rect> elements (pill + body):

<filter id="gooey" colorInterpolationFilters="sRGB">
<feGaussianBlur in="SourceGraphic" stdDeviation="9" result="blur" />
<feColorMatrix
  in="blur"
  mode="matrix"
  values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 20 -10"
  result="goo"
/>
<feComposite in="SourceGraphic" in2="goo" operator="atop" />
</filter>

How it works:
1. feGaussianBlur (stdDeviation 9) blurs all shapes in the group
2. feColorMatrix pushes the alpha channel (the "20 -10" at the end) so
 blurred edges snap back to solid
3. Where the pill and body overlap, their blurred edges fuse into one
 continuous surface
4. feComposite operator="atop" composites original sharp graphics on top

The filter region must be expanded: x="-20%" y="-20%" width="140%" height="140%"
to prevent clipping of the blur.

Generate a unique ID for each filter instance (useId or equivalent) to avoid
collisions when multiple toasts exist.

---

GEOMETRY

These five values are tuned together. Changing one without adjusting the
others will cause the gooey merge to gap or bleed. Do not modify them
independently.

TOAST_W  = 260   // overall SVG width
PILL_H   = 40    // pill height (standard touch target)
BODY_H   = 60    // body height (room for 2-3 lines)
RADIUS   = 18    // border radius on both rects
OVERLAP  = 4     // vertical overlap between pill and body

The blur stdDeviation is RADIUS * 0.5 = 9. This relationship matters.

Derived values:
- pillWidth = clamp(120, TITLE.length * 9 + 60, 190) centered in TOAST_W
- bodyWidth = 220, centered in TOAST_W
- bodyTop   = PILL_H - OVERLAP + 4
- pillFullH = PILL_H + BODY_H - 8 (the pill's full stretched height)

---

MOTION MODEL

One boolean (open) drives every animated property:

PILL (the top <rect>):
- Full height is pillFullH, but when closed it is scaled to PILL_H/pillFullH
- When open: scaleY(1). When closed: scaleY(PILL_H / pillFullH)
- transformBox: "fill-box", transformOrigin: "50% 0%"
- This means the pill stretches DOWNWARD to meet the body

BODY (the bottom <rect>):
- When open: scaleY(1), opacity 1. When closed: scaleY(0), opacity 0
- transformBox: "fill-box", transformOrigin: "50% 0%"
- This means the body grows UPWARD from its top edge

Both shapes actively participate in the motion. This is critical. Without the
pill stretching down, it feels like two separate layers. With it, the whole
thing feels like one object changing shape.

HEADER TEXT (positioned over the pill):
- When open: translateY(3px) scale(0.9). When closed: translateY(0) scale(1)
- Creates a subtle "pushed down by the expansion" feel

BODY TEXT (positioned over the body):
- When open: opacity 0.72. When closed: opacity 0, pointerEvents none

All properties transition with the same easing and duration.

---

SPRING EASING

33-stop CSS linear() function. 600ms duration. Overshoots to 1.028 before
settling. The overshoot works well with the gooey filter because the filter
already softens edges, so the bounce reads as organic stretch.

const SPRING = `linear(
0, 0.002 0.6%, 0.007 1.2%, 0.015 1.8%, 0.025 2.4%,
0.057 3.7%, 0.104 5.2%, 0.151 6.5%, 0.208 7.9%,
0.455 13.6%, 0.566 16.3%, 0.619 17.7%, 0.669 19.1%,
0.715 20.5%, 0.755 21.8%, 0.794 23.2%, 0.829 24.6%,
0.861 26%, 0.889 27.4%, 0.914 28.8%, 0.937 30.3%,
0.956 31.8%, 0.974 33.4%, 0.987 34.8%, 0.997 36.2%,
1.014 39.2%, 1.024 42.5%, 1.028 46.3%, 1.026 51.9%,
1.01 66.1%, 1.003 74.9%, 1 85.2%, 1
)`

Use this exact value. Apply it to all transition properties as:
transition: transform 600ms ${SPRING}, opacity 600ms ${SPRING}

---

REDUCED MOTION

Use 20ms duration, NOT 0ms. Instant state changes cause a visible flash as
the gooey filter recalculates the blur in one frame. A near-instant 20ms
transition avoids this artifact while still respecting prefers-reduced-motion.

Check if the project already has a useReducedMotion hook or equivalent.
Use theirs. If none exists, add a minimal one:

function useReducedMotion() {
  const [reduced, setReduced] = useState(false)
  useEffect(() => {
    const mql = window.matchMedia("(prefers-reduced-motion: reduce)")
    setReduced(mql.matches)
    const handler = (e) => setReduced(e.matches)
    mql.addEventListener("change", handler)
    return () => mql.removeEventListener("change", handler)
  }, [])
  return reduced
}

---

COLORS

Map to the project's existing design tokens:
- Fill color (the rect fill): use the project's foreground/text color token
- Text color (the content on top): use the project's background/inverse token

These should automatically swap in dark mode if the project uses CSS custom
properties or a theme system. Do not hardcode hex values.

---

INTERACTION

- mouseenter on the toast container sets open = true
- mouseleave sets open = false
- Optionally add a toggle button that calls setOpen(v => !v)

---

IMPLEMENTATION RULES

- Single component file. Export one named function component.
- One useState boolean. No additional state.
- No useRef. No useEffect beyond reduced motion detection.
- No animation library (no Framer Motion, no GSAP, no react-spring).
- All motion is CSS transition with the linear() spring easing.
- The SVG and its filter are inline in the component JSX.
- Position the text overlays absolutely on top of the SVG.
- The component should accept title and description as props.
- Use the project's existing styling approach for the text overlays.

Full implementation prompt with one-click copy.