V-/Srcv0.1.0beta

// slider

Slider

A glass lens for a thumb; the rim bends the track as it slides. The switch, inverted.

// install
npx shadcn add @vsrc/slider

Props

Only the vsrc surface is documented here. Everything else passes through to Radix Slider.

// Slider

proptypedefaultnotes
glass"subtle" | "regular" | "heavy" | LiquidGlassOptions | falseOptics override for each thumb — the track stays flat. A preset name replaces the tuned values, an object merges over them, `false` disables refraction entirely.
thumbLabelsstring[]Accessible names for the thumbs — role=slider lives on the thumb, not the root, so a root aria-label never names the control. A bare aria-label covers the single-thumb case.

Optics

THUMB_OPTICS: scale −72, chroma 5, border 0.18, mapBlur 5, blur 0. Blur is zero on purpose: the thumb is a lens, and a lens must bend the track crisply — fog would read as dirt, not glass. Translation-only movement keeps the displacement map stable mid-drag.

Accessibility

  • Radix slider semantics: role=slider, arrow keys, home/end, aria-valuenow.
  • Multi-thumb ready — each thumb carries its own lens.

Source

The exact file the CLI copies into your project — you own it after install.

// registry/vsrc/ui/slider.tsx
"use client";

import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";
import { mergeGlass, type GlassPreset, type LiquidGlassOptions } from "vsrc/react";

import { GlassSurface } from "@/registry/vsrc/ui/glass-surface";
import { cn } from "@/lib/utils";

// A small, strong lens: the switch inverted. The track carries the colour and
// the thumb is the glass, so blur is 0 — the lens must bend the track crisply,
// not fog it. Dragging only moves the thumb's position (Radix sets `left` on a
// wrapper), never its box, so the engine's ResizeObserver never regenerates the
// displacement map mid-drag — the same reason the site's lens cursor is cheap.
const THUMB_OPTICS: LiquidGlassOptions = {
  scale: -72,
  chroma: 5,
  border: 0.18,
  mapBlur: 5,
  blur: 0,
  saturate: 1.5,
  fallbackBlur: 1,
};

function Slider({
  className,
  glass,
  defaultValue,
  value,
  thumbLabels,
  "aria-label": ariaLabel,
  ...props
}: React.ComponentProps<typeof SliderPrimitive.Root> & {
  glass?: LiquidGlassOptions | GlassPreset | false;
  /** Accessible names for the thumbs — role=slider lives on the THUMB, not the
   *  root, so an aria-label on the root never names the control. A bare
   *  aria-label covers the single-thumb case; multi-thumb sliders name each. */
  thumbLabels?: string[];
}) {
  const thumbCount = React.useMemo(() => {
    if (Array.isArray(value)) return value.length;
    if (Array.isArray(defaultValue)) return defaultValue.length;
    return 1;
  }, [value, defaultValue]);

  return (
    <SliderPrimitive.Root
      data-slot="slider"
      defaultValue={defaultValue}
      value={value}
      className={cn(
        "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50",
        className,
      )}
      {...props}
    >
      {/* A coloured, ticked track so the lens has something to bend — signal red
          is decoration here (no text sits on it). */}
      <SliderPrimitive.Track
        data-slot="slider-track"
        className="relative h-2 w-full grow overflow-hidden rounded-full"
        style={{
          background:
            "linear-gradient(90deg, color-mix(in oklab, var(--primary) 70%, transparent), color-mix(in oklab, var(--signal) 55%, transparent) 55%, color-mix(in oklab, var(--foreground) 28%, transparent)), repeating-linear-gradient(90deg, transparent 0 13px, color-mix(in oklab, var(--foreground) 30%, transparent) 13px 14px)",
        }}
      >
        <SliderPrimitive.Range data-slot="slider-range" className="absolute h-full bg-foreground/15" />
      </SliderPrimitive.Track>
      {Array.from({ length: thumbCount }, (_, i) => (
        <GlassSurface asChild glass={mergeGlass(THUMB_OPTICS, glass)} key={i}>
          <SliderPrimitive.Thumb
            aria-label={thumbLabels?.[i] ?? ariaLabel}
            data-slot="slider-thumb"
            className={cn(
              "relative block size-7 rounded-full outline-none",
              "transition-[border-color] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background",
              "disabled:pointer-events-none",
            )}
          />
        </GlassSurface>
      ))}
    </SliderPrimitive.Root>
  );
}

export { Slider };