blobby-modal
v1.0.1
Published
React modal with gooey blob animations and morph transitions
Maintainers
Readme
blobby-modal
A React modal library with gooey blob animations, morph transitions, and asymmetric squish effects. Built with Framer Motion.
Features
- Gooey blob animation — SVG filter creates organic, liquid blob edges during open/close
- Morph transition — modal expands directly from any trigger element and collapses back
- Asymmetric squish — spring-driven scaleX/scaleY deformation for a realistic blob feel
- Fully responsive — adapts to any screen size with automatic width clamping
- Customizable springs — tune stiffness, damping, and mass or disable springs entirely
- Backdrop blur — optional frosted glass effect behind the modal
- Live updates — update modal content while it's open
- TypeScript — full type definitions included
- Lightweight — ~14 kB published, zero runtime dependencies beyond peer deps
Installation
npm install blobby-modalPeer dependencies (must be installed separately):
npm install react react-dom framer-motionRequires React 18+ and Framer Motion 11+.
Quick Start
Wrap your app with ModalProvider, then use useModal anywhere inside it.
import { ModalProvider, useModal } from 'blobby-modal'
function App() {
return (
<ModalProvider>
<MyPage />
</ModalProvider>
)
}
function MyPage() {
const modal = useModal()
return (
<button onClick={() => modal.open({
title: 'Hello!',
children: <p>This is a blobby modal.</p>,
})}>
Open Modal
</button>
)
}Morph Transition
Use useMorphTrigger to make the modal expand from a specific element and collapse back into it.
import { useMorphTrigger } from 'blobby-modal'
function MyButton() {
const morph = useMorphTrigger<HTMLButtonElement>()
return (
<button
ref={morph.ref}
onClick={() => morph.openModal({
title: 'Morphed!',
children: <p>This modal expanded from the button.</p>,
})}
>
Click me
</button>
)
}The modal captures the trigger element's position, size, border radius, and background color, then animates a smooth spring transition between the two states.
API
useModal()
Returns an object with:
| Method | Description |
|--------|-------------|
| open(options) | Open a modal with the given options |
| close() | Close the current modal |
| update(options) | Update the open modal's options (partial) |
| isOpen | boolean — whether a modal is currently open |
useMorphTrigger<T>()
Returns an object with:
| Property | Description |
|----------|-------------|
| ref | Attach to the trigger element (ref={morph.ref}) |
| openModal(options) | Open a modal that morphs from the trigger element |
ModalOptions
All options you can pass to open() or openModal():
Content
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| title | string | — | Modal heading text |
| children | ReactNode | — | Body content |
Buttons
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| primaryLabel | string | "OK" | Primary button label |
| onPrimary | () => void | — | Primary button callback |
| secondaryLabel | string | — | Secondary button label (shows button when set) |
| onSecondary | () => void | — | Secondary button callback |
| onClose | () => void | — | Called on dismiss (close button, ESC, backdrop click) |
Layout
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| width | number | 480 | Modal width in px (auto-clamped to viewport) |
| borderRadius | number | 20 | Corner radius in px |
Colors
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| backgroundColor | string | "#ffffff" | Modal background |
| titleColor | string | "#111111" | Title text color |
| bodyColor | string | "#555555" | Body text color |
| primaryButtonBg | string | titleColor | Primary button background |
| primaryButtonTextColor | string | backgroundColor | Primary button text |
Animation
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| spring | boolean \| object | true | true = default spring, false = ease curve, { stiffness, damping, mass } = custom |
| gooeyBlur | number | 10 | Gooey SVG filter blur radius |
| gooeyIntensity | number | 20 | Gooey SVG filter intensity |
Backdrop
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| backdropBlur | number | 0 | Backdrop blur in px |
| backdropColor | string | "rgba(0,0,0,0.4)" | Backdrop overlay color |
Behaviour
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| showCloseButton | boolean | true | Show/hide the close button |
| hideHeader | boolean | false | Hide built-in header |
| hideFooter | boolean | false | Hide built-in footer |
Custom Styles
| Prop | Type | Description |
|------|------|-------------|
| style | CSSProperties | Custom styles on modal card |
| primaryButtonStyle | CSSProperties | Custom styles on primary button |
| secondaryButtonStyle | CSSProperties | Custom styles on secondary button |
Examples
Confirm Dialog
modal.open({
title: 'Delete item?',
children: <p>This action cannot be undone.</p>,
primaryLabel: 'Delete',
secondaryLabel: 'Cancel',
primaryButtonBg: '#dc2626',
})Custom Spring
modal.open({
title: 'Bouncy',
children: <p>Extra bouncy spring animation.</p>,
spring: { stiffness: 200, damping: 10, mass: 1 },
})Backdrop Blur
modal.open({
title: 'Frosted',
children: <p>Blurred backdrop behind the modal.</p>,
backdropBlur: 8,
})Custom Content (No Header/Footer)
modal.open({
hideHeader: true,
hideFooter: true,
children: <MyCustomForm onClose={() => modal.close()} />,
})Update While Open
modal.open({
title: 'Loading...',
showCloseButton: false,
})
// Later...
modal.update({
title: 'Done!',
showCloseButton: true,
primaryLabel: 'Great',
})How It Works
Gooey mode uses an SVG filter (feGaussianBlur + feColorMatrix) applied during the scale animation to create organic blob edges. The filter is active during expansion and collapse, then removed when the modal is at rest.
Morph mode captures the trigger element's bounding rect and computed styles, then animates position, size, border radius, and background color from the trigger to the modal's final state using a spring.
Squish effect adds asymmetric scaleX/scaleY deformation after the main animation completes, using Math.sin(v * Math.PI) as an intensity curve. The spring overshoot creates natural wobble.
License
MIT
