GlassKit UI
Primitives

Slider

A continuous level control (volume, brightness, the quick controls). A native range tinted with accent-color; arrow keys / Neural-Band pinch-twist adjust it. Controlled via value + onChange.

Volume · 60
Arrow keys adjust while focused

Installation

npx @glasskit-ui/cli add slider

Install the SDK (it provides GlassViewport, useDpad and the stylesheet), then copy these files into your project:

npm install @glasskit-ui/react
// components/lib/utils.tsimport { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge";export type { ClassValue };/** * Merge class names the shadcn way: clsx joins conditionals, tailwind-merge * de-dupes conflicting Tailwind utilities so a consumer's `className` override * wins (e.g. passing `px-2` beats the component's `px-6`). Lens components are * Tailwind utilities + `--gk-*` tokens, so this de-dupe matters. */export function cn(...inputs: ClassValue[]): string {  return twMerge(clsx(inputs));}/** * Accessible name from a free-form `label` prop: the label itself when it's a * plain string, otherwise undefined (a ReactNode can't become an aria-label). */export function stringLabel(label: unknown): string | undefined {  return typeof label === "string" ? label : undefined;}
// components/glasskit/slider.tsximport type { ReactNode } from "react";import { cn, stringLabel } from "../lib/utils";/** * <Slider> — a continuous level control (volume, brightness — the quick * controls). A native range so the fill + thumb need no inline style * (`accent-color` tints them); arrow keys / Neural-Band pinch-twist adjust it. * Controlled via `value` + `onChange`. Omit `onChange` for a read-only * display — the slider then leaves the D-pad focus order (`readOnly` is * meaningless on a range input; an adjustable-looking dead control is worse * than a plain level display). */export function Slider({  value,  min = 0,  max = 100,  label,  icon,  onChange,  className,}: {  value: number;  min?: number;  max?: number;  /** a11y / caption label. */  label?: ReactNode;  /** Leading glyph — typically a <Icon> (volume / brightness). */  icon?: ReactNode;  onChange?: (next: number) => void;  className?: string;}) {  return (    <div className={cn("flex w-full items-center gap-4", className)}>      {/* gk-slider__icon stays: it sizes/tints the Icon child via a       * descendant rule (.gk-slider__icon .gk-icon). */}      {icon != null ? <span className="gk-slider__icon">{icon}</span> : null}      {/* gk-slider__input stays: the range track + thumb are styled through       * ::-webkit-slider-thumb / ::-moz-range-thumb pseudo-elements + the focus       * ring — none of which Tailwind utilities can reach. */}      <input        type="range"        className={cn("gk-slider__input", onChange != null && "focusable")}        min={min}        max={max}        value={value}        // readOnly only suppresses React's controlled-input warning (the        // attribute itself is meaningless on type="range") — the real        // read-only mechanism is leaving the focus order above.        readOnly={onChange == null}        tabIndex={onChange == null ? -1 : undefined}        aria-readonly={onChange == null ? true : undefined}        aria-label={stringLabel(label)}        onChange={          onChange ? (e) => onChange(Number(e.currentTarget.value)) : undefined        }      />      {label != null ? (        <span className="t-caption flex-none text-foreground-faint">          {label}        </span>      ) : null}    </div>  );}

Usage

<Slider  value={volume} onChange={setVolume}  icon={<Icon size="md"><VolumeIcon /></Icon>}  label="Volume"/>

Props

PropTypeDefaultDescription
valuenumberCurrent value (controlled).
min / maxnumber0 / 100Bounds.
onChange(next: number) => voidFires on adjust.
icon / labelReactNodeLeading glyph / caption.

On this page