Skip to content
Pebble Pro is here at $49 one timeSee what’s included ↗
Pebble
ComponentsDocsMCPPricing
⌕ Find a component↗Log in
All components 673

Categories

Core inputs & forms46Buttons & actions33Navigation36Overlays & feedback49Data display33Charts & data viz40Cards & layout43Marketing blocks38App shells & pages37AI interfaces40Text & typography effects33Backgrounds & visual effects38Motion & transitions32Numbers & counters32Media34E-commerce333D & interactive36Themed sets40
Start with the essentials.Installation guide ↗Connect your agent ↗
← All componentsCore inputs & forms

Slider

Pick a value or a range by dragging thumbs along a track.

Preview

Nightly rate

$80 – $240 · 193 stays

$80$240
$0$400+

Output volume

Malik’s headphones
64%

Bitrate

128 kbps · locked on Free

Equalizer

Priya’s studio preset

+3 dB60
+5 dB230
-2 dB910
+1 dB3.6k
+4 dB14k

Install

Add the editable source with the shadcn CLI. Your project also needs the shared theme tokens this component uses. Setup and requirements. The Pebble installer is not published yet.

npx shadcn@latest add https://pebbleui.com/r/slider.json

Props

PropTypeDefaultDescription
valuenumber[]—Controlled; one thumb per entry
defaultValuenumber[][min]Uncontrolled; one thumb per entry
onValueChange(value: number[]) => void—Every change
onValueCommit(value: number[]) => void—At the end of a drag or key press
min / max / stepnumber0 / 100 / 1Range and increment
orientation"horizontal" | "vertical""horizontal"Vertical needs a parent height or uses min-h-44
thumbLabelsstring[]—Accessible name per thumb, in order
showValue"interact" | "always"—Value bubble above each thumb: on hover, focus and drag, or always
formatValue(value: number, index: number) => string—Formats the bubble text; also becomes the thumb's aria-valuetext
aria-label / aria-labelledbystring—Applied to the thumb when there is a single thumb
disabledbooleanfalseNot interactive, 50% opacity
Build notes
Radix Slider primitive handles pointer capture, keyboard, RTL and value
clamping. We render `Thumb`s from `value ?? defaultValue ?? [min]`, so the
thumb count always matches the value array (shadcn defaults to `[min, max]`;
we default to a single thumb). `aria-label` / `aria-labelledby` are pulled out
of the root props and put on every thumb, because a label on the role-less
root span names nothing. A range adds a position suffix to each thumb: with
`aria-labelledby`, the thumb gets `aria-labelledby="<shared> <thumbId>"` and
its own `aria-label` is the suffix; otherwise `aria-label` is
`"<label> <suffix>"`. When both are given, `aria-labelledby` wins and the
caller's `aria-label` is ignored, as ARIA naming does. An empty
`thumbLabels[i]` counts as missing. Layout swaps on `data-orientation`: track
`h-1.5 w-full` horizontally, `w-1.5 h-full` vertically; the range fills the
cross axis. The root's size (`h-5`, or `h-full min-h-44 w-5` vertically) is
chosen from the `orientation` prop as plain classes, not `data-[orientation]`
variants, so a consumer `h-*` / `min-h-0` in `className` replaces it. Forced colors: track gets a `CanvasText` border, range is painted
`Highlight`, thumb `Canvas` with a `CanvasText` edge.

**Pebble (2026-11-03).** Track is a recessed groove (`bg-muted`, `border-control` edge, `inset-shadow-xs`, 6 px); range is `bg-primary`. Thumb is an 18 px raised pebble (`bg-card`, `shadow-raised-sm`, `inset-shadow-highlight`) that lifts to `raised-md` on hover and scales to 95% on press; in dark mode it becomes the paper key so it reads on ink. Added optional `showValue` (`"interact"` bubble on hover, focus-visible and drag; `"always"` pinned) and `formatValue`, which also feeds `aria-valuetext`. The uncontrolled value is mirrored in state through a wrapped `onValueChange` so the bubble follows it. The bubble is `aria-hidden`, sits above the thumb (beside it when vertical) and fades/scales with `duration-(--motion-base) ease-enter`; reduced motion keeps only the fade.

<!-- PEBBLE-LICENSE -->
View source

registry/input/slider/slider.tsx

"use client";

/* PEBBLE-LICENSE
Built by Pebble. Component: slider.

First-party license: MIT

The following first-party terms apply only to original Pebble material.

MIT License

Copyright (c) 2026 Praxys Solutions LLC (Pebble)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

THIRD-PARTY NOTICES
These notices apply to identified upstream portions; Pebble terms do not replace or narrow their rights.

Upstream: https://raw.githubusercontent.com/shadcn-ui/ui/main/LICENSE.md
MIT License

Copyright (c) 2023 shadcn

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
END PEBBLE-LICENSE */

import * as React from "react";
import { Slider as SliderPrimitive } from "radix-ui";

import { cn } from "@/lib/utils";

export type SliderProps = React.ComponentProps<typeof SliderPrimitive.Root> & {
  /** Accessible name for each thumb, in order. */
  thumbLabels?: string[];
  /**
   * Show each thumb's value in a small bubble above it: `"interact"` on
   * hover, focus and drag; `"always"` at all times. Omit for no bubble.
   */
  showValue?: "interact" | "always";
  /**
   * Formats a thumb's value for the bubble. When given it also becomes the
   * thumb's `aria-valuetext`, so screen readers hear "$240", not "240".
   */
  formatValue?: (value: number, index: number) => string;
};

