Avenra
Liquid GlassComponents
Liquid GlassComponents

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-glass
pnpm add @avenra/liquid-glass
yarn add @avenra/liquid-glass
bun add @avenra/liquid-glass

Add 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.json
pnpm dlx shadcn@latest add https://avenra.online/r/liquid-button.json
yarn dlx shadcn@latest add https://avenra.online/r/liquid-button.json
bunx shadcn@latest add https://avenra.online/r/liquid-button.json

This 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:

components/ui/liquid-button.tsx
'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

On this page