UI Elements

Cloud Dancer Slider

Base UI slider dressed as the Cloud Dancer pill: a teal-shaded off-white strip with a dark filled range and a dark orb thumb. Takes one value or a range, with a label per thumb.

Free, MIT licenseOpen in Browse
"use client"

import { Slider as SliderPrimitive } from "@base-ui/react/slider"
import { cn } from "cn"

// Cloud Dancer slider, inverted: the stock Slider with the Cloud Dancer pill
// as the strip, teal-shaded, a void bar for the filled range and a void orb.
// --s-main and --s-accent are the strip's shade and rim, as rgb channels.
const TRACK =
  "ring-1 ring-foreground/10 dark:ring-foreground/25 [--s-main:90_144_144] [--s-accent:184_92_76] bg-[radial-gradient(circle_at_35%_35%,#ffffff_0%,oklch(0.995_0.002_90)_40%,oklch(0.915_0.006_85)_100%)] shadow-[inset_-1.5px_-2px_4px_rgb(var(--s-main)/0.28),inset_-1px_-1px_2px_rgb(var(--s-main)/0.15),inset_1px_1.5px_2.5px_rgb(255_255_255/0.85),inset_0.5px_1px_1.5px_rgb(255_255_255/0.60),inset_-2px_-2.5px_4px_rgb(var(--s-accent)/0.10),0_1px_3px_rgb(var(--s-main)/0.18),0_1px_1.5px_rgb(0_0_0/0.06)]"

const RANGE =
  "bg-[radial-gradient(circle_at_40%_38%,oklch(0.28_0.008_60),oklch(0.16_0.006_60))] shadow-[inset_1px_1.5px_3px_rgb(255_255_255/0.07),inset_-1px_-1px_3px_rgb(0_0_0/0.45)]"

const THUMB =
  "bg-[radial-gradient(circle_at_40%_38%,oklch(0.28_0.008_60),oklch(0.16_0.006_60))] shadow-[inset_1.5px_2px_4px_rgb(255_255_255/0.07),inset_-1px_-1.5px_4px_rgb(0_0_0/0.45),0_2px_5px_rgb(0_0_0/0.14),0_1px_2px_rgb(0_0_0/0.08)] transition-[box-shadow]"

/**
 * A Base UI slider styled as the Cloud Dancer pill: an off-white,
 * teal-shaded strip with a dark filled range and a dark orb thumb. Give
 * `value` or `defaultValue` a number for one thumb or an array for a range,
 * and name each thumb with `thumbLabels`. The strip, range and thumb keep
 * their colors in light and dark themes.
 */
function CloudDancerSlider({
  className,
  defaultValue,
  value,
  min = 0,
  max = 100,
  thumbLabels,
  ...props
}: SliderPrimitive.Root.Props & {
  /** One name per thumb for a range, e.g. ["Minimum price", "Maximum price"]. */
  thumbLabels?: string[]
}) {
  // A number or no value is one thumb; an array is one thumb per entry.
  const initial = value ?? defaultValue
  const thumbs = Array.isArray(initial) ? initial.length : 1

  return (
    <SliderPrimitive.Root
      className={(state) =>
        cn(
          "data-horizontal:w-full data-vertical:h-full",
          typeof className === "function" ? className(state) : className
        )
      }
      data-slot="cloud-dancer-slider"
      defaultValue={defaultValue}
      value={value}
      min={min}
      max={max}
      thumbAlignment="edge"
      {...props}
    >
      <SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col">
        <SliderPrimitive.Track
          data-slot="cloud-dancer-slider-track"
          className={cn(
            "relative grow overflow-hidden rounded-full select-none data-horizontal:h-2.5 data-horizontal:w-full data-vertical:h-full data-vertical:w-2.5",
            TRACK
          )}
        >
          <SliderPrimitive.Indicator
            data-slot="cloud-dancer-slider-range"
            className={cn(
              "rounded-full select-none data-horizontal:h-full data-vertical:w-full",
              RANGE
            )}
          />
        </SliderPrimitive.Track>
        {Array.from({ length: thumbs }, (_, index) => (
          <SliderPrimitive.Thumb
            data-slot="cloud-dancer-slider-thumb"
            key={index}
            index={index}
            aria-label={thumbLabels?.[index]}
            className={cn(
              "block size-4.5 shrink-0 rounded-full ring-0 transition-shadow select-none hover:ring-4 hover:ring-ring/30 focus-visible:ring-4 focus-visible:ring-ring/30 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
              THUMB
            )}
          />
        ))}
      </SliderPrimitive.Control>
    </SliderPrimitive.Root>
  )
}

export { CloudDancerSlider }

Installation

pnpm dlx shadcn add @01orbital/cloud-dancer-slider
npx shadcn add @01orbital/cloud-dancer-slider
yarn shadcn add @01orbital/cloud-dancer-slider
bunx --bun shadcn add @01orbital/cloud-dancer-slider

Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.

It lands at components/slider/cloud-dancer-slider.tsx, as source you own and edit.

The example also uses field. Install it too:

pnpm dlx shadcn add @01orbital/field
npx shadcn add @01orbital/field
yarn shadcn add @01orbital/field
bunx --bun shadcn add @01orbital/field

Usage

"use client"

import { useId } from "react"

import { CloudDancerSlider } from "@/components/slider/cloud-dancer-slider"
import { Field, FieldTitle } from "@/components/ui/field"

export default function CloudDancerSliderDemo() {
  const id = useId()
  return (
    <div className="relative isolate grid min-h-full w-full flex-1 place-items-center overflow-hidden bg-background py-24 text-foreground">
      <Field className="w-64">
        <FieldTitle id={id}>Volume</FieldTitle>
        <CloudDancerSlider
          aria-labelledby={id}
          defaultValue={[35]}
          max={100}
          step={1}
        />
      </Field>
    </div>
  )
}

API reference

Prop

Type

It also accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.

Dependencies

npm: @base-ui/react, cn. The CLI installs them for you.

No other registry pieces.