feat(Slider): tweak styles for a knob and add slider label

This commit is contained in:
Ilia Mashkov
2026-02-18 16:55:11 +03:00
parent f356851d97
commit 147df04c22
2 changed files with 76 additions and 45 deletions

View File

@@ -98,11 +98,11 @@ const handleInputChange: ChangeEventHandler<HTMLInputElement> = event => {
function calculateScale(index: number): number | string { function calculateScale(index: number): number | string {
const calculate = () => const calculate = () =>
orientation === 'horizontal' orientation === 'horizontal'
? (control.min + (index * (control.max - control.min) / 4)) ? control.min + (index * (control.max - control.min)) / 4
: (control.max - (index * (control.max - control.min) / 4)); : control.max - (index * (control.max - control.min)) / 4;
return Number.isInteger(control.step) return Number.isInteger(control.step)
? Math.round(calculate()) ? Math.round(calculate())
: (calculate()).toFixed(2); : calculate().toFixed(2);
} }
</script> </script>
@@ -111,7 +111,9 @@ function calculateScale(index: number): number | string {
class={cn( class={cn(
'flex gap-4 sm:py-4 sm:px-1 rounded-xl transition-all duration-300', 'flex gap-4 sm:py-4 sm:px-1 rounded-xl transition-all duration-300',
'', '',
orientation === 'horizontal' ? 'flex-row items-end w-full' : 'flex-col items-center h-full', orientation === 'horizontal'
? 'flex-row items-end w-full'
: 'flex-col items-center h-full',
className, className,
)} )}
> >
@@ -120,7 +122,9 @@ function calculateScale(index: number): number | string {
<div <div
class={cn( class={cn(
'absolute flex justify-between', 'absolute flex justify-between',
orientation === 'horizontal' ? 'flex-row w-full -top-5 px-0.5' : 'flex-col h-full -left-5 py-0.5', orientation === 'horizontal'
? 'flex-row w-full -top-8 px-0.5'
: 'flex-col h-full -left-5 py-0.5',
)} )}
> >
{#each Array(5) as _, i} {#each Array(5) as _, i}
@@ -133,7 +137,12 @@ function calculateScale(index: number): number | string {
<span class="font-mono text-[0.375rem] text-text-muted tabular-nums"> <span class="font-mono text-[0.375rem] text-text-muted tabular-nums">
{calculateScale(i)} {calculateScale(i)}
</span> </span>
<div class={cn('bg-border-muted', orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1')}> <div
class={cn(
'bg-border-muted',
orientation === 'horizontal' ? 'w-px h-1' : 'h-px w-1',
)}
>
</div> </div>
</div> </div>
{/each} {/each}
@@ -146,6 +155,7 @@ function calculateScale(index: number): number | string {
min={control.min} min={control.min}
max={control.max} max={control.max}
step={control.step} step={control.step}
{label}
{orientation} {orientation}
/> />
</div> </div>
@@ -162,16 +172,6 @@ function calculateScale(index: number): number | string {
variant="ghost" variant="ghost"
/> />
{/if} {/if}
{#if label}
<div class="flex items-center gap-2 opacity-70">
<div class="w-1 h-1 rounded-full bg-foreground"></div>
<div class="w-px h-2 bg-text-muted/50"></div>
<span class="font-mono text-[8px] uppercase tracking-[0.2em] text-text-subtle font-medium">
{label}
</span>
</div>
{/if}
</div> </div>
{/snippet} {/snippet}

View File

@@ -9,11 +9,20 @@ import {
type SliderRootProps, type SliderRootProps,
} from 'bits-ui'; } from 'bits-ui';
type Props = Omit<SliderRootProps, 'type' | 'onValueChange' | 'onValueCommit'> & { type Props =
& Omit<
SliderRootProps,
'type' | 'onValueChange' | 'onValueCommit'
>
& {
/** /**
* Slider value, numeric. * Slider value, numeric.
*/ */
value: number; value: number;
/**
* Optional label displayed inline on the track before the filled range.
*/
label?: string;
/** /**
* A callback function called when the value changes. * A callback function called when the value changes.
* @param newValue - number * @param newValue - number
@@ -26,11 +35,17 @@ type Props = Omit<SliderRootProps, 'type' | 'onValueChange' | 'onValueCommit'> &
onValueCommit?: (newValue: number) => void; onValueCommit?: (newValue: number) => void;
}; };
let { value = $bindable(), orientation = 'horizontal', class: className, ...rest }: Props = $props(); let {
value = $bindable(),
orientation = 'horizontal',
class: className,
label,
...rest
}: Props = $props();
</script> </script>
<Slider.Root <Slider.Root
bind:value={value} bind:value
class={cn( class={cn(
'relative flex h-full w-6 touch-none select-none items-center justify-center', 'relative flex h-full w-6 touch-none select-none items-center justify-center',
orientation === 'horizontal' ? 'w-48 h-6' : 'w-6 h-48', orientation === 'horizontal' ? 'w-48 h-6' : 'w-6 h-48',
@@ -41,13 +56,23 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
{...rest} {...rest}
> >
{#snippet children(props)} {#snippet children(props)}
{#if label && orientation === 'horizontal'}
<span class="absolute top-0 left-0 -translate-y-1/2 text-[0.5rem] uppercase text-gray-400">
{label}
</span>
{/if}
<span <span
{...props} {...props}
class={cn('relative bg-background-muted rounded-full', orientation === 'horizontal' ? 'w-full h-px' : 'h-full w-px')} class={cn(
'relative bg-background-muted rounded-full',
orientation === 'horizontal' ? 'w-full h-px' : 'h-full w-px',
)}
> >
<!-- Filled range with NO transition -->
<Slider.Range <Slider.Range
class={cn('absolute bg-foreground rounded-full', orientation === 'horizontal' ? 'h-full' : 'w-full')} class={cn(
'absolute bg-foreground rounded-full',
orientation === 'horizontal' ? 'h-full' : 'w-full',
)}
/> />
<Slider.Thumb <Slider.Thumb
@@ -56,27 +81,32 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
'group/thumb relative block', 'group/thumb relative block',
'size-2', 'size-2',
orientation === 'horizontal' ? '-top-1' : '-left-1', orientation === 'horizontal' ? '-top-1' : '-left-1',
'rounded-sm', 'rounded-full',
'bg-foreground', 'bg-foreground',
// Glow shadow // Glow shadow
'shadow-[0_0_6px_rgba(0,0,0,0.4)]', 'shadow-[0_0_6px_rgba(0,0,0,0.4)]',
// Smooth transitions only for size/position // Smooth transitions only for size/position
'duration-200 ease-out', 'duration-200 ease-out',
orientation === 'horizontal' ? 'transition-[height,top,left,box-shadow]' : 'transition-[width,top,left,box-shadow]', orientation === 'horizontal'
? 'transition-[height,top,left,box-shadow]'
: 'transition-[width,top,left,box-shadow]',
// Hover: bigger glow // Hover: bigger glow
'hover:shadow-[0_0_10px_rgba(0,0,0,0.5)]', 'hover:shadow-[0_0_10px_rgba(0,0,0,0.5)]',
orientation === 'horizontal' ? 'hover:size-3 hover:-top-[5.5px]' : 'hover:size-3 hover:-left-[5.5px]', orientation === 'horizontal'
? 'hover:size-3 hover:-top-[5.5px]'
: 'hover:size-3 hover:-left-[5.5px]',
// Active: smaller glow // Active: smaller glow
'active:shadow-[0_0_4px_rgba(0,0,0,0.3)]', 'active:shadow-[0_0_4px_rgba(0,0,0,0.3)]',
orientation === 'horizontal' ? 'active:h-2.5 active:-top-[4.5px]' : 'active:w-2.5 active:-left-[4.5px]', orientation === 'horizontal'
? 'active:h-2.5 active:-top-[4.5px]'
: 'active:w-2.5 active:-left-[4.5px]',
'focus:outline-none', 'focus:outline-none',
'cursor-grab active:cursor-grabbing', 'cursor-grab active:cursor-grabbing',
)} )}
> >
<!-- Soft glow on hover -->
<div <div
class=" class="
absolute inset-0 rounded-sm absolute inset-0 rounded-full
bg-background-20 bg-background-20
opacity-0 group-hover/thumb:opacity-100 opacity-0 group-hover/thumb:opacity-100
transition-opacity duration-200 transition-opacity duration-200
@@ -84,11 +114,12 @@ let { value = $bindable(), orientation = 'horizontal', class: className, ...rest
> >
</div> </div>
<!-- Value label -->
<span <span
class={cn( class={cn(
'absolute', 'absolute',
orientation === 'horizontal' ? '-top-8 left-1/2 -translate-x-1/2' : 'left-5 top-1/2 -translate-y-1/2', orientation === 'horizontal'
? '-top-8 left-1/2 -translate-x-1/2'
: 'left-5 top-1/2 -translate-y-1/2',
'px-1.5 py-0.5 rounded-md', 'px-1.5 py-0.5 rounded-md',
'bg-foreground/90 backdrop-blur-sm', 'bg-foreground/90 backdrop-blur-sm',
'font-mono text-[0.625rem] font-medium text-background', 'font-mono text-[0.625rem] font-medium text-background',