Installation
npx @park-ui/cli add sliderAdd Component
Copy the code snippet below into you components folder.
'use client'
import { ark } from '@ark-ui/react/factory'
import { Slider, useSliderContext } from '@ark-ui/react/slider'
import { type ComponentProps, forwardRef } from 'react'
import { createStyleContext } from 'styled-system/jsx'
import { slider } from 'styled-system/recipes'
const { withProvider, withContext } = createStyleContext(slider)
export const Root = withProvider(Slider.Root, 'root')
export const Control = withContext(Slider.Control, 'control')
export const DraggingIndicator = withContext(Slider.DraggingIndicator, 'draggingIndicator')
export const Label = withContext(Slider.Label, 'label')
export const Marker = withContext(Slider.Marker, 'marker')
export const MarkerIndicator = withContext(ark.div, 'markerIndicator')
export const MarkerGroup = withContext(Slider.MarkerGroup, 'markerGroup')
export const Range = withContext(Slider.Range, 'range')
export const Thumb = withContext(Slider.Thumb, 'thumb')
export const Track = withContext(Slider.Track, 'track')
export const ValueText = withContext(Slider.ValueText, 'valueText')
export const HiddenInput = Slider.HiddenInput
export { SliderContext as Context } from '@ark-ui/react/slider'
export type RootProps = ComponentProps<typeof Root>
export type MarkerGroupProps = ComponentProps<typeof MarkerGroup>
export type ThumbProps = ComponentProps<typeof Thumb>
export interface MarksProps extends MarkerGroupProps {
marks?: Array<number | { value: number; label: React.ReactNode }> | undefined
}
export const Marks = forwardRef<HTMLDivElement, MarksProps>(function Marks(props, ref) {
const { marks, ...rest } = props
if (!marks?.length) return null
return (
<MarkerGroup ref={ref} {...rest}>
{marks.map((mark, index) => {
const value = typeof mark === 'number' ? mark : mark.value
const label = typeof mark === 'number' ? undefined : mark.label
return (
<Marker key={index} value={value}>
<MarkerIndicator />
{label != null && <span>{label}</span>}
</Marker>
)
})}
</MarkerGroup>
)
})
export const Thumbs = (props: Omit<ThumbProps, 'index'>) => {
const slider = useSliderContext()
return slider.value.map((_, index) => (
<Thumb key={index} index={index} {...props}>
<HiddenInput />
</Thumb>
))
}
Integrate Recipe
Integrate this recipe in to your Panda config.
import { sliderAnatomy } from '@ark-ui/react/slider'
import { defineSlotRecipe } from '@pandacss/dev'
export const slider = defineSlotRecipe({
className: 'slider',
slots: sliderAnatomy.extendWith('markerIndicator').keys(),
base: {
root: {
display: 'flex',
flexDirection: 'column',
gap: '1',
textStyle: 'sm',
position: 'relative',
isolation: 'isolate',
touchAction: 'none',
width: 'full',
},
label: {
fontWeight: 'medium',
textStyle: 'sm',
},
control: {
display: 'inline-flex',
alignItems: 'center',
},
track: {
overflow: 'hidden',
borderRadius: 'full',
flex: '1',
},
range: {
width: 'inherit',
height: 'inherit',
},
markerGroup: {
position: 'absolute!',
zIndex: '1',
},
marker: {
display: 'flex',
alignItems: 'center',
gap: 'calc(var(--slider-thumb-size) / 2)',
color: 'fg.muted',
textStyle: 'xs',
},
markerIndicator: {
width: 'var(--slider-marker-size)',
height: 'var(--slider-marker-size)',
borderRadius: 'full',
bg: 'colorPalette.solid.fg',
},
thumb: {
width: 'var(--slider-thumb-size)',
height: 'var(--slider-thumb-size)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
outline: 0,
zIndex: '2',
borderRadius: 'full',
_focusVisible: {
ring: '2px',
ringColor: 'colorPalette.solid',
ringOffset: '2px',
ringOffsetColor: 'bg',
},
},
},
defaultVariants: {
size: 'md',
variant: 'outline',
orientation: 'horizontal',
},
variants: {
size: {
sm: {
root: {
'--slider-thumb-size': 'sizes.5',
'--slider-track-size': 'sizes.2',
'--slider-marker-center': '8px',
'--slider-marker-size': 'sizes.1',
'--slider-marker-inset': '4px',
},
},
md: {
root: {
'--slider-thumb-size': 'sizes.5',
'--slider-track-size': 'sizes.2',
'--slider-marker-center': '8px',
'--slider-marker-size': 'sizes.1',
'--slider-marker-inset': '4px',
},
},
lg: {
root: {
'--slider-thumb-size': 'sizes.5',
'--slider-track-size': 'sizes.2',
'--slider-marker-center': '8px',
'--slider-marker-size': 'sizes.1',
'--slider-marker-inset': '4px',
},
},
},
variant: {
outline: {
thumb: {
bg: 'gray.surface.bg',
borderWidth: '2px',
borderColor: 'colorPalette.solid.bg',
boxShadow: 'xs',
},
range: {
bg: 'colorPalette.solid.bg',
},
track: {
bg: 'border',
},
},
},
orientation: {
vertical: {
root: {
display: 'inline-flex',
},
control: {
flexDirection: 'column',
height: '100%',
minWidth: 'var(--slider-thumb-size)',
'&[data-has-mark-label]': {
marginEnd: '4',
},
},
track: {
width: 'var(--slider-track-size)',
},
thumb: {
left: '50%',
translate: '-50% 0',
},
markerGroup: {
insetStart: 'var(--slider-marker-center)',
insetBlock: 'var(--slider-marker-inset)',
},
marker: {
flexDirection: 'row',
},
},
horizontal: {
control: {
flexDirection: 'row',
width: '100%',
minHeight: 'var(--slider-thumb-size)',
'&[data-has-mark-label]': {
marginBottom: '4',
},
},
track: {
height: 'var(--slider-track-size)',
},
thumb: {
top: '50%',
translate: '0 -50%',
},
markerGroup: {
top: 'var(--slider-marker-center)',
insetInline: 'var(--slider-marker-inset)',
},
marker: {
flexDirection: 'column',
},
},
},
},
})
Usage
import { Slider } from '@/components/ui'
<Slider.Root>
<Slider.Label />
<Slider.ValueText />
<Slider.Control>
<Slider.Track>
<Slider.Range />
</Slider.Track>
<Slider.Thumb>
<Slider.DraggingIndicator />
<Slider.HiddenInput />
</Slider.Thumb>
<Slider.MarkerGroup>
<Slider.Marker />
</Slider.MarkerGroup>
</Slider.Control>
</Slider.Root>
Props
Root
| Prop | Default | Type |
|---|---|---|
size | 'md' | 'sm' | 'md' | 'lg' |
variant | 'outline' | 'outline' |
orientation | \horizontal\ | 'horizontal' | 'vertical'The orientation of the slider |
max | 100 | numberThe maximum value of the slider |
min | 0 | numberThe minimum value of the slider |
minStepsBetweenThumbs | 0 | numberThe minimum permitted steps between multiple thumbs. `minStepsBetweenThumbs` * `step` should reflect the gap between the thumbs. - `step: 1` and `minStepsBetweenThumbs: 10` => gap is `10` - `step: 10` and `minStepsBetweenThumbs: 2` => gap is `20` |
origin | \start\ | 'center' | 'start' | 'end'The origin of the slider range. The track is filled from the origin to the thumb for single values. - "start": Useful when the value represents an absolute value - "center": Useful when the value represents an offset (relative) - "end": Useful when the value represents an offset from the end |
step | 1 | numberThe step value of the slider |
thumbAlignment | \contain\ | 'center' | 'contain'The alignment of the slider thumb relative to the track - `center`: the thumb will extend beyond the bounds of the slider track. - `contain`: the thumb will be contained within the bounds of the track. |
aria-label | string[]The aria-label of each slider thumb. Useful for providing an accessible name to the slider | |
aria-labelledby | string[]The `id` of the elements that labels each slider thumb. Useful for providing an accessible name to the slider | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
defaultValue | number[]The initial value of the slider when rendered. Use when you don't need to control the value of the slider. | |
disabled | booleanWhether the slider is disabled | |
form | stringThe associate form of the underlying input element. | |
getAriaValueText | (details: ValueTextDetails) => stringFunction that returns a human readable value for the slider thumb | |
id | stringThe unique identifier of the machine. | |
ids | Partial<{
root: string
thumb: (index: number) => string
hiddenInput: (index: number) => string
control: string
track: string
range: string
label: string
valueText: string
marker: (index: number) => string
}>The ids of the elements in the slider. Useful for composition. | |
invalid | booleanWhether the slider is invalid | |
name | stringThe name associated with each slider thumb (when used in a form) | |
onFocusChange | (details: FocusChangeDetails) => voidFunction invoked when the slider's focused index changes | |
onValueChange | (details: ValueChangeDetails) => voidFunction invoked when the value of the slider changes | |
onValueChangeEnd | (details: ValueChangeDetails) => voidFunction invoked when the slider value change is done | |
readOnly | booleanWhether the slider is read-only | |
thumbSize | { width: number; height: number }The slider thumbs dimensions | |
value | number[]The controlled value of the slider |
Marker
| Prop | Default | Type |
|---|---|---|
value* | number | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. |
Thumb
| Prop | Default | Type |
|---|---|---|
index* | number | |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
name | string |