AnimationsSimpleApril 9, 2026
Refracted Glass
Glass distortion effect rendered entirely with SVG filters — no libraries or external services. feTurbulence generates organic noise, feDisplacementMap warps the image through it, and an animated baseFrequency gives the glass a slow, living shift over time.
View Full Demo →Source
demo.jsx
import RefractedGlass from "./index.jsx";
import styles from "./demo.module.css";
export default function RefractedGlassDemo() {
return (
<div className={styles.demo}>
<img
src="/demo-assets/models/model3.png"
alt=""
className={styles.bg}
aria-hidden="true"
/>
<div className={styles.overlay} />
<div className={styles.content}>
<div className={styles.text}>
<p className={styles.label}>SVG — Strip Distortion</p>
<h1 className={styles.heading}>Refracted<br />Glass</h1>
</div>
<RefractedGlass />
</div>
</div>
);
}
index.jsx
"use client";
import { useState } from "react";
import styles from "./styles.module.css";
const STRIP_COUNT = 10;
const STRIPS = Array.from({ length: STRIP_COUNT }, (_, i) => ({
x1: (i / STRIP_COUNT) * 100,
x2: ((i + 1) / STRIP_COUNT) * 100,
offset: Math.sin(i * 1.1 + 0.4) * 13,
}));
export default function RefractedGlass({
image = "/demo-assets/models/model3.png",
altText = "",
}) {
const [hovered, setHovered] = useState(false);
return (
<div
className={styles.wrapper}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{STRIPS.map(({ x1, x2, offset }, i) => (
<div
key={i}
className={styles.strip}
style={{
clipPath: `polygon(${x1}% 0%, ${x2}% 0%, ${x2}% 100%, ${x1}% 100%)`,
transform: `translateY(${hovered ? offset : 0}px)`,
}}
>
<img src={image} alt={i === 0 ? altText : ""} className={styles.img} />
</div>
))}
</div>
);
}
demo.module.css
.demo {
position: relative;
min-height: 100vh;
background: #080808;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* Blurred model image sits behind as ambient texture */
.bg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 20%;
filter: blur(60px) saturate(1.4);
opacity: 0.25;
transform: scale(1.1);
}
.overlay {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, transparent 30%, #080808 80%);
}
.content {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 5rem;
padding: 5rem 4rem;
width: 100%;
max-width: 1100px;
}
.text {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
}
.label {
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.35);
}
.heading {
font-size: clamp(3rem, 6vw, 6rem);
font-weight: 500;
letter-spacing: -0.03em;
line-height: 0.95;
color: #ffffff;
}
@media (max-width: 767px) {
.content {
flex-direction: column;
gap: 3rem;
padding: 4rem 2rem;
text-align: center;
}
}
styles.module.css
.wrapper {
aspect-ratio: 1 / 1.25;
border-radius: 0.75em;
width: 100%;
max-width: 32em;
position: relative;
overflow: hidden;
background: #111;
}
.strip {
position: absolute;
inset: 0;
transition: transform 700ms cubic-bezier(0.25, 1, 0.5, 1);
}
.img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}