Avenra
Liquid GlassComponents
Liquid GlassComponents

Liquid Switch

A physics-based liquid glass toggle switch with refractive bezel effects, specular highlights, and fluid on/off animations. Supports both controlled and uncontrolled usage.

Preview

Off

'use client';

import { useState } from 'react';
import { LiquidSwitch } from '@/components/ui/liquid-switch';

export default function Demo() {
  const [checked, setChecked] = useState(false);

  return (
    <LiquidSwitch
      checked={checked}
      onCheckedChange={setChecked}
    />
  );
}

Installation

Install the dependency

LiquidSwitch is powered by 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

npx shadcn@latest add https://avenra.online/r/liquid-switch.json
pnpm dlx shadcn@latest add https://avenra.online/r/liquid-switch.json
yarn dlx shadcn@latest add https://avenra.online/r/liquid-switch.json
bunx shadcn@latest add https://avenra.online/r/liquid-switch.json

This places the component at components/ui/liquid-switch.tsx.

Manual installation

Create components/ui/liquid-switch.tsx with the following content:

components/ui/liquid-switch.tsx
'use client';

import * as React from 'react';
import {
  createLiquidSwitch,
  type LiquidSwitchHandle,
  type LiquidSwitchOptions,
  type LiquidSwitchEventMap,
} from '@avenra/liquid-glass';

export interface LiquidSwitchProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
  /** Controlled checked state. */
  checked?: boolean;
  /** Initial state for uncontrolled usage. */
  defaultChecked?: boolean;
  /** Called when the checked state changes. */
  onCheckedChange?: (checked: boolean) => void;
  /** Glass effect configuration. */
  options?: LiquidSwitchOptions;
}

export const LiquidSwitch = React.forwardRef<LiquidSwitchHandle | null, LiquidSwitchProps>(
  ({ checked, defaultChecked = false, onCheckedChange, options, className, ...props }, ref) => {
    const containerRef = React.useRef<HTMLDivElement>(null);
    const instanceRef = React.useRef<LiquidSwitchHandle | null>(null);

    const isControlled = checked !== undefined;

    React.useEffect(() => {
      if (!containerRef.current) return;
      if (instanceRef.current) return;
      if (containerRef.current.children.length > 0) return;

      const instance = createLiquidSwitch(containerRef.current, {
        checked: checked ?? defaultChecked,
        ...options,
      });

      instanceRef.current = instance;

      const handleChange = (payload: LiquidSwitchEventMap['change']) => {
        onCheckedChange?.(payload.checked);
      };

      instance.on('change', handleChange);

      return () => {
        instance.off('change', handleChange);
        instance.destroy();
        instanceRef.current = null;
      };
    }, []);

    React.useEffect(() => {
      if (!instanceRef.current || !isControlled) return;
      instanceRef.current.checked = checked!;
    }, [checked, isControlled]);

    React.useImperativeHandle(ref, () => instanceRef.current!, []);

    return <div ref={containerRef} className={className} {...props} />;
  },
);

LiquidSwitch.displayName = 'LiquidSwitch';

Usage

import { LiquidSwitch } from '@/components/ui/liquid-switch';

Controlled — you own the state:

'use client';

import { useState } from 'react';
import { LiquidSwitch } from '@/components/ui/liquid-switch';

export default function Example() {
  const [checked, setChecked] = useState(false);

  return (
    <LiquidSwitch
      checked={checked}
      onCheckedChange={setChecked}
    />
  );
}

Uncontrolled — the switch manages its own state internally:

<LiquidSwitch defaultChecked={true} />

Examples

Uncontrolled with defaultChecked

Use defaultChecked when you don't need to track the switch state in React — the component manages its own toggle internally.

starts off
starts on
{/* starts unchecked */}
<LiquidSwitch defaultChecked={false} />

{/* starts checked */}
<LiquidSwitch defaultChecked={true} />

Controlled with external state

Drive the switch from outside using checked and onCheckedChange. The glass thumb position syncs reactively whenever checked changes.

State: false

'use client';

import { useState } from 'react';
import { LiquidSwitch } from '@/components/ui/liquid-switch';

export default function Example() {
  const [checked, setChecked] = useState(false);

  return (
    <div className="flex flex-col gap-4">
      <LiquidSwitch checked={checked} onCheckedChange={setChecked} />
      <div className="flex gap-2">
        <button onClick={() => setChecked(false)}>Force Off</button>
        <button onClick={() => setChecked(true)}>Force On</button>
      </div>
      <p>State: {String(checked)}</p>
    </div>
  );
}

With label

A common pattern — pair LiquidSwitch with a label in a settings list. Each switch is independently controlled.

Push notifications
Dark mode
Analytics
'use client';

import { useState } from 'react';
import { LiquidSwitch } from '@/components/ui/liquid-switch';

export default function Example() {
  const [notifications, setNotifications] = useState(true);
  const [darkMode, setDarkMode]           = useState(false);
  const [analytics, setAnalytics]         = useState(true);

  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center justify-between">
        <span className="text-sm font-medium">Push notifications</span>
        <LiquidSwitch checked={notifications} onCheckedChange={setNotifications} />
      </div>
      <div className="flex items-center justify-between">
        <span className="text-sm font-medium">Dark mode</span>
        <LiquidSwitch checked={darkMode} onCheckedChange={setDarkMode} />
      </div>
      <div className="flex items-center justify-between">
        <span className="text-sm font-medium">Analytics</span>
        <LiquidSwitch checked={analytics} onCheckedChange={setAnalytics} />
      </div>
    </div>
  );
}

Listening to change events

onCheckedChange fires every time the checked state flips — whether the user clicked or the state was set programmatically.

toggle the switch…
<LiquidSwitch
  defaultChecked={false}
  onCheckedChange={(checked) => {
    console.log('switch is now:', checked);
    // e.g. save to database, update a store, etc.
  }}
/>

Glass appearance options

All GlassOptions are available via the options prop — controlling refraction intensity, bezel width, specular highlight, and more.

Subtle
Default
Intense
{/* Subtle — low refraction, soft highlight */}
<LiquidSwitch
  checked={checked}
  onCheckedChange={setChecked}
  options={{ refractiveIndex: 1.2, specularSlope: 0.3 }}
/>

{/* Default */}
<LiquidSwitch checked={checked} onCheckedChange={setChecked} />

{/* Intense — strong distortion, wide bezel, full glint */}
<LiquidSwitch
  checked={checked}
  onCheckedChange={setChecked}
  options={{ refractiveIndex: 2.0, specularSlope: 1, bezelWidth: 32 }}
/>

Imperative API via ref

LiquidSwitch forwards its ref to the underlying LiquidSwitchHandle instance, giving you access to toggle() and checked imperatively.

'use client';

import { useRef } from 'react';
import { LiquidSwitch } from '@/components/ui/liquid-switch';
import type { LiquidSwitchHandle } from '@avenra/liquid-glass';

export default function Example() {
  const switchRef = useRef<LiquidSwitchHandle | null>(null);

  return (
    <div className="flex flex-col gap-4">
      <LiquidSwitch ref={switchRef} defaultChecked={false} />
      <button onClick={() => switchRef.current?.toggle()}>
        Toggle imperatively
      </button>
      <button onClick={() => console.log(switchRef.current?.checked)}>
        Log current state
      </button>
    </div>
  );
}

Note: Unlike LiquidButton and LiquidSlider, the ref on LiquidSwitch points to the LiquidSwitchHandle instance — not the DOM element. Use switchRef.current?.element to access the underlying DOM node.


API Reference

LiquidSwitchProps

Prop

Type

All standard HTMLDivElement attributes (className, style, aria-*, etc.) are forwarded via rest props. onChange is intentionally excluded — use onCheckedChange instead.


LiquidSwitchOptions

LiquidSwitchOptions extends GlassOptions with one additional field.

Prop

Type


LiquidSwitchHandle

The imperative handle exposed via ref. Gives direct access to the underlying engine instance.

Prop

Type


LiquidSwitchEventMap

Prop

Type

On this page