Skip to content

Stubborn Slider slider

A slider that never compromises when disabled. (。-`ω´-)

Usage Examples

Basic Usage

When the state is disabled, the more you drag the handle, the longer and tighter it gets. ᕕ( ゚ ∀。)ᕗ

Current Value: 50
View example source code
vue
<!-- #region main-code -->
<template>
  <div class="w-full flex flex-col gap-4">
    <base-checkbox
      v-model="disabled"
      class="w-full border rounded p-4"
      :label="t('disabled')"
    />

    <div class="flex flex-col flex-1 justify-center">
      {{ t('currentValue') }} {{ Math.floor(value) }}

      <slider-stubborn
        v-model="value"
        :disabled="disabled"
        :max-thumb-length="thumbLength"
        class="w-full"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import SliderStubborn from '../slider-stubborn.vue'

const { width, height } = useWindowSize()

const { t } = useI18n()
const disabled = ref(false)
const value = ref(50)

const thumbLength = computed(() =>
  Math.min(width.value, height.value) / 3,
)
</script>
<!-- #endregion main-code -->

<i18n lang="json">
{
  "zh-hant": {
    "currentValue": "目前數值:",
    "disabled": "停用"
  },
  "en": {
    "currentValue": "Current Value:",
    "disabled": "Disabled"
  }
}
</i18n>

Component Props

Styles can be freely adjusted.

Color:
View example source code
vue
<!-- #region main-code -->
<template>
  <div class="w-full flex flex-col gap-10 py-10">
    <div class="mb-10 flex items-center gap-1">
      <base-input
        v-model.number="thumbSize"
        type="range"
        :label="`${t('size')}: ${thumbSize}`"
        class="flex-1"
        :min="10"
        :step="1"
        :max="80"
      />

      <div class="flex-1">
        <div class="text-sm font-bold">
          {{ t('color') }}
        </div>
        <input
          v-model="thumbColor"
          type="color"
          class="h-[40px] w-full"
        >
      </div>
    </div>

    <slider-stubborn
      v-model="value"
      disabled
      :max-thumb-length="thumbMaxLength / 4"
      :thumb-color="thumbColor"
      :thumb-size="thumbSize"
      class="z-[999] w-full"
    />
  </div>
</template>

<script setup lang="ts">
import { useWindowSize } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseInput from '../../../components/base-input.vue'
import SliderStubborn from '../slider-stubborn.vue'

const { width, height } = useWindowSize()

const { t } = useI18n()
const thumbSize = ref(40)
const thumbColor = ref('#FF639B')
const value = ref(50)

const thumbMaxLength = computed(() =>
  Math.min(width.value, height.value),
)
</script>
<!-- #endregion main-code -->

<i18n lang="json">
{
  "zh-hant": {
    "size": "尺寸:",
    "color": "顏色:"
  },
  "en": {
    "size": "Size:",
    "color": "Color:"
  }
}
</i18n>

Plan Selection

Disable the slider for a specific range to emphasize the disabled effect.

Select Your Plan
Basic Fishbowl
1 Just Right
Premium Fishbowl
3~6 Full
Recommended Plan
Luxury Ocean
Choose Your Own
Select Fish Count: 5
View example source code
vue
<!-- #region main-code -->
<template>
  <div class="w-full flex flex-col gap-4 py-10">
    <div class="flex flex-col flex-1 justify-center gap-4">
      <div class="text-lg font-bold opacity-90">
        {{ t('selectPlan') }}
      </div>

      <div class="mb-10 flex gap-4">
        <div
          class="card transform rounded-lg from-gray-400 to-gray-500 bg-gradient-to-bl p-1 shadow-md transition-all hover:shadow-lg hover:-translate-y-0.5"
          :class="{ ' border-[0.3rem]': plan === 'basic' }"
          @click="plan = 'basic'"
        >
          <div class="text-lg font-bold md:text-2xl">
            {{ t('basicFishbowl') }}
          </div>

          <div class="mt-1 text-xs opacity-90 md:text-sm">
            {{ t('basicFishbowlDescription') }}
          </div>
        </div>

        <div
          class="card transform border-2 border-indigo-200 rounded-lg from-blue-400 to-indigo-500 bg-gradient-to-bl p-1 shadow-lg transition-all hover:shadow-xl hover:-translate-y-1"
          :class="{ ' border-[0.3rem]': plan === 'premium' }"
          @click="plan = 'premium'"
        >
          <div class="text-lg font-bold md:text-2xl">
            {{ t('premiumFishbowl') }}
          </div>

          <div class="mt-1 text-xs opacity-90 md:text-sm">
            {{ t('premiumFishbowlDescription') }}
          </div>

          <div
            class="absolute right-0 top-0 translate-x-2 transform rounded-bl-lg rounded-tr-lg bg-yellow-400 px-2 py-1 shadow-sm -translate-y-2"
          >
            <div class="text-xs text-red-900 font-semibold md:text-sm">
              {{ t('recommendedPlan') }}
            </div>
          </div>
        </div>

        <div
          class="card transform border-2 border-pink-200 rounded-lg from-purple-500 to-pink-500 bg-gradient-to-bl p-1 shadow-xl transition-all hover:shadow-2xl hover:-translate-y-1.5"
          :class="{ ' border-[0.3rem]': plan === 'luxury' }"
          @click="plan = 'luxury'"
        >
          <div class="text-lg font-bold md:text-2xl">
            {{ t('luxuryOcean') }}
          </div>

          <div class="mt-1 text-xs opacity-90 md:text-sm">
            {{ t('luxuryOceanDescription') }}
          </div>
        </div>
      </div>

      <div class="text-lg font-bold opacity-90">
        {{ t('selectFishCount') }} {{ Math.floor(sliderValue) }}
      </div>

      <slider-stubborn
        v-model="sliderValue"
        v-bind="disabledParams"
        :min="0"
        :max="10"
        :step="0.1"
        :max-thumb-length="thumbLength"
        :thumb-size="40"
        class="w-full py-4"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import type { ComponentProps } from 'vue-component-type-helpers'
import { useWindowSize } from '@vueuse/core'
import { pipe } from 'remeda'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import SliderStubborn from '../slider-stubborn.vue'

type Props = ComponentProps<typeof SliderStubborn>

type Plan = 'basic' | 'premium' | 'luxury'

const { t } = useI18n()
const { width, height } = useWindowSize()

const plan = ref<Plan>('premium')
const sliderValue = ref(5)

const planRangleMap: Record<Plan, [number, number]> = {
  basic: [1, 1],
  premium: [3, 6],
  luxury: [-1, 11],
}
const disabledParams = computed<
  Pick<Props, 'minDisabled' | 'maxDisabled'>
>(() => pipe(
  planRangleMap[plan.value],
  ([min, max]) => ({
    minDisabled: min,
    maxDisabled: max,
  }),
))

const thumbLength = computed(() =>
  Math.min(width.value, height.value) / 3,
)
</script>

<style lang="sass" scoped>
.card
  display: flex
  flex-direction: column
  justify-content: center
  align-items: center
  aspect-ratio: 1 / 1.3
  flex: 1
  transition-duration: 200ms
  color: white
  cursor: pointer
</style>

<!-- #endregion main-code -->

<i18n lang="json">
{
  "zh-hant": {
    "selectPlan": "選擇喜歡的方案",
    "basicFishbowl": "基本魚缸",
    "basicFishbowlDescription": "1 隻剛剛好",
    "premiumFishbowl": "高級池塘",
    "premiumFishbowlDescription": "3~6 隻吃飽飽",
    "recommendedPlan": "推薦方案",
    "luxuryOcean": "尊爵大海",
    "luxuryOceanDescription": "任你選!",
    "selectFishCount": "可選鱈魚數:"
  },
  "en": {
    "selectPlan": "Select Your Plan",
    "basicFishbowl": "Basic Fishbowl",
    "basicFishbowlDescription": "1 Just Right",
    "premiumFishbowl": "Premium Fishbowl",
    "premiumFishbowlDescription": "3~6 Full",
    "recommendedPlan": "Recommended Plan",
    "luxuryOcean": "Luxury Ocean",
    "luxuryOceanDescription": "Choose Your Own",
    "selectFishCount": "Select Fish Count:"
  }
}
</i18n>

How It Works

Uses an SVG path to create the stretching and bending elastic effect.

For a detailed explanation, see this article.

Warning! Σ(ˊДˋ;)

Do not set overflow to hidden, otherwise the handle will be clipped when it stretches.

Source Code

API

Props

interface Props {
  modelValue: number;
  disabled?: boolean;
  /** 小於此數值也會有 disabled 效果 */
  minDisabled?: number;
  /** 大於此數值也會有 disabled 效果 */
  maxDisabled?: number;
  min?: number;
  max?: number;
  step?: number;
  /** 握把被拉長的最大長度 */
  maxThumbLength?: number;
  thumbSize?: number;
  thumbColor?: string;
  trackClass?: string;
}

Emits

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

v0.54.1