@rowix/ink-reveal-image
v1.0.0
Published
Beautiful ink blob reveal animation for lazy-loaded images in React. Canvas-based with smooth organic ink splatter effects.
Maintainers
Readme
🖋️ Ink Reveal Image
Beautiful ink blob reveal animation for lazy-loaded images in React
A React component that reveals images with an organic ink splatter effect using canvas-based animation. Perfect for galleries, portfolios, and any image-heavy application.
Demo • Installation • Usage • Props • Examples
✨ Features
- 🎨 Organic Ink Blobs — Natural, randomized blob shapes that feel hand-painted
- 🚀 Lazy Loading — Automatically triggers when images enter the viewport
- ⚡ Canvas-Powered — Smooth 60fps animations with high-DPI support
- 🎛️ Highly Customizable — 35+ props to fine-tune every aspect
- 📦 Tiny Bundle — Tree-shakeable with zero dependencies
- 🔧 TypeScript Ready — Full type definitions included
- ♿ Accessible — ARIA labels and semantic HTML
📦 Installation
# npm
npm install @rowix/ink-reveal-image
# yarn
yarn add @rowix/ink-reveal-image
# pnpm
pnpm add @rowix/ink-reveal-image🚀 Quick Start
import { InkRevealImage } from '@rowix/ink-reveal-image';
function App() {
return (
<div style={{ width: 400, height: 300 }}>
<InkRevealImage
src="https://example.com/image.jpg"
alt="Beautiful landscape"
/>
</div>
);
}📖 Usage
Basic Usage
<InkRevealImage
src="/photo.jpg"
alt="My photo"
/>With Placeholder
<InkRevealImage
src="/photo-large.jpg"
placeholder="/photo-tiny.jpg"
alt="My photo"
placeholderBlur={25}
/>Custom Animation
<InkRevealImage
src="/photo.jpg"
alt="My photo"
duration={3500}
easing="easeOutElastic"
delay={200}
blobCount={20}
pattern="explosion"
/>With Callbacks
<InkRevealImage
src="/photo.jpg"
alt="My photo"
onLoadStart={() => console.log('Loading...')}
onLoad={() => console.log('Loaded!')}
onRevealStart={() => console.log('Animating...')}
onProgress={(p) => console.log(`${Math.round(p * 100)}%`)}
onRevealComplete={() => console.log('Done!')}
onError={(e) => console.error('Failed:', e)}
/>⚙️ Props
Core Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| src | string | required | Image source URL |
| alt | string | '' | Alt text for accessibility |
| placeholder | string | — | Low-res placeholder image URL |
| className | string | '' | Additional CSS class for container |
| style | CSSProperties | — | Inline styles for container |
Animation Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| duration | number | 2500 | Animation duration in milliseconds |
| easing | EasingFunction | 'easeOut' | Easing function for blob growth |
| delay | number | 0 | Delay before animation starts (ms) |
| triggerOnViewport | boolean | true | Start animation when in viewport |
| viewportThreshold | number | 0.1 | Viewport visibility threshold (0-1) |
| viewportRootMargin | string | '0px' | IntersectionObserver root margin |
Available Easing Functions
type EasingFunction =
| 'linear' // Constant speed
| 'easeOut' // Slow down at end (default)
| 'easeOutStrong' // More dramatic slowdown
| 'easeInOut' // Slow start and end
| 'easeOutElastic'// Bouncy overshoot
| 'easeOutBounce' // Multiple bouncesBlob Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| blobCount | number | 14 | Number of ink blobs |
| blobSizeMin | number | 0.12 | Minimum blob size (0-1 fraction) |
| blobSizeMax | number | 0.30 | Maximum blob size (0-1 fraction) |
| blobRoughness | number | 0.3 | Edge roughness/noise (0-1) |
| blobComplexity | number | 60 | Points defining blob shape |
| blobStagger | number | 0.15 | Max delay stagger between blobs |
| pattern | RevealPattern | 'random' | Preset pattern for blob placement |
| customBlobs | Array | — | Custom blob positions |
Available Patterns
type RevealPattern =
| 'random' // Random scattered (default)
| 'center' // Radiates from center
| 'corners' // Starts from all corners
| 'spiral' // Spiral from center outward
| 'wave' // Horizontal wave pattern
| 'explosion' // All from center, random delaysCustom Blobs
<InkRevealImage
src="/photo.jpg"
customBlobs={[
{ x: 0.2, y: 0.2, size: 0.25, delay: 0 },
{ x: 0.8, y: 0.3, size: 0.20, delay: 0.1 },
{ x: 0.5, y: 0.7, size: 0.30, delay: 0.05 },
]}
/>Visual Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| objectFit | ObjectFit | 'cover' | CSS object-fit for image |
| objectPosition | string | 'center' | CSS object-position |
| backgroundColor | string | '#e5e7eb' | Background color while loading |
| placeholderBlur | number | 20 | Blur amount for placeholder (px) |
| fadeInStart | number | 0.7 | When to start final fade-in (0-1) |
| showLoader | boolean | false | Show loading spinner |
| loader | ReactNode | — | Custom loader component |
| borderRadius | string \| number | — | Border radius for container |
Callback Props
| Prop | Type | Description |
|------|------|-------------|
| onLoadStart | () => void | Called when image starts loading |
| onLoad | () => void | Called when image finishes loading |
| onRevealStart | () => void | Called when animation starts |
| onProgress | (progress: number) => void | Progress updates (0-1) |
| onRevealComplete | () => void | Called when animation completes |
| onError | (error: Event) => void | Called if image fails to load |
Advanced Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| highDPI | boolean | true | Enable retina canvas rendering |
| disableAnimation | boolean | false | Skip animation, show immediately |
| crossOrigin | string | 'anonymous' | CORS setting for images |
| paused | boolean | false | Pause the animation |
| animationKey | string \| number | — | Change to re-trigger animation |
| ariaLabel | string | — | ARIA label override |
| ariaDescribedBy | string | — | ARIA describedby attribute |
💡 Examples
Gallery Grid
const images = [
{ src: '/img1.jpg', alt: 'Image 1' },
{ src: '/img2.jpg', alt: 'Image 2' },
{ src: '/img3.jpg', alt: 'Image 3' },
];
function Gallery() {
return (
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 16
}}>
{images.map((img, i) => (
<div key={i} style={{ aspectRatio: '4/3' }}>
<InkRevealImage
src={img.src}
alt={img.alt}
duration={2000 + i * 200}
pattern="center"
borderRadius={8}
/>
</div>
))}
</div>
);
}Hero Image
function HeroSection() {
return (
<div style={{ height: '100vh', width: '100%' }}>
<InkRevealImage
src="/hero-4k.jpg"
placeholder="/hero-tiny.jpg"
alt="Epic mountain landscape"
duration={4000}
easing="easeOutStrong"
blobCount={25}
blobSizeMax={0.4}
pattern="explosion"
onRevealComplete={() => {
// Trigger other animations
}}
/>
</div>
);
}Controlled Animation
function ControlledImage() {
const [key, setKey] = useState(0);
const [paused, setPaused] = useState(false);
return (
<>
<div style={{ width: 400, height: 300 }}>
<InkRevealImage
src="/photo.jpg"
animationKey={key}
paused={paused}
duration={5000}
/>
</div>
<button onClick={() => setKey(k => k + 1)}>Replay</button>
<button onClick={() => setPaused(p => !p)}>
{paused ? 'Resume' : 'Pause'}
</button>
</>
);
}Custom Loader
<InkRevealImage
src="/photo.jpg"
showLoader
loader={
<div className="my-spinner">
<span>Loading...</span>
</div>
}
/>With Progress Bar
function ImageWithProgress() {
const [progress, setProgress] = useState(0);
return (
<div>
<div style={{ width: 400, height: 300 }}>
<InkRevealImage
src="/photo.jpg"
onProgress={setProgress}
/>
</div>
<div className="progress-bar">
<div style={{ width: `${progress * 100}%` }} />
</div>
</div>
);
}Disable Animation (SSR/Reduced Motion)
function AccessibleImage() {
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
return (
<InkRevealImage
src="/photo.jpg"
alt="Description"
disableAnimation={prefersReducedMotion}
/>
);
}🎨 Styling
The component renders a container div with the class ink-reveal-image. You can style it with CSS:
.ink-reveal-image {
/* Your styles */
}
/* Container must have dimensions */
.my-image-wrapper {
width: 100%;
aspect-ratio: 16/9;
}Important: The container element must have defined dimensions (width and height). The component fills its container at 100% width and height.
🔧 TypeScript
Full TypeScript support is included. Import types as needed:
import {
InkRevealImage,
InkRevealImageProps,
EasingFunction,
RevealPattern,
ObjectFit,
InkBlob
} from '@rowix/ink-reveal-image';📱 Browser Support
- Chrome 64+
- Firefox 69+
- Safari 12+
- Edge 79+
Requires IntersectionObserver and Canvas 2D support.
⚡ Performance Tips
- Use placeholders — Small blurred images prevent layout shift
- Optimize images — Use appropriate sizes and formats (WebP)
- Limit blob count — 10-20 blobs is usually enough
- Use
viewportThreshold— Trigger before fully visible for smoother UX - Consider
disableAnimation— Respect user's reduced motion preference
📄 License
MIT © rowix
Made with ❤️ for beautiful web experiences
