Skip to content

Fragile Slider slider

Yes, it breaks. (´・ω・`)

Examples

Basic Usage

Pull it too far and the handle snaps off! (´・ω・`)

value: 50
View Example Source Code
vue
<template>
  <div class="w-full flex flex-col gap-4 py-14">
    value: {{ Math.floor(value) }}

    <slider-stubborn
      v-model="value"
      class="w-full"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import SliderStubborn from '../slider-fragile.vue'

const value = ref(50)
</script>

Repair & Snap

You can break it on purpose... or fix it if you’re feeling kind. (´・ω・`)ノ

View Example Source Code
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <div class="w-full py-12">
      <slider-stubborn
        ref="slider"
        v-model="value"
        class="w-full"
        durable
        thumb-color="#34c600"
        joint-color="#a0d48e"
      />
    </div>

    <div class="flex gap-4">
      <base-btn
        :label="t('repair')"
        class="flex-1"
        @click="sliderRef?.repair()"
      />
      <base-btn
        :label="t('break')"
        class="flex-1"
        @click="sliderRef?.break()"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseBtn from '../../base-btn.vue'
import SliderStubborn from '../slider-fragile.vue'

const sliderRef = useTemplateRef('slider')
const value = ref(50)

const { t } = useI18n()
</script>

Sweetness Preference

Unyielding sweetness level! ◝(´・ω・`)◟

Drink Type:
Sweetness:10 points
Ice:0 points
View Example Source Code
vue
<template>
  <div class="w-full flex flex-col gap-6 py-6">
    <div class="flex flex-col items-start gap-4 border border-gray-300 rounded-xl p-6">
      <div class="font-bold">
        {{ t('drinkType') }}
      </div>

      <div class="w-full flex flex-wrap justify-between gap-4 whitespace-nowrap">
        <label
          v-for="item, i in list"
          :key="i"
        >
          <input
            v-model="value"
            type="radio"
            :value="item"
          />
          {{ t(`drinkOptions.${item}`) }}
        </label>
      </div>
    </div>

    <div class="grid grid-cols-6 w-full flex-nowrap items-center gap-8">
      <div class="col-span-2 text-lg">
        {{ t('sweetness') }}:10 {{ t('points') }}
      </div>

      <div class="col-span-4 pr-4">
        <slider-stubborn
          ref="slider"
          :model-value="10"
          disabled
          :max="10"
        />
      </div>
    </div>

    <div class="grid grid-cols-6 w-full flex-nowrap items-center gap-8">
      <div class="col-span-2 text-lg">
        {{ t('ice') }}:{{ iceValue }} {{ t('points') }}
      </div>

      <div class="col-span-4 pr-4">
        <slider-stubborn
          v-model="iceValue"
          durable
          :step="3"
          :max="9"
        />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import SliderStubborn from '../slider-fragile.vue'

const sliderRef = useTemplateRef('slider')

const { t } = useI18n()

const value = ref('綠茶')
const list = [
  '綠茶',
  '紅茶',
  '蜂蜜奶茶',
  '鮮奶茶',
  '泰奶',
  '水果茶',
  '檸檬茶',
]

const iceValue = ref(0)

watch(value, () => {
  sliderRef.value?.repair()
})
</script>

How It Works

This slider is made up of two main components:

Main Body (slider-fragile.vue)

Handles the core logic—calculating values, detecting mouse interactions, and sending that info (like ratio and isHeld) to the thumb component.

The Thumb (slider-fragile-thumb.vue)

This is where all the drama happens: breaking, shaking, and flying off into the void.

When the user clicks and drags the thumb (isHeld is true), we measure how far the mouse is from the thumb’s center.

The farther it gets, the shakier it becomes—like it’s being yanked hard.

Here are its main states:

  • Normal: The thumb is neatly positioned using the ratio from the parent component.
  • Broken: If dragged too far (beyond breakLength) and held long enough (minSecondsToBreak), the thumb snaps. isBroken turns true.
  • After Breaking:
    • When the mouse is released, physics kicks in: it launches with initial velocity (based on your dragging), then falls with gravity, bounces off screen edges, and slows down until it... naps. (thumbData.sleep)
    • You can click and drag it again, and it’ll float around with your mouse.

A quirky detail: while in the normal state, the thumb uses absolute positioning; once broken, it switches to fixed so it can freely roam the screen.

API

Props

interface Props {
  modelValue: number;
  disabled?: boolean;
  min?: number;
  max?: number;
  step?: number;
  thumbSize?: number;
  thumbColor?: string;
  jointColor?: string;
  trackClass?: string;
  durable?: boolean;
  breakLength?: number;
  minSecondsToBreak?: number;
}

Emits

const emit = defineEmits<{
  'update:modelValue': [value: Props['modelValue']];
}>()

Methods

interface Expose {
  repair: () => void;
  break: () => void;
}

v0.38.10