Liquid Button
A physics-based liquid glass button with refractive bezel effects, specular highlights, and smooth interaction animations.
Preview
import { LiquidButton } from '@/components/ui/liquid-button';
export default function Demo() {
return (
<LiquidButton label="Click me" className="px-8 py-3 text-base font-medium" />
);
}Installation
Install the dependency
The LiquidButton component requires the @avenra/liquid-glass package.
npm install @avenra/liquid-glasspnpm add @avenra/liquid-glassyarn add @avenra/liquid-glassbun add @avenra/liquid-glassAdd the component via shadcn CLI
Run the following command to add LiquidButton directly to your project:
npx shadcn@latest add https://avenra.online/r/liquid-button.jsonpnpm dlx shadcn@latest add https://avenra.online/r/liquid-button.jsonyarn dlx shadcn@latest add https://avenra.online/r/liquid-button.jsonbunx shadcn@latest add https://avenra.online/r/liquid-button.jsonThis will place the component at components/ui/liquid-button.tsx.
Manual installation
If you prefer to copy the file manually, create components/ui/liquid-button.tsx with the following content:
'use client';
import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
import {
createLiquidButton,
LiquidButtonEventMap,
LiquidButtonHandle,
LiquidButtonOptions,
} from '@avenra/liquid-glass';
type LiquidButtonEvents = {
[K in keyof LiquidButtonEventMap]?: (event: LiquidButtonEventMap[K]) => void;
};
export interface LiquidButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
label: string;
options?: LiquidButtonOptions;
events?: LiquidButtonEvents;
}
export const LiquidButton = forwardRef<HTMLButtonElement, LiquidButtonProps>(
({ label, options, events, ...props }, ref) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const handleRef = useRef<LiquidButtonHandle | null>(null);
useImperativeHandle(ref, () => buttonRef.current as HTMLButtonElement);
useEffect(() => {
if (!buttonRef.current) return;
handleRef.current = createLiquidButton(buttonRef.current, { label, ...options });
return () => {
handleRef.current?.destroy();
handleRef.current = null;
};
}, []);
useEffect(() => {
handleRef.current?.setLabel(label);
}, [label]);
useEffect(() => {
if (!handleRef.current || !events) return;
(Object.keys(events) as (keyof LiquidButtonEventMap)[]).forEach((event) => {
const handler = events[event];
if (handler) {
handleRef.current?.on(
event,
handler as (e: LiquidButtonEventMap[typeof event]) => void,
);
}
});
return () => {
if (!handleRef.current || !events) return;
(Object.keys(events) as (keyof LiquidButtonEventMap)[]).forEach((event) => {
handleRef.current?.off?.(event);
});
};
}, [events]);
return <button ref={buttonRef} {...props} />;
},
);
LiquidButton.displayName = 'LiquidButton';Usage
Import and use the component anywhere in your app:
import { LiquidButton } from '@/components/ui/liquid-button';<LiquidButton label="Get Started" />Examples
Default
The simplest usage — a button with a label and the glass effect applied automatically.
<LiquidButton label="Get Started" />Custom Refractive Index
Adjust the index of refraction to make the glass effect more or less pronounced. Values close to 1.0 are nearly invisible; higher values (e.g. 2.0) produce an intense fish-eye distortion.
{/* Subtle refraction */}
<LiquidButton label="Subtle" options={{ refractiveIndex: 1.2 }} />
{/* Default glass-like refraction */}
<LiquidButton label="Default" options={{ refractiveIndex: 1.5 }} />
{/* Strong distortion */}
<LiquidButton label="Strong" options={{ refractiveIndex: 2.0 }} />Bezel Width
The bezelWidth option controls the width of the refractive rim around the button edge.
<LiquidButton label="Thin Bezel" options={{ bezelWidth: 8 }} />
<LiquidButton label="Default Bezel" options={{ bezelWidth: 20 }} />
<LiquidButton label="Thick Bezel" options={{ bezelWidth: 40 }} />Surface Profile
The profile option changes the height-map of the bezel, altering how light refracts across it.
<LiquidButton label="convexSquircle" options={{ profile: 'convexSquircle' }} />
<LiquidButton label="flat" options={{ profile: 'flat' }} />
<LiquidButton label="concave" options={{ profile: 'concave' }} />Specular Highlight Intensity
The specularSlope option (0–1) controls how bright the specular glint on the glass surface is.
<LiquidButton label="No Glint" options={{ specularSlope: 0 }} />
<LiquidButton label="Default Glint" options={{ specularSlope: 0.8 }} />
<LiquidButton label="Full Glint" options={{ specularSlope: 1 }} />Handling Events
Pass typed mouse event handlers via the events prop. onClick is intentionally omitted from the component's props — use events.click instead for full type safety against the underlying engine's event system.
<LiquidButton
label="Click me"
events={{
click: (e: MouseEvent) => console.log('clicked', e),
mouseenter: (e: MouseEvent) => console.log('entered', e),
mouseleave: (e: MouseEvent) => console.log('left', e),
}}
/>Forwarded Ref
LiquidButton forwards its ref to the underlying <button> element, letting you call DOM methods imperatively.
'use client';
import { useRef } from 'react';
import { LiquidButton } from '@/components/ui/liquid-button';
export default function Example() {
const btnRef = useRef<HTMLButtonElement>(null);
return (
<LiquidButton
ref={btnRef}
label="Focus me"
events={{
click: () => btnRef.current?.blur(),
}}
/>
);
}Dynamic Label
The label syncs reactively — update it via state and the glass button re-renders the text without destroying the effect instance.
'use client';
import { useState } from 'react';
import { LiquidButton } from '@/components/ui/liquid-button';
export default function Example() {
const [count, setCount] = useState(0);
return (
<LiquidButton
label={`Clicked ${count} times`}
events={{ click: () => setCount((c) => c + 1) }}
/>
);
}API Reference
LiquidButtonProps
Prop
Type
All standard HTMLButtonElement attributes (className, disabled, style, aria-*, etc.) are forwarded via rest props. onClick is intentionally excluded — use events.click instead.
LiquidButtonOptions
LiquidButtonOptions extends GlassOptions with one additional field.
Prop
Type
LiquidButtonEventMap
Prop
Type