Text
Liquid Text
#text#liquid#canvas
implementedInfinityUI component
This component is fully implemented in InfinityUI and wired into the docs and registry flow.
Installation
Use the registry command to add the component source, and install any package dependencies if needed.
Infinity Registry
Install the component source into your project with the shadcn CLI.
npx shadcn@latest add https://infinityui-pearl.vercel.app/r/liquid-textPackage Dependencies
Install the npm packages used by this component source.
No additional npm packages required.Component Code
Copy and paste this code into your component file.
tsx
"use client";
import { useEffect, useRef } from "react";
interface LiquidTextProps {
text?: string;
fontSize?: number;
color?: string;
className?: string;
}
export function LiquidText({ text = "LIQUID", fontSize = 80, color = "#6366f1", className = "" }: LiquidTextProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animRef = useRef<number>(0);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const W = canvas.offsetWidth;
const H = canvas.offsetHeight;
canvas.width = W;
canvas.height = H;
let time = 0;
const draw = () => {
ctx.clearRect(0, 0, W, H);
time += 0.02;
// Animated liquid fill level
const fillLevel = H * 0.55 + Math.sin(time * 0.7) * H * 0.06;
// Clipping region: text shape
ctx.save();
ctx.font = `900 ${fontSize}px 'Inter', sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.beginPath();
// Draw text as clip path
ctx.font = `900 ${fontSize}px 'Inter', sans-serif`;
ctx.textBaseline = "middle";
(ctx as CanvasRenderingContext2D & { letterSpacing?: string }).letterSpacing = "2px";
ctx.fillStyle = "rgba(255,255,255,0.05)";
ctx.fillText(text, W / 2, H / 2);
ctx.clip(); // Can't clip text directly — use globalCompositeOperation approach
ctx.restore();
// Draw background text (outline)
ctx.font = `900 ${fontSize}px 'Inter', sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.strokeStyle = color + "40";
ctx.lineWidth = 2;
ctx.strokeText(text, W / 2, H / 2);
// Animated wave that fills the text
ctx.save();
ctx.globalCompositeOperation = "source-over";
// Create a clip from text
ctx.beginPath();
// We draw onto an offscreen approach via fill text trick
ctx.font = `900 ${fontSize}px 'Inter', sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = color;
ctx.fillText(text, W / 2, H / 2);
ctx.restore();
// Wave overlay clipped to text using destination-in
const offscreen = document.createElement("canvas");
offscreen.width = W;
offscreen.height = H;
const octx = offscreen.getContext("2d")!;
// Draw text
octx.font = `900 ${fontSize}px 'Inter', sans-serif`;
octx.textAlign = "center";
octx.textBaseline = "middle";
octx.fillStyle = color + "30";
octx.fillText(text, W / 2, H / 2);
// Draw liquid wave
octx.globalCompositeOperation = "source-in";
const grad = octx.createLinearGradient(0, fillLevel - 20, 0, H);
grad.addColorStop(0, color + "cc");
grad.addColorStop(1, color);
octx.fillStyle = grad;
octx.beginPath();
octx.moveTo(0, fillLevel);
for (let x = 0; x <= W; x += 4) {
const waveY = fillLevel + Math.sin((x / W) * Math.PI * 4 + time * 2) * 8;
octx.lineTo(x, waveY);
}
octx.lineTo(W, H);
octx.lineTo(0, H);
octx.closePath();
octx.fill();
ctx.drawImage(offscreen, 0, 0);
animRef.current = requestAnimationFrame(draw);
};
draw();
return () => cancelAnimationFrame(animRef.current);
}, [text, fontSize, color]);
return (
<canvas
ref={canvasRef}
className={`w-full h-full ${className}`}
style={{ display: "block" }}
aria-label={text}
/>
);
}