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. ᕕ( ゚ ∀。)ᕗ
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.
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
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']];
}>()