Components
Slider

Slider

A control element that allows for a range of selections.

Usage

import { Slider, type SliderProps } from '~/components/ui/slider'

export const Demo = (props: SliderProps) => {
  return (
    <Slider
      value={[33]}
      marks={[
        { value: 25, label: '25' },
        { value: 50, label: '50' },
        { value: 75, label: '75' },
      ]}
      {...props}
    />
  )
}

Examples

Range Slider

To use the slider as a range slider, you need to provide an array of values.

<Slider value={[33, 66]}>Label</Slider>

Unlabeled

The slider can also be used without a label while still showing indicators within the track.

<Slider defaultValue={[33]} marks={[{ value: 0 }, { value: 25 }, { value: 50 }, { value: 75 }, { value: 100 }]} />

Installation

1

Add Component

Insert code snippet into your project. Update import paths as needed.

import { Slider as ArkSlider, type SliderRootProps } from '@ark-ui/react/slider'
import { forwardRef, type ReactNode } from 'react'
import { css, cx } from 'styled-system/css'
import { splitCssProps } from 'styled-system/jsx'
import { slider, type SliderVariantProps } from 'styled-system/recipes'
import type { Assign, JsxStyleProps } from 'styled-system/types'

export interface SliderProps extends Assign<JsxStyleProps, SliderRootProps>, SliderVariantProps {
  children?: ReactNode
  marks?: {
    value: number
    label?: ReactNode
  }[]
}

export const Slider = forwardRef<HTMLDivElement, SliderProps>((props, ref) => {
  const [variantProps, sliderProps] = slider.splitVariantProps(props)
  const [cssProps, localProps] = splitCssProps(sliderProps)
  const { children, className, ...rootProps } = localProps
  const styles = slider(variantProps)

  return (
    <ArkSlider.Root ref={ref} className={cx(styles.root, css(cssProps), className)} {...rootProps}>
      {(api) => (
        <>
          {children && <ArkSlider.Label className={styles.label}>{children}</ArkSlider.Label>}
          <ArkSlider.Control className={styles.control}>
            <ArkSlider.Track className={styles.track}>
              <ArkSlider.Range className={styles.range} />
            </ArkSlider.Track>
            {api.value.map((_, index) => (
              <ArkSlider.Thumb key={index} index={index} className={styles.thumb} />
            ))}
          </ArkSlider.Control>
          {props.marks && (
            <ArkSlider.MarkerGroup className={styles.markerGroup}>
              {props.marks.map((mark) => (
                <ArkSlider.Marker key={mark.value} value={mark.value} className={styles.marker}>
                  {mark.label}
                </ArkSlider.Marker>
              ))}
            </ArkSlider.MarkerGroup>
          )}
        </>
      )}
    </ArkSlider.Root>
  )
})

Slider.displayName = 'Slider'
2

Add Recipe

This step is necessary only if you do not use any of the Park UI plugins.

import { sliderAnatomy } from '@ark-ui/anatomy'
import { defineSlotRecipe } from '@pandacss/dev'

export const slider = defineSlotRecipe({
  className: 'slider',
  slots: sliderAnatomy.keys(),
  base: {
    root: {
      colorPalette: 'accent',
      display: 'flex',
      flexDirection: 'column',
      gap: '1',
      width: 'full',
    },
    control: {
      position: 'relative',
      display: 'flex',
      alignItems: 'center',
    },
    track: {
      backgroundColor: 'bg.emphasized',
      borderRadius: 'full',
      overflow: 'hidden',
      flex: '1',
    },
    range: {
      background: 'colorPalette.default',
    },
    thumb: {
      background: 'bg.default',
      borderColor: 'colorPalette.default',
      borderRadius: 'full',
      borderWidth: '2px',
      boxShadow: 'sm',
      outline: 'none',
      zIndex: '1',
    },
    label: {
      color: 'fg.default',
      fontWeight: 'medium',
    },
    markerGroup: {
      mt: '-1',
    },
    marker: {
      '--before-background': {
        base: 'white',
        _dark: 'colors.colorPalette.fg',
      },
      color: 'fg.muted',
      _before: {
        background: 'white',
        borderRadius: 'full',
        content: "''",
        display: 'block',
        left: '50%',
        position: 'relative',
        transform: 'translateX(-50%)',
      },
      _underValue: {
        _before: {
          background: 'var(--before-background)',
        },
      },
    },
  },
  defaultVariants: {
    size: 'md',
  },
  variants: {
    size: {
      sm: {
        control: {
          height: '4',
        },
        range: {
          height: '1.5',
        },
        track: {
          height: '1.5',
        },
        thumb: {
          height: '4',
          width: '4',
        },
        marker: {
          _before: {
            height: '1',
            top: '-2.5',
            width: '1',
          },
          textStyle: 'sm',
        },
        label: {
          textStyle: 'sm',
        },
      },
      md: {
        control: {
          height: '5',
        },
        range: {
          height: '2',
        },
        track: {
          height: '2',
        },
        thumb: {
          height: '5',
          width: '5',
        },
        marker: {
          _before: {
            height: '1',
            top: '-3',
            width: '1',
          },
          textStyle: 'sm',
        },
        label: {
          textStyle: 'sm',
        },
      },
      lg: {
        control: {
          height: '6',
        },
        range: {
          height: '2.5',
        },
        track: {
          height: '2.5',
        },
        thumb: {
          height: '6',
          width: '6',
        },
        marker: {
          _before: {
            height: '1.5',
            top: '-15px',
            width: '1.5',
          },
          textStyle: 'md',
        },
        label: {
          textStyle: 'md',
        },
      },
    },
  },
})