// Without thumbLabels, each range thumb gets a position suffix so no two share a name.
function thumbSuffix(index: number, count: number) {
  if (count === 1) return undefined;
  if (count === 2) return index === 0 ? "minimum" : "maximum";
  return `${index + 1} of ${count}`;
}

export function Slider({
  className,
  defaultValue,
  value,
  min = 0,
  max = 100,
  orientation = "horizontal",
  thumbLabels,
  showValue,
  formatValue,
  onValueChange,
  "aria-label": ariaLabel,
  "aria-labelledby": ariaLabelledBy,
  ...props
}: SliderProps) {
  // Mirror the uncontrolled value so the bubble can show it; a controlled
  // `value` always wins. One thumb per value, or one at min with neither.
  const [inner, setInner] = React.useState(() => defaultValue ?? [min]);
  const values = value ?? inner;
  const baseId = React.useId();

  return (
    <SliderPrimitive.Root
      data-slot="slider"
      defaultValue={defaultValue}
      value={value}
      min={min}
      max={max}
      orientation={orientation}
      onValueChange={(next) => {
        setInner(next);
        onValueChange?.(next);
      }}
      className={cn(
        "group/slider relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50",
        // Plain (not data-variant) size classes, so tailwind-merge lets a
        // consumer's h-* / min-h-* / w-* replace them instead of losing on specificity.
        orientation === "vertical" ? "h-full min-h-44 w-5 flex-col" : "h-5",
        className,
      )}
      {...props}
    >
      <SliderPrimitive.Track
        data-slot="slider-track"
        // A recessed groove: muted well, inner shade and the control edge.
        className="relative grow overflow-hidden rounded-full border border-border-control bg-muted inset-shadow-xs data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5 forced-colors:border-[CanvasText]"
      >
        <SliderPrimitive.Range
          data-slot="slider-range"
          className="absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full forced-colors:bg-[Highlight]!"
        />
      </SliderPrimitive.Track>
      {values.map((current, index) => {
        // An empty string counts as missing, so the thumb keeps its suffix.
        const explicit = thumbLabels?.[index] || undefined;
        const suffix = thumbSuffix(index, values.length);
        const thumbId = `${baseId}-thumb-${index}`;
        // aria-labelledby wins over aria-label, so a range points it at the shared
        // label and then at the thumb itself, whose aria-label is the suffix.
        const labelledBy =
          explicit || !ariaLabelledBy
            ? undefined
            : suffix
              ? `${ariaLabelledBy} ${thumbId}`
              : ariaLabelledBy;
        const label =
          explicit ??
          (ariaLabelledBy ? suffix : suffix && ariaLabel ? `${ariaLabel} ${suffix}` : ariaLabel);
        const text = formatValue ? formatValue(current, index) : String(current);
        return (
          <SliderPrimitive.Thumb
            key={index}
            id={thumbId}
            data-slot="slider-thumb"
            aria-label={label}
            aria-labelledby={labelledBy}
            aria-valuetext={formatValue ? text : undefined}
            className={cn(
              "group/thumb relative block size-[18px] shrink-0 cursor-grab rounded-full active:cursor-grabbing",
              // A raised pebble: card face, control edge, low shadow and a top
              // light. In dark mode it becomes the paper key so it reads on ink.
              "border border-border-control bg-card shadow-raised-sm inset-shadow-highlight dark:border-key-edge dark:bg-linear-to-b dark:from-key-top dark:to-key-bottom",
              "transition-[box-shadow,scale] duration-(--motion-fast) ease-enter motion-reduce:transition-none",
              "hover:shadow-raised-md active:scale-95 motion-reduce:active:scale-100",
              "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
              "data-[disabled]:pointer-events-none",
              "forced-colors:border-[CanvasText] forced-colors:bg-[Canvas]!",
            )}
          >
            {showValue && (
              <span
                aria-hidden
                data-slot="slider-value"
                className={cn(
                  "pointer-events-none absolute rounded-md bg-card px-1.5 py-0.5 text-xs font-medium whitespace-nowrap text-foreground tabular-nums shadow-raised-md ring-1 inset-shadow-highlight ring-hairline",
                  "bottom-full left-1/2 mb-2 origin-bottom -translate-x-1/2",
                  "group-data-[orientation=vertical]/thumb:start-full group-data-[orientation=vertical]/thumb:top-1/2 group-data-[orientation=vertical]/thumb:bottom-auto group-data-[orientation=vertical]/thumb:left-auto group-data-[orientation=vertical]/thumb:ms-2.5 group-data-[orientation=vertical]/thumb:mb-0 group-data-[orientation=vertical]/thumb:origin-left group-data-[orientation=vertical]/thumb:translate-x-0 group-data-[orientation=vertical]/thumb:-translate-y-1/2",
                  showValue === "interact" &&
                    "scale-[0.96] opacity-0 transition-[opacity,scale] duration-(--motion-base) ease-enter group-hover/thumb:scale-100 group-hover/thumb:opacity-100 group-focus-visible/thumb:scale-100 group-focus-visible/thumb:opacity-100 group-active/slider:scale-100 group-active/slider:opacity-100 motion-reduce:scale-100 motion-reduce:transition-opacity",
                )}
              >
                {text}
              </span>
            )}
          </SliderPrimitive.Thumb>
        );
      })}
    </SliderPrimitive.Root>
  );
}
Pebble

Small details.
Better interfaces.

React components to explore, make your own, and build on.

Library

  • Components
  • Installation
  • Pricing

Resources

  • For agents
  • About
  • Support

Legal

  • License
  • Terms
  • Privacy
  • Final sale

Built by Pebble

A product of Praxys Solutions LLC ↗

Pebble Pro is live

$49 one time, lifetime updates ↗