Skip to main content

Creating a Dissolve Effect Using SVG

Written by on .

engineering
javascript
react

  1. Understanding SVG Filters
    1. The Building Blocks of the Dissolve Effect
      1. Creating the DissolveFilter Component
        1. Creating the useDissolveEffect Hook
          1. Creating the DeleteButton Component
          2. Demo

            I saw this button on X and I was mesmerized by it:

            Dissolve Effect
            Dissolve effect as seen on https://refero.design/

            I felt compelled to recreate it. However, I wasn't sure where to start. Luckily, Mike provided a brief explanation of the effect on X:

            Under the hood: I used SVG filters with feTurbulence for random noise and feDisplacementMap to create that smooth dissolve. feDisplacementMap warps the image based on noise intensity, shifting pixels horizontally and vertically in unique ways each time. By resetting the noise seed, every delete effect gets its own randomized distortion, giving a fresh look every time

            And after some tinkering, I got to what looks pretty close to the original effect:

            Click on the "Delete" button to see the effect in action.

            In this tutorial, I'll walk you through how to create this dissolve effect using SVG and React.

            Understanding SVG Filters

            SVG filters are powerful tools that allow you to apply advanced visual effects to SVG elements. They can manipulate graphics in various ways, such as blurring, distorting, or adding textures. In our case, we'll use SVG filters to create a dissolve effect by distorting the button's appearance and gradually reducing its opacity.

            The Building Blocks of the Dissolve Effect

            To create the dissolve effect, we'll use the following SVG filter primitives:

            • <feTurbulence>: Generates an image using the Perlin turbulence function. It's useful for creating cloud-like or noisy textures.
            • <feDisplacementMap>: Uses the pixel values from one image to spatially displace the pixels in another image, creating a distortion effect.
            • <feComponentTransfer>: Allows you to manipulate pixel values via component-wise transfer functions.
            • <feMerge>: Combines multiple filter effects into one.

            By combining these primitives, we can create a noise pattern that distorts the button, giving the illusion of it dissolving away.

            There is too much here to go deeper than this. If you are interested in learning more about SVG filters, I recommend these two articles: article 1, article 2.

            Creating the DissolveFilter Component

            The DissolveFilter component defines the SVG filter that creates the dissolve effect.

            I used Chrome's DevTools "Break on subtree modifications" feature to inspect how the effect is rendered on https://refero.design/. It showed me an SVG that is being added to create the effect. The below code recreates that logic using React components and hooks.

            import React, { forwardRef } from 'react'; const DissolveFilter = forwardRef((props, ref) => { const { height, seed, width } = props; return ( <svg overflow="visible" style={{ position: 'absolute' }} viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg" > <defs> <filter id="dissolve-filter" width="400%" height="400%" x="-200%" y="-200%" colorInterpolationFilters="sRGB" overflow="visible" > {/* Large Noise */} <feTurbulence type="fractalNoise" baseFrequency="0.015" numOctaves="1" seed={seed} result="bigNoise" /> {/* Adjust Noise Levels */} <feComponentTransfer in="bigNoise" result="bigNoiseAdjusted"> <feFuncR type="linear" slope="2" intercept="-0.4" /> <feFuncG type="linear" slope="2" intercept="-0.4" /> </feComponentTransfer> {/* Fine Noise */} <feTurbulence type="fractalNoise" baseFrequency="1" numOctaves="2" result="fineNoise" /> {/* Combine Noises */} <feMerge result="combinedNoise"> <feMergeNode in="bigNoiseAdjusted" /> <feMergeNode in="fineNoise" /> </feMerge> {/* Displacement Map */} <feDisplacementMap in="SourceGraphic" in2="combinedNoise" scale="0" xChannelSelector="R" yChannelSelector="G" ref={ref} /> </filter> </defs> </svg> ); });

            Explanation:

            • <feTurbulence> (bigNoise and fineNoise): We create two turbulence effects with different frequencies to simulate large and fine noise patterns.
            • <feComponentTransfer>: Adjusts the noise levels to get the desired effect.
            • <feMerge>: Combines the large and fine noise into one.
            • <feDisplacementMap>: Applies the combined noise to distort the source graphic (our button). The scale attribute controls the intensity of the displacement.

            Creating the useDissolveEffect Hook

            This hook manages the animation state and updates the filter's displacement scale over time to create the animation.

            import { useEffect, useRef, useState } from 'react'; const useDissolveEffect = (elementRef) => { const filterRef = useRef(null); const [state, setState] = useState('idle'); useEffect(() => { if (!elementRef.current || state !== 'dissolving') return; const startTime = performance.now(); const ANIMATION_DURATION = 600; // Animation duration in ms const FADE_START_POINT = 0.3; // When to start fading out const animate = (currentTime) => { const elapsed = currentTime - startTime; const progress = Math.min(elapsed / ANIMATION_DURATION, 1); // Calculate displacement scale const displacementScale = (1 - Math.cos((progress * Math.PI) / 1.5)) * 300; // Calculate opacity const opacityProgress = Math.max(0, (progress - FADE_START_POINT) / (1 - FADE_START_POINT)); const opacity = 1 - opacityProgress; // Update filter scale if (filterRef.current) { filterRef.current.setAttribute('scale', displacementScale.toString()); } // Update element opacity if (elementRef.current) { elementRef.current.style.opacity = opacity.toString(); } if (progress < 1) { requestAnimationFrame(animate); } else { setState('dissolved'); } }; requestAnimationFrame(animate); }, [elementRef, state]); const dissolve = () => { if (state === 'idle') { setState('dissolving'); } }; const DissolveEffect = () => { if (state === 'dissolving') { const { offsetWidth: width, offsetHeight: height } = elementRef.current || {}; if (!width || !height) return null; return ( <DissolveFilter width={width} height={height} seed={Math.floor(Math.random() * 1000)} ref={filterRef} /> ); } return null; }; return { dissolve, DissolveEffect, styles: { filter: 'url(#dissolve-filter)' }, }; };

            Explanation:

            • State Management: We use a state variable to track the animation state (idle, dissolving, dissolved).
            • Animation Logic: The useEffect hook runs the animation when the state is dissolving.
            • Animation Frames: We use requestAnimationFrame to create smooth animations.
            • Displacement Scale: We calculate a displacementScale value that increases over time, intensifying the distortion.
            • Opacity: We reduce the opacity after a certain point to fade out the element.
            • dissolve Function: Initiates the dissolve effect.
            • DissolveEffect Component: Renders the DissolveFilter component when the effect is active.

            Creating the DeleteButton Component

            This component renders the button and applies the dissolve effect when clicked.

            import React, { useRef } from 'react'; import useDissolveEffect from './useDissolveEffect'; const DeleteButton = () => { const buttonRef = useRef(null); const { dissolve, DissolveEffect, styles } = useDissolveEffect(buttonRef); return ( <> <div ref={buttonRef} onClick={dissolve} style={{ ...styles, padding: '16px 48px', borderRadius: '4px', background: '#3f1e23', color: '#fe4a4b', fontFamily: 'monospace', cursor: 'pointer', textAlign: 'center', userSelect: 'none', }} > Delete </div> <DissolveEffect /> </> ); }; export default DeleteButton;

            Explanation:

            • Ref Assignment: We assign a ref to the button element to manipulate it directly.
            • Event Handling: The onClick event triggers the dissolve function.
            • Styles: We apply the styles from the useDissolveEffect hook to apply the SVG filter.
            • Rendering the Effect: We include the DissolveEffect component to render the SVG filter when needed.

            Demo

            Finally, once we put it all together, we get this:

            You can find the full code in the GitHub gist. I am by no means an expert in React, so I welcome any feedback on how to improve this code.

            The next step is to add this effect to Glama. I want to use the dissolve effect when deleting a chat session. A very cool eye-catching effect that I think we will see more in the future.

            Shout out to Mike Bespalov for coming up with this effect. I was heavil inspired by his work on https://refero.design/ when creating this button.