Svelte 5 component · fractalsvelte-ui · Forms
Slider
A precision numeric range slider featuring a styled track, fill bar, and draggable thumb. Exposes current values through a bindable value and renders optional floating tooltips during interaction.
Install
Slider is a Svelte 5 slider component in @fractaldesign/fractalsvelte-ui. It works in SvelteKit and in any Svelte 5 app.
pnpm add @fractaldesign/fractalsvelte-uiImport
import { Slider } from '@fractaldesign/fractalsvelte-ui';Usage
<Slider value={0} min={0} max={100} />Props
Also accepts every attribute of HTMLAttributes, spread onto
the root element.
| Name | Type | Default |
|---|---|---|
value bindable | number | 0 |
min | number | 0 |
max | number | 100 |
step | number | 1 |
label | string | — |
description | string | — |
showTooltip | boolean | false |
showValue | boolean | false |
formatValue | (val: number) => string | (v: number) => String(v) |
disabled | boolean | false |
size | Size 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' |
id | string | — |
class | string | '' |
Source
Show the Slider.svelte source
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import type { Size } from '../../types';
interface Props extends HTMLAttributes<HTMLDivElement> {
value?: number;
min?: number;
max?: number;
step?: number;
label?: string;
description?: string;
showTooltip?: boolean;
showValue?: boolean;
formatValue?: (val: number) => string;
disabled?: boolean;
size?: Size;
id?: string;
class?: string;
}
let {
value = $bindable(0),
min = 0,
max = 100,
step = 1,
label,
description,
showTooltip = false,
showValue = false,
formatValue = (v: number) => String(v),
disabled = false,
size = 'md',
id,
class: className = '',
...restProps
}: Props = $props();
let trackEl: HTMLDivElement | null = $state(null);
let isDragging = $state(false);
let isHovering = $state(false);
const autoId = 'f-sld-' + Math.random().toString(36).slice(2, 9);
const sliderId = $derived(id || autoId);
const percentage = $derived.by(() => {
if (max <= min) return 0;
const clamped = Math.max(min, Math.min(max, value));
return ((clamped - min) / (max - min)) * 100;
});
function roundToStep(val: number): number {
const steps = Math.round((val - min) / step);
const raw = min + steps * step;
const decimals = (step.toString().split('.')[1] || '').length;
return Number(Math.max(min, Math.min(max, raw)).toFixed(decimals));
}
function updateValueFromPointer(clientX: number) {
if (!trackEl || disabled) return;
const rect = trackEl.getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const rawVal = min + ratio * (max - min);
value = roundToStep(rawVal);
}
function handlePointerDown(e: PointerEvent) {
if (disabled) return;
e.preventDefault();
isDragging = true;
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
updateValueFromPointer(e.clientX);
}
function handlePointerMove(e: PointerEvent) {
if (!isDragging || disabled) return;
updateValueFromPointer(e.clientX);
}
function handlePointerUp(e: PointerEvent) {
if (isDragging) {
isDragging = false;
try {
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
} catch {}
}
}
function handleKeyDown(e: KeyboardEvent) {
if (disabled) return;
let nextVal = value;
if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
e.preventDefault();
nextVal = Math.min(max, value + step);
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
e.preventDefault();
nextVal = Math.max(min, value - step);
} else if (e.key === 'PageUp') {
e.preventDefault();
nextVal = Math.min(max, value + step * 10);
} else if (e.key === 'PageDown') {
e.preventDefault();
nextVal = Math.max(min, value - step * 10);
} else if (e.key === 'Home') {
e.preventDefault();
nextVal = min;
} else if (e.key === 'End') {
e.preventDefault();
nextVal = max;
} else {
return;
}
value = roundToStep(nextVal);
}
</script>
<div
class="f-slider-container {className}"
data-size={size}
data-disabled={disabled || undefined}
{...restProps}
>
{#if label || showValue}
<div class="f-slider-header">
{#if label}
<label class="f-slider-label" for={sliderId}>{label}</label>
{/if}
{#if showValue}
<span class="f-slider-value-badge">{formatValue(value)}</span>
{/if}
</div>
{/if}
{#if description}
<span class="f-slider-desc">{description}</span>
{/if}
<div
bind:this={trackEl}
role="presentation"
class="f-slider-track-wrap"
onpointerdown={handlePointerDown}
onpointermove={handlePointerMove}
onpointerup={handlePointerUp}
onpointercancel={handlePointerUp}
onmouseenter={() => (isHovering = true)}
onmouseleave={() => (isHovering = false)}
>
<div class="f-slider-track">
<div class="f-slider-fill" style="width: {percentage}%;"></div>
</div>
<div
id={sliderId}
role="slider"
tabindex={disabled ? -1 : 0}
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
aria-disabled={disabled || undefined}
onkeydown={handleKeyDown}
class="f-slider-thumb"
style="left: {percentage}%;"
>
{#if (showTooltip && (isDragging || isHovering))}
<div class="f-slider-tooltip" aria-hidden="true">
{formatValue(value)}
</div>
{/if}
</div>
</div>
</div>
<style>
@layer fractalsvelte-ui.components {
.f-slider-container {
display: flex;
flex-direction: column;
gap: var(--f-space-2xs);
width: 100%;
font-family: inherit;
box-sizing: border-box;
user-select: none;
}
.f-slider-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.f-slider-label {
font-size: var(--f-text-sm);
font-weight: 500;
color: var(--f-text);
}
.f-slider-value-badge {
font-size: var(--f-text-xs);
font-weight: 600;
color: var(--f-primary);
font-variant-numeric: tabular-nums;
}
.f-slider-desc {
font-size: var(--f-text-xs);
color: var(--f-text-muted);
margin-top: -2px;
}
.f-slider-track-wrap {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 24px;
cursor: pointer;
touch-action: none;
}
.f-slider-track {
position: relative;
width: 100%;
background: var(--f-sunken);
border: 1px solid var(--f-border);
border-radius: var(--f-radius-full);
overflow: hidden;
}
.f-slider-fill {
height: 100%;
background: var(--f-primary);
border-radius: var(--f-radius-full);
transition: width 0.05s ease-out;
}
.f-slider-thumb {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
background: #ffffff;
border: 2px solid var(--f-primary);
border-radius: var(--f-radius-full);
box-shadow: var(--f-shadow-sm);
cursor: grab;
outline: none;
box-sizing: border-box;
transition: transform var(--f-duration-fast) var(--f-ease),
box-shadow var(--f-duration-fast) var(--f-ease);
}
.f-slider-thumb:hover {
transform: translate(-50%, -50%) scale(1.15);
}
.f-slider-thumb:focus-visible {
box-shadow: 0 0 0 3px var(--f-ring);
transform: translate(-50%, -50%) scale(1.15);
}
.f-slider-track-wrap:active .f-slider-thumb {
cursor: grabbing;
transform: translate(-50%, -50%) scale(1.1);
}
/* Floating Tooltip */
.f-slider-tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%);
padding: 2px 6px;
background: var(--f-panel);
color: var(--f-text);
border: 1px solid var(--f-border);
border-radius: var(--f-radius-sm);
font-size: var(--f-text-xs);
font-weight: 600;
white-space: nowrap;
box-shadow: var(--f-shadow-md);
pointer-events: none;
animation: f-sld-fade 0.15s ease-out;
}
@keyframes f-sld-fade {
from {
opacity: 0;
transform: translate(-50%, 4px);
}
to {
opacity: 1;
transform: translate(-50%, 0);
}
}
/* Sizes */
.f-slider-container[data-size='sm'] .f-slider-track {
height: 4px;
}
.f-slider-container[data-size='sm'] .f-slider-thumb {
width: 14px;
height: 14px;
}
.f-slider-container[data-size='md'] .f-slider-track {
height: 6px;
}
.f-slider-container[data-size='md'] .f-slider-thumb {
width: 18px;
height: 18px;
}
.f-slider-container[data-size='lg'] .f-slider-track {
height: 8px;
}
.f-slider-container[data-size='lg'] .f-slider-thumb {
width: 22px;
height: 22px;
}
/* Disabled State */
.f-slider-container[data-disabled='true'] {
opacity: 0.55;
cursor: not-allowed;
}
.f-slider-container[data-disabled='true'] .f-slider-track-wrap,
.f-slider-container[data-disabled='true'] .f-slider-thumb {
cursor: not-allowed;
}
}
</style>Related components
Switch A tactile toggle switch control featuring smooth sliding spring transitions and optional track or thumb…TagInput An interactive multi-tag input creating removable chip pills upon Enter or comma keypress.TextArea A multi-line text input field supporting dynamic height auto-resizing and character limit tracking.Checkbox An accessible selection control supporting checked, unchecked, and indeterminate states.ColorPicker An integrated color selection interface pairing a visual swatch trigger with hex text input and palette…FileInput A drag-and-drop file upload zone providing file type validation, size restriction checks, and an interactive…