Distorsión líquida al apuntar
Un filtro SVG de turbulencia ondula titular e ilustración mientras el puntero está encima, y se relaja al salir.
Se derrite
Pasa el puntero sobre el titular o la imagen
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useEffect, useId, useRef, type CSSProperties } from "react";
/** Distorsión líquida al apuntar: un filtro SVG de turbulencia y desplazamiento ondula el titular y la ilustración mientras el puntero está encima, y se relaja al salir. Con movimiento reducido solo cambia de color. React + TypeScript, sin dependencias. */
export type EfectoDistorsionProps = {
title?: string;
strength?: number;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcedi-root { box-sizing: border-box; display: grid; place-items: center; gap: 22px; width: 100%; max-width: 640px; padding: 34px 20px; border-radius: 20px; background: #16120d; color: #f4ecda; font: 500 15px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; text-align: center; overflow: hidden; }
.gcedi-root *, .gcedi-root *::before, .gcedi-root *::after { box-sizing: border-box; }
.gcedi-svg { position: absolute; width: 0; height: 0; }
.gcedi-title { margin: 0; padding: .1em .2em; font: 500 clamp(40px, 9vw, 88px)/1 "Newsreader", Georgia, ui-serif, serif; letter-spacing: -.04em; filter: var(--gcedi-f); transition: color .3s; cursor: default; outline: 0; }
.gcedi-title:hover, .gcedi-title:focus-visible { color: #ff8a5c; }
.gcedi-art { width: min(260px, 70%); aspect-ratio: 16 / 9; border-radius: 14px; overflow: hidden; filter: var(--gcedi-f); cursor: default; }
.gcedi-art svg { display: block; width: 100%; height: 100%; }
.gcedi-hint { margin: 0; font-size: 12px; color: #8d846f; }
@media (prefers-reduced-motion: reduce) { .gcedi-title, .gcedi-art { filter: none !important; } }`;
export function EfectoDistorsion({ title = "Se derrite", strength = 36, className = "", style }: EfectoDistorsionProps) {
const uid = useId().replace(/[^a-zA-Z0-9]/g, "");
const fid = `gcedi-f-${uid}`;
const disp = useRef<SVGFEDisplacementMapElement>(null);
const turb = useRef<SVGFETurbulenceElement>(null);
const raf = useRef(0);
const amount = useRef(0);
const goal = useRef(0);
const t = useRef(0);
useEffect(() => () => cancelAnimationFrame(raf.current), []);
function loop() {
amount.current += (goal.current - amount.current) * 0.09;
t.current += 0.012;
disp.current?.setAttribute("scale", String(amount.current * strength));
turb.current?.setAttribute("baseFrequency", `${0.012 + Math.sin(t.current) * 0.004} ${0.03 + Math.cos(t.current * 0.8) * 0.006}`);
raf.current = goal.current > 0 || amount.current > 0.01 ? requestAnimationFrame(loop) : 0;
}
function on() { goal.current = 1; if (!raf.current) raf.current = requestAnimationFrame(loop); }
function off() { goal.current = 0; if (!raf.current) raf.current = requestAnimationFrame(loop); }
return (
<div className={`gcedi-root ${className}`} style={{ "--gcedi-f": `url(#${fid})`, ...style } as CSSProperties} onPointerEnter={on} onPointerLeave={off}>
<style>{CSS}</style>
<svg className="gcedi-svg" aria-hidden="true" focusable="false">
<filter id={fid} x="-10%" y="-20%" width="120%" height="140%">
<feTurbulence ref={turb} type="fractalNoise" baseFrequency="0.012 0.03" numOctaves="2" seed="3" result="n" />
<feDisplacementMap ref={disp} in="SourceGraphic" in2="n" scale="0" xChannelSelector="R" yChannelSelector="G" />
</filter>
</svg>
<h2 className="gcedi-title" tabIndex={0} onFocus={on} onBlur={off}>{title}</h2>
<div className="gcedi-art" aria-hidden="true">
<svg viewBox="0 0 160 90" preserveAspectRatio="xMidYMid slice">
<rect width="160" height="90" fill="#ff8a5c" />
<circle cx="110" cy="36" r="22" fill="#ffe7b0" />
<path d="M0 70 C 30 50 60 80 90 62 S 140 54 160 66 V90 H0Z" fill="#3a2a5e" />
<path d="M0 80 C 40 66 70 90 110 76 S 150 72 160 80 V90 H0Z" fill="#241a40" />
</svg>
</div>
<p className="gcedi-hint">Pasa el puntero sobre el titular o la imagen</p>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/efecto-distorsionEjemplo de uso de Distorsión líquida al apuntar
Después de copiar efecto-distorsion.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { EfectoDistorsion } from "./efecto-distorsion";
export default function Ejemplo() {
return <EfectoDistorsion title="Ejemplo" />;
}Propiedades de EfectoDistorsion
EfectoDistorsion acepta 4 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
strength? | number | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 68 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Distorsión líquida al apuntar por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Anima con requestAnimationFrame, sincronizado con el refresco de pantalla.
- Es un componente de cliente de Next.js: incluye la directiva "use client".