# Elastic Slider

A spring-driven slider. It thickens when you hover, focus or drag it, and if you pull past either end the track stretches like rubber, the end icon pops, and it wobbles back when you let go. It runs horizontal or vertical, as a thin line or a fat pill with icons and the value inside the track, with ticks, a handle, a value bubble and a fill that can grow from the middle. Arrow keys, Page Up/Down, Home/End and (optionally) the scroll wheel also move it. In the demo, switch between the player, control-center, editor and basic scenes in the panel.

<demo-elastic-slider>



</demo-elastic-slider>

## Installation

```bash
npx shadcn-vue@latest add "https://nxui.geoql.in/r/elastic-slider.json"
```

## Usage

```vue
<script setup lang="ts">
  import { ref } from 'vue';
  import ElasticSlider from '~/components/ui/elastic-slider/ElasticSlider.vue';

  const volume = ref(64);
  const exposure = ref(0);

  function signed(value: number) {
    return value > 0 ? `+${Math.round(value)}` : String(Math.round(value));
  }
</script>

<template>
  <ElasticSlider
    v-model="volume"
    left-icon="lucide:volume-1"
    right-icon="lucide:volume-2"
    aria-label="Volume"
  />

  <!-- Two-way fill from the middle with a handle -->
  <ElasticSlider
    v-model="exposure"
    :starting-value="-100"
    :max-value="100"
    :fill-from="0"
    :ticks="[0]"
    thumb
    value-position="none"
    :left-icon="null"
    :right-icon="null"
    :format-value="signed"
    class="w-72"
    aria-label="Exposure"
  />

  <!-- Vertical pill with the icon inside -->
  <ElasticSlider
    orientation="vertical"
    icon-placement="inside"
    value-position="none"
    :right-icon="null"
    :thickness="84"
    :active-thickness="88"
    :radius="28"
    :rest-opacity="1"
    :jump-to-click="false"
    class="h-46"
  >
    <template #left-icon>
      <Icon name="lucide:sun" class="size-6" :size="24" />
    </template>
  </ElasticSlider>
</template>
```

