Avenra
Liquid GlassComponents
Liquid GlassComponents

Liquid Slider

A physics-based liquid glass range slider with refractive bezel effects, specular highlights, and smooth drag interactions.

Preview

Value: 50

'use client';

import { useState } from 'react';
import { LiquidSlider } from '@/components/ui/liquid-slider';

export default function Demo() {
  const [value, setValue] = useState(50);

  return (
    <LiquidSlider
      min={0}
      max={100}
      value={value}
      className="w-full"
      events={{ input: ({ value }) => setValue(value) }}
    />
  );
}

Installation

Install the dependency

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

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

Manual installation

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

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

import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react';
import {
  createLiquidSlider,
  LiquidSliderHandle,
  LiquidSliderEventMap,
  LiquidSliderOptions,
} from '@avenra/liquid-glass';

export interface LiquidSliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
  min: number;
  max: number;
  value: number;
  options?: LiquidSliderOptions;
  events?: {
    [K in keyof LiquidSliderEventMap]?: (payload: LiquidSliderEventMap[K]) => void;
  };
}

export const LiquidSlider = forwardRef<HTMLDivElement, LiquidSliderProps>(
  ({ min, max, value, options, events, ...props }, ref) => {
    const containerRef = useRef<HTMLDivElement>(null);
    const handleRef = useRef<LiquidSliderHandle | null>(null);

    useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);

    useEffect(() => {
      if (!containerRef.current) return;

      if (containerRef.current.children.length > 0) return; // already initialized

      handleRef.current = createLiquidSlider(containerRef.current, {
        min, max, value, blur: 0, ...options,
      });
      return () => { handleRef.current?.destroy(); handleRef.current = null; };
    }, []);

    useEffect(() => {
      if (!handleRef.current) return;
      handleRef.current.value = value;
    }, [value]);

    useEffect(() => {
      if (!handleRef.current) return;
      handleRef.current.value = Math.min(Math.max(value, min), max);
    }, [min, max]);

    useEffect(() => {
      if (!handleRef.current || !events) return;
      const handle = handleRef.current;
      for (const key in events) {
        const event = key as keyof LiquidSliderEventMap;
        const handler = events[event];
        if (handler) handle.on(event, handler as (payload: LiquidSliderEventMap[typeof event]) => void);
      }
      return () => {
        if (!handleRef.current || !events) return;
        for (const key in events) {
          const event = key as keyof LiquidSliderEventMap;
          const handler = events[event];
          if (handler) handleRef.current.off(event, handler as (payload: LiquidSliderEventMap[typeof event]) => void);
        }
      };
    }, [events]);

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

LiquidSlider.displayName = 'LiquidSlider';

Usage

import { LiquidSlider } from '@/components/ui/liquid-slider';
'use client';

import { useState } from 'react';
import { LiquidSlider } from '@/components/ui/liquid-slider';

export default function Example() {
  const [value, setValue] = useState(50);

  return (
    <LiquidSlider
      min={0}
      max={100}
      value={value}
      className="w-full"
      events={{ input: ({ value }) => setValue(value) }}
    />
  );
}

LiquidSlider is a controlled component — you must supply a value prop and update it via the events.input handler. The component will not update its position without an external state change.


Examples

Controlled with external buttons

Drive the slider value from outside the component using React state. The glass handle position updates reactively whenever value changes.

30
'use client';

import { useState } from 'react';
import { LiquidSlider } from '@/components/ui/liquid-slider';

export default function Example() {
  const [value, setValue] = useState(30);

  return (
    <div className="flex flex-col gap-4">
      <LiquidSlider
        min={0}
        max={100}
        value={value}
        className="w-full"
        events={{ input: ({ value }) => setValue(value) }}
      />
      <div className="flex items-center gap-3">
        <button onClick={() => setValue((v) => Math.max(0, v - 10))}>−10</button>
        <span>{value}</span>
        <button onClick={() => setValue((v) => Math.min(100, v + 10))}>+10</button>
      </div>
    </div>
  );
}

Step snapping

Use the step option inside options to snap the thumb to discrete intervals.

Snaps to: 0 · 25 · 50 · 75 · 100 — current: 25

<LiquidSlider
  min={0}
  max={100}
  value={value}
  options={{ step: 25 }}
  className="w-full"
  events={{ input: ({ value }) => setValue(value) }}
/>

Custom range

min and max accept any numeric range — negative values are supported. The slider clamps the current value automatically when the range changes.

Temperature: 20°C

<LiquidSlider
  min={-20}
  max={50}
  value={temp}
  className="w-full"
  events={{ input: ({ value }) => setTemp(value) }}
/>

input vs change events

input fires continuously while dragging (like oninput on a native range). change fires once on pointer-up (like onchange). Use change for expensive operations such as API calls.

Live (input): 50Committed (change): 50
<LiquidSlider
  min={0}
  max={100}
  value={live}
  className="w-full"
  events={{
    input:  ({ value }) => setLive(value),      // every frame
    change: ({ value }) => commitToServer(value), // pointer-up only
  }}
/>

Forwarded ref

LiquidSlider forwards its ref to the underlying <div> container, giving you direct DOM access.

'use client';

import { useRef } from 'react';
import { LiquidSlider } from '@/components/ui/liquid-slider';

export default function Example() {
  const sliderRef = useRef<HTMLDivElement>(null);

  return (
    <LiquidSlider
      ref={sliderRef}
      min={0}
      max={100}
      value={50}
      className="w-full"
    />
  );
}

Glass appearance options

All GlassOptions from the shared engine are available via the options prop — the same knobs exposed by LiquidButton.

<LiquidSlider
  min={0}
  max={100}
  value={value}
  className="w-full"
  options={{
    bezelWidth: 12,
    refractiveIndex: 1.8,
    specularSlope: 1,
    saturation: 1.6,
    profile: 'convexSquircle',
  }}
  events={{ input: ({ value }) => setValue(value) }}
/>

API Reference

LiquidSliderProps

Prop

Type

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


LiquidSliderOptions

LiquidSliderOptions extends GlassOptions with slider-specific fields.

Prop

Type


LiquidSliderEventMap

Prop

Type

On this page