feat(Slider): create reusable slider component - a styled version of bits-ui slider

This commit is contained in:
Ilia Mashkov
2026-02-08 14:18:17 +03:00
parent 7f01a9cc85
commit b16928ac80
3 changed files with 157 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Slider from './Slider.svelte';
const { Story } = defineMeta({
title: 'Shared/Slider',
component: Slider,
tags: ['autodocs'],
parameters: {
docs: {
description: {
component: 'Styled bits-ui slider component for selecting a value within a range.',
},
story: { inline: false }, // Render stories in iframe for state isolation
},
},
argTypes: {
value: {
control: 'number',
description: 'Current value (two-way bindable)',
},
min: {
control: 'number',
description: 'Minimum value',
},
max: {
control: 'number',
description: 'Maximum value',
},
step: {
control: 'number',
description: 'Step size for value increments',
},
},
});
</script>
<script lang="ts">
let minValue = 0;
let maxValue = 100;
let stepValue = 1;
let value = $state(50);
</script>
<Story name="Horizontal" args={{ orientation: 'horizontal', min: minValue, max: maxValue, step: stepValue, value }}>
<Slider bind:value min={minValue} max={maxValue} step={stepValue} />
</Story>
<Story name="Vertical" args={{ orientation: 'vertical', min: minValue, max: maxValue, step: stepValue, value }}>
<Slider bind:value min={minValue} max={maxValue} step={stepValue} orientation="vertical" />
</Story>

View File

@@ -0,0 +1,105 @@
<!--
Component: Slider
Styled bits-ui slider component with single value.
-->
<script lang="ts">
import { cn } from '$shared/shadcn/utils/shadcn-utils';
import {
Slider,
type SliderRootProps,
} from 'bits-ui';
type Props = Omit<SliderRootProps, 'type' | 'onValueChange' | 'onValueCommit'> & {
/**
* Slider value, numeric.
*/
value: number;
/**
* A callback function called when the value changes.
* @param newValue - number
*/
onValueChange?: (newValue: number) => void;
/**
* A callback function called when the user stops dragging the thumb and the value is committed.
* @param newValue - number
*/
onValueCommit?: (newValue: number) => void;
};
let { value = $bindable(), orientation = 'horizontal', class: className, ...rest }: Props = $props();
</script>
<Slider.Root
bind:value={value}
class={cn(
'relative flex h-full w-6 touch-none select-none items-center justify-center',
orientation === 'horizontal' ? 'w-48 h-6' : 'w-6 h-48',
className,
)}
type="single"
{orientation}
{...rest}
>
{#snippet children(props)}
<span
{...props}
class={cn('relative bg-gray-200 rounded-full', orientation === 'horizontal' ? 'w-full h-px' : 'h-full w-px')}
>
<!-- Filled range with NO transition -->
<Slider.Range
class={cn('absolute bg-gray-900 rounded-full', orientation === 'horizontal' ? 'h-full' : 'w-full')}
/>
<Slider.Thumb
index={0}
class={cn(
'group/thumb relative block',
orientation === 'horizontal' ? '-top-1 w-2 h-2.25' : '-left-1 h-2 w-2.25',
'rounded-sm',
'bg-gray-900',
// Glow shadow
'shadow-[0_0_6px_rgba(0,0,0,0.4)]',
// Smooth transitions only for size/position
'duration-200 ease-out',
orientation === 'horizontal' ? 'transition-[height,top,left,box-shadow]' : 'transition-[width,top,left,box-shadow]',
// Hover: bigger glow
'hover:shadow-[0_0_10px_rgba(0,0,0,0.5)]',
orientation === 'horizontal' ? 'hover:h-3 hover:-top-[5.5px]' : 'hover:w-3 hover:-left-[5.5px]',
// Active: smaller glow
'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]',
'focus:outline-none',
'cursor-grab active:cursor-grabbing',
)}
>
<!-- Soft glow on hover -->
<div
class="
absolute inset-0 rounded-sm
bg-white/20
opacity-0 group-hover/thumb:opacity-100
transition-opacity duration-200
"
>
</div>
<!-- Value label -->
<span
class={cn(
'absolute',
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',
'bg-gray-900/90 backdrop-blur-sm',
'font-mono text-[0.625rem] font-medium text-white ',
'opacity-0 group-hover/thumb:opacity-100',
'transition-all duration-300',
'pointer-events-none',
'shadow-sm',
)}
>
{value}
</span>
</Slider.Thumb>
</span>
{/snippet}
</Slider.Root>

View File

@@ -12,4 +12,5 @@ export { default as Logo } from './Logo/Logo.svelte';
export { default as SearchBar } from './SearchBar/SearchBar.svelte'; export { default as SearchBar } from './SearchBar/SearchBar.svelte';
export { default as Section } from './Section/Section.svelte'; export { default as Section } from './Section/Section.svelte';
export { default as Skeleton } from './Skeleton/Skeleton.svelte'; export { default as Skeleton } from './Skeleton/Skeleton.svelte';
export { default as Slider } from './Slider/Slider.svelte';
export { default as VirtualList } from './VirtualList/VirtualList.svelte'; export { default as VirtualList } from './VirtualList/VirtualList.svelte';