@@ -16,6 +16,7 @@ const variants = {
1616 // light track
1717 thumb :
1818 "h-4.5 w-4.5 border border-border-bright bg-white shadow-sm dark:border-transparent dark:bg-charcoal-200 dark:shadow-none" ,
19+ thumbSize : 18 ,
1920 } ,
2021 tertiary : {
2122 container : "h-6 gap-1 rounded-sm hover:bg-background-raised px-1" ,
@@ -25,6 +26,7 @@ const variants = {
2526 range : "bg-transparent group-hover:bg-secondary" ,
2627 thumb :
2728 "h-3 w-3 border-2 border-text-dimmed bg-grid-bright shadow-[0_1px_3px_4px_rgb(0_0_0/0.2),0_1px_2px_-1px_rgb(0_0_0/0.1)] hover:border-text-dimmed focus:shadow-[0_1px_3px_4px_rgb(0_0_0/0.2),0_1px_2px_-1px_rgb(0_0_0/0.1)]" ,
29+ thumbSize : 12 ,
2830 } ,
2931} ;
3032
@@ -40,6 +42,8 @@ export type SliderProps = ComponentProps<typeof RadixSlider.Root> & {
4042 * tracks the handle exactly. Reads the controlled `value`, so pass one.
4143 */
4244 valueTooltip ?: ( value : number ) => string ;
45+ /** Values to tick on the track, e.g. the setting's default. */
46+ marks ?: number [ ] ;
4347} ;
4448
4549export function Slider ( {
@@ -49,13 +53,16 @@ export function Slider({
4953 TrailingIcon,
5054 "aria-label" : ariaLabel ,
5155 valueTooltip,
56+ marks,
5257 ...props
5358} : SliderProps ) {
5459 const variation = variants [ variant ] ;
5560 // The pointer leaves the thumb while dragging, so hover alone can't keep the
5661 // label up.
5762 const [ isDragging , setIsDragging ] = useState ( false ) ;
5863 const currentValue = props . value ?. [ 0 ] ?? props . defaultValue ?. [ 0 ] ?? 0 ;
64+ const min = props . min ?? 0 ;
65+ const max = props . max ?? 100 ;
5966
6067 return (
6168 < div className = { cn ( "group flex items-center" , variation . container ) } >
@@ -83,6 +90,23 @@ export function Slider({
8390 < RadixSlider . Track className = { cn ( "relative grow rounded-full" , variation . track ) } >
8491 < RadixSlider . Range className = { cn ( "absolute h-full rounded-full" , variation . range ) } />
8592 </ RadixSlider . Track >
93+ { marks ?. map ( ( mark ) => {
94+ const percent = ( ( mark - min ) / ( max - min ) ) * 100 ;
95+ if ( ! Number . isFinite ( percent ) || percent < 0 || percent > 100 ) return null ;
96+ // Radix keeps the thumb inside the track by offsetting it against its
97+ // own width, so a plain percentage would sit off the handle. Same
98+ // formula as its `getThumbInBoundsOffset`, so the tick lands under
99+ // the thumb's centre.
100+ const offset = variation . thumbSize * ( 0.5 - percent / 100 ) ;
101+ return (
102+ < span
103+ key = { mark }
104+ aria-hidden
105+ className = "absolute top-1/2 h-2 w-px -translate-x-1/2 -translate-y-1/2 bg-text-dimmed"
106+ style = { { left : `calc(${ percent } % + ${ offset } px)` } }
107+ />
108+ ) ;
109+ } ) }
86110 { /* The thumb is the role="slider" element, so the label lives here */ }
87111 < RadixSlider . Thumb
88112 aria-label = { ariaLabel }
@@ -94,13 +118,16 @@ export function Slider({
94118 { valueTooltip && (
95119 < span
96120 className = { cn (
97- "pointer-events-none absolute bottom-full left-1/2 mb-2 -translate-x-1/2 rounded border border-grid-bright bg-background-bright px-1.5 py-0.5 text-xs tabular-nums text-text-bright shadow-md transition-opacity" ,
121+ "pointer-events-none absolute bottom-full left-1/2 mb-2.5 -translate-x-1/2 rounded border border-grid-bright bg-background-bright px-1.5 py-0.5 text-xs tabular-nums text-text-bright shadow-md transition-opacity" ,
98122 // Deliberately not keyed off focus: the thumb keeps focus after a
99123 // click, which would leave the label stuck on.
100124 isDragging ? "opacity-100" : "opacity-0 group-hover/thumb:opacity-100"
101125 ) }
102126 >
103127 { valueTooltip ( currentValue ) }
128+ { /* Straddles the bottom edge, hiding the border it overlaps, so the
129+ two outer sides read as an arrow pointing at the handle. */ }
130+ < span className = "absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 border-b border-l border-grid-bright bg-background-bright" />
104131 </ span >
105132 ) }
106133 </ RadixSlider . Thumb >
0 commit comments