## Props

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        model-value
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Controlled value (<code>
        v-model
      </code>
      
      ). Leave unset for an uncontrolled slider.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        default-value
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        50
      </code>
    </td>
    
    <td>
      Starting value when uncontrolled.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        starting-value
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Lowest value of the range.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        max-value
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        100
      </code>
    </td>
    
    <td>
      Highest value of the range.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        is-stepped
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Snaps the value to <code>
        step-size
      </code>
      
       increments.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        step-size
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        1
      </code>
    </td>
    
    <td>
      Size of each step when <code>
        is-stepped
      </code>
      
       is on.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        orientation
      </code>
    </td>
    
    <td>
      <code>
        'horizontal' | 'vertical'
      </code>
    </td>
    
    <td>
      <code>
        'horizontal'
      </code>
    </td>
    
    <td>
      Direction of the track. Vertical sliders fill from the bottom.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        left-icon
      </code>
    </td>
    
    <td>
      <code>
        string | null
      </code>
    </td>
    
    <td>
      <code>
        'lucide:volume-1'
      </code>
    </td>
    
    <td>
      Iconify name at the start. <code>
        null
      </code>
      
       hides it. The <code>
        left-icon
      </code>
      
       slot overrides it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        right-icon
      </code>
    </td>
    
    <td>
      <code>
        string | null
      </code>
    </td>
    
    <td>
      <code>
        'lucide:volume-2'
      </code>
    </td>
    
    <td>
      Iconify name at the end. <code>
        null
      </code>
      
       hides it. The <code>
        right-icon
      </code>
      
       slot overrides it.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        icon-placement
      </code>
    </td>
    
    <td>
      <code>
        'outside' | 'inside'
      </code>
    </td>
    
    <td>
      <code>
        'outside'
      </code>
    </td>
    
    <td>
      Icons beside the track, or inside it once the track is thick enough.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        value-position
      </code>
    </td>
    
    <td>
      <code>
        'top' | 'bubble' | 'inside' | 'none'
      </code>
    </td>
    
    <td>
      <code>
        'top'
      </code>
    </td>
    
    <td>
      Where the value shows. The bubble rides above the fill while the slider is active.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        format-value
      </code>
    </td>
    
    <td>
      <code>
        (value: number) => string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Formats the shown value and the value read by screen readers.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ticks
      </code>
    </td>
    
    <td>
      <code>
        boolean | number[]
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
      
       marks every step (or every tenth when not stepped). An array marks those values.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        thumb
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Shows a round handle at the end of the fill.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fill-from
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        starting-value
      </code>
    </td>
    
    <td>
      Value the fill grows from. Set it to the middle for a centered, two-way fill.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        thickness
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        6
      </code>
    </td>
    
    <td>
      Track thickness in px at rest.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        active-thickness
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        12
      </code>
    </td>
    
    <td>
      Track thickness in px while hovered, focused or dragged.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        radius
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Corner radius of the track in px. Fully round when unset.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        rest-opacity
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.7
      </code>
    </td>
    
    <td>
      Opacity while idle. The slider brightens to full when active.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        grow-on-hover
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Hovering activates the slider. When off it only grows while pressed or focused.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        max-stretch
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        50
      </code>
    </td>
    
    <td>
      Furthest the track stretches in px when pulled past an end. <code>
        0
      </code>
      
       turns the stretch off.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        bounce
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.5
      </code>
    </td>
    
    <td>
      How much the track wobbles as it springs back, 0–0.95.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        jump-to-click
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Pressing the track jumps to the pointer. When off, dragging moves the value from where it was.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        wheel
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Lets the scroll wheel change the value.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        track-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'color-mix(in srgb, currentColor 18%, transparent)'
      </code>
    </td>
    
    <td>
      Color of the empty track.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        range-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'currentColor'
      </code>
    </td>
    
    <td>
      Color of the fill.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        icon-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'currentColor'
      </code>
    </td>
    
    <td>
      Color of the outside icons.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        content-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'currentColor'
      </code>
    </td>
    
    <td>
      Color of icons, values and ticks over the empty track.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        fill-content-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'auto'
      </code>
    </td>
    
    <td>
      Color of icons, values and ticks over the fill. <code>
        auto
      </code>
      
       picks light or dark from the fill color.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        thumb-color
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        '#ffffff'
      </code>
    </td>
    
    <td>
      Color of the handle.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        disabled
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Turns off interaction and dims the slider.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        aria-label
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        'Slider'
      </code>
    </td>
    
    <td>
      Accessible name of the slider.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Extra classes for the root (e.g. <code>
        w-full
      </code>
      
      , <code>
        h-60
      </code>
      
      ).
    </td>
  </tr>
</tbody>
</table>

## Events

<table>
<thead>
  <tr>
    <th>
      Event
    </th>
    
    <th>
      Payload
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        update:modelValue
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Fired as the value changes (<code>
        v-model
      </code>
      
      ).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        change
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Fired as the value changes.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        change-end
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      Fired when a drag, key press or scroll finishes.
    </td>
  </tr>
</tbody>
</table>

## Slots

<table>
<thead>
  <tr>
    <th>
      Slot
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        left-icon
      </code>
    </td>
    
    <td>
      Custom start icon (replaces <code>
        left-icon
      </code>
      
      ).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        right-icon
      </code>
    </td>
    
    <td>
      Custom end icon (replaces <code>
        right-icon
      </code>
      
      ).
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- The track is a `role="slider"` element with `aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext` (from `format-value`) and `aria-orientation`.
- Arrow keys move one step (or 1% of the range), Page Up/Down move a tenth, Home/End jump to the ends. Pressing past an end plays the rubber-band bump.
- A focus ring shows on keyboard focus only, and keyboard focus also grows the track.
- With `prefers-reduced-motion: reduce`, springs snap straight to their target and the end-icon pulse is skipped.
- Ported from [React Bits](https://reactbits.dev/components/elastic-slider) by David Haz.
