Skip to content

fix(Slider): update tick position calculation for consistent scaling #1375

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fresh-gifts-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"bits-ui": patch
---

fix(Slider): update tick position calculation for consistent scaling
16 changes: 12 additions & 4 deletions docs/content/components/slider.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Allows users to select a value from a continuous range by sliding a
---

<script>
import { APISection, ComponentPreviewV2, SliderDemo, SliderDemoMultiple, Callout } from '$lib/components/index.js'
import { APISection, ComponentPreviewV2, SliderDemo, SliderDemoMultiple, SliderDemoTicks, Callout } from '$lib/components/index.js'
let { schemas } = $props()
</script>

Expand Down Expand Up @@ -151,11 +151,11 @@ If the `value` prop has more than one value, the slider will render multiple thu
{#snippet children({ ticks, thumbs })}
<Slider.Range />

{#each thumbs as index}
{#each thumbs as index (index)}
<Slider.Thumb {index} />
{/each}

{#each ticks as index}
{#each ticks as index (index)}
<Slider.Tick {index} />
{/each}
{/snippet}
Expand All @@ -164,6 +164,14 @@ If the `value` prop has more than one value, the slider will render multiple thu

To determine the number of ticks that will be rendered, you can simply divide the `max` value by the `step` value.

<ComponentPreviewV2 name="slider-demo-ticks" componentName="Slider">

{#snippet preview()}
<SliderDemoTicks />
{/snippet}

</ComponentPreviewV2>

## Single Type

Set the `type` prop to `"single"` to allow only one accordion item to be open at a time.
Expand Down Expand Up @@ -206,7 +214,7 @@ You can use the `orientation` prop to change the orientation of the slider, whic
</Slider.Root>
```

## RTL Support
## RTL

You can use the `dir` prop to change the reading direction of the slider, which defaults to `"ltr"`.

Expand Down
1 change: 1 addition & 0 deletions docs/src/lib/components/demos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export { default as ScrollAreaDemoCustom } from "./scroll-area-demo-custom.svelt
export { default as SeparatorDemo } from "./separator-demo.svelte";
export { default as SliderDemo } from "./slider-demo.svelte";
export { default as SliderDemoMultiple } from "./slider-demo-multiple.svelte";
export { default as SliderDemoTicks } from "./slider-demo-ticks.svelte";
export { default as SwitchDemo } from "./switch-demo.svelte";
export { default as SwitchDemoCustom } from "./switch-demo-custom.svelte";
export { default as TabsDemo } from "./tabs-demo.svelte";
Expand Down
36 changes: 36 additions & 0 deletions docs/src/lib/components/demos/slider-demo-ticks.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
import { Slider } from "bits-ui";

let value = $state([5, 7]);
</script>

<div class="w-full md:max-w-[280px]">
<Slider.Root
step={1}
min={0}
max={10}
type="multiple"
bind:value
class="relative flex w-full touch-none select-none items-center"
>
{#snippet children({ ticks, thumbs })}
<span
class="bg-dark-10 relative h-2 w-full grow cursor-pointer overflow-hidden rounded-full"
>
<Slider.Range class="bg-foreground absolute h-full" />
</span>
{#each thumbs as thumb}
<Slider.Thumb
index={thumb}
class="border-border-input bg-background hover:border-dark-40 focus-visible:ring-foreground dark:bg-foreground dark:shadow-card focus-visible:outline-hidden z-5 block size-[25px] cursor-pointer rounded-full border shadow-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50"
/>
{/each}
{#each ticks as tick}
<Slider.Tick
index={tick}
class="dark:bg-background/20 bg-background z-1 h-2 w-[1px]"
/>
{/each}
{/snippet}
</Slider.Root>
</div>
14 changes: 7 additions & 7 deletions packages/bits-ui/src/lib/bits/slider/slider.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,12 +305,9 @@ class SliderSingleRootState extends SliderBaseRootState {
const currValue = this.opts.value.current;

return Array.from({ length: count }, (_, i) => {
const tickPosition = i * (step / difference) * 100;
const tickPosition = i * step;

const scale = linearScale(
[this.opts.min.current, this.opts.max.current],
this.getThumbScale()
);
const scale = linearScale([0, (count - 1) * step], this.getThumbScale());

const isFirst = i === 0;
const isLast = i === count - 1;
Expand Down Expand Up @@ -623,12 +620,15 @@ class SliderMultiRootState extends SliderBaseRootState {
const currValue = this.opts.value.current;

return Array.from({ length: count }, (_, i) => {
const tickPosition = i * (step / difference) * 100;
const tickPosition = i * step;

const scale = linearScale([0, (count - 1) * step], this.getThumbScale());

const isFirst = i === 0;
const isLast = i === count - 1;
const offsetPercentage = isFirst ? 0 : isLast ? -100 : -50;
const style = getTickStyles(this.direction, tickPosition, offsetPercentage);

const style = getTickStyles(this.direction, scale(tickPosition), offsetPercentage);
const tickValue = min + i * step;
const bounded =
currValue.length === 1
Expand Down