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-glasspnpm add @avenra/liquid-glassyarn add @avenra/liquid-glassbun add @avenra/liquid-glassAdd the component via shadcn CLI
npx shadcn@latest add https://avenra.online/r/liquid-switch.jsonpnpm dlx shadcn@latest add https://avenra.online/r/liquid-switch.jsonyarn dlx shadcn@latest add https://avenra.online/r/liquid-switch.jsonbunx shadcn@latest add https://avenra.online/r/liquid-switch.jsonThis places the component at components/ui/liquid-switch.tsx.
Manual installation
Create components/ui/liquid-switch.tsx with the following content:
'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 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.
'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.
<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 — 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
LiquidButtonandLiquidSlider, the ref onLiquidSwitchpoints to theLiquidSwitchHandleinstance — not the DOM element. UseswitchRef.current?.elementto 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