Every little fish swimming around is a visitor on this page right now, and one of them is you ( ´ ▽ ` )ノ If they get in the way of reading, feel free to turn them off.
Naughty Button
buttonA button that runs further and further away when disabled. ᕕ( ゚ ∀。)ᕗ
The more you try to catch it, the farther it runs, just like your cat. (._.`)
Tech Keywords
Section titled “Tech Keywords”| Name | Description |
|---|---|
| Vector Math | Math operations for direction, acceleration, velocity, and more |
| Pointer Events | Detects pointer movement, clicks, hovers, and more, providing coordinates and target information |
| CSS Animation | Implemented with CSS transition and animation |
| IntersectionObserver | Detects when elements enter or leave the viewport |
Examples
Section titled “Examples”Basic Usage
Section titled “Basic Usage”When the button is disabled and you hover, click, or press Enter on it, it starts running away!
<template> <div class="example-wrap w-full flex flex-col gap-4"> <div class="example-ctrl flex flex-col gap-4"> <base-checkbox v-model="disabled" :label="t('disableButton')" />
<base-input v-model="text" :placeholder="t('inputPlaceholder')" class="w-full" /> </div>
<div class="flex justify-center"> <btn-naughty :label="t('naughtyButtonLabel')" class="font-bold" :disabled="disabled" z-index="30" /> </div> </div></template>
<script setup lang="ts">import { ref } from 'vue'import { useI18n } from 'vue-i18n'import BaseCheckbox from '../../base-checkbox.vue'import BaseInput from '../../base-input.vue'import BtnNaughty from '../btn-naughty.vue'
const text = ref('')const disabled = ref(true)
const { t } = useI18n()</script>Moving Distance
Section titled “Moving Distance”Specify maxDistanceMultiple to set the maximum moving distance multiplier (based on its own width/height). If the button runs out of the specified range or off the screen, it will automatically return to its original position.
<template> <div class="w-full flex flex-col items-center justify-center gap-4 example-wrap"> <base-input v-model="maxMultiple" type="number" outlined :label="t('倍數')" />
<btn-naughty :label="t('按鈕')" class="font-bold" disabled :max-distance-multiple="maxMultiple" z-index="30" /> </div></template>
<script setup lang="ts">import { ref } from 'vue'import { useI18n } from 'vue-i18n'import BaseInput from '../../base-input.vue'import BtnNaughty from '../btn-naughty.vue'
const { t } = useI18n()
const maxMultiple = ref(3)</script>Calling Methods
Section titled “Calling Methods”Besides its default behavior, you can also directly call methods to trigger actions.
<template> <div class="w-full flex flex-col items-center justify-center gap-4 example-wrap"> <div class="example-ctrl w-full flex gap-4"> <base-btn :label="t('move')" @click="run" /> <base-btn :label="t('back')" @click="back" /> </div>
<btn-naughty ref="btn" :label="t('buttonLabel')" z-index="30" /> </div></template>
<script setup lang="ts">import { ref } from 'vue'import { useI18n } from 'vue-i18n'import BaseBtn from '../../base-btn.vue'import BtnNaughty from '../btn-naughty.vue'
const { t } = useI18n()
const btn = ref<InstanceType<typeof BtnNaughty>>()function run() { btn.value?.run()}function back() { btn.value?.back()}</script>Custom Button
Section titled “Custom Button”You can use the default slot to customize the button's appearance.
<template> <div class="flex justify-center example-wrap"> <btn-naughty disabled z-index="30" > <div class="custom-button"> {{ t('customButton') }} </div> </btn-naughty> </div></template>
<script setup lang="ts">import { useI18n } from 'vue-i18n'import BtnNaughty from '../btn-naughty.vue'
const { t } = useI18n()</script>
<style scoped lang="sass">.custom-button background: #ff8345 color: white padding: 0.5rem 1.5rem border-radius: 999rem</style>Custom Rubbing Effect
Section titled “Custom Rubbing Effect”You ask if the rubbing effect can be customized? Of course! When have I ever said no?
Use the rubbing slot to customize the button's rubbing effect content.
<template> <div class="flex justify-center example-wrap"> <btn-naughty disabled z-index="30" > <template #rubbing> <div class="rubbing"> {{ t('啪!跑了') }} </div> </template>
<template #default> <div class="btn"> {{ t('自定義按鈕') }} </div> </template> </btn-naughty> </div></template>
<script setup lang="ts">import { useI18n } from 'vue-i18n'import BtnNaughty from '../btn-naughty.vue'
const { t } = useI18n()</script>
<style scoped lang="sass">.btn padding: 0.5rem 1.5rem background: #26A69A border-radius: 999rem font-weight: bold color: white cursor: pointer
.rubbing padding: 0.5rem 1.5rem background: light-dark(#FEFEFE, #333) border-radius: 999rem border: 1px dashed light-dark(#777, #999) text-align: center</style>Slot Props
Section titled “Slot Props”Using slot props allows for even more creative possibilities.
<template> <div class="flex justify-center example-wrap"> <btn-naughty disabled z-index="30" > <template #rubbing="{ isRunning }"> <div class="rubbing"> {{ isRunning ? '😜' : '😗' }} </div> </template>
<template #default="{ isRunning }"> <div class="btn"> {{ isRunning ? t('點不到咧') : t('點此中獎') }} </div> </template> </btn-naughty> </div></template>
<script setup lang="ts">import { useI18n } from 'vue-i18n'import BtnNaughty from '../btn-naughty.vue'
const { t } = useI18n()</script>
<style scoped lang="sass">.btn padding: 0.5rem 1.5rem background: #26A69A border-radius: 999rem font-weight: bold color: white font-size: 1.25rem cursor: pointer
.rubbing padding: 0.5rem 1.5rem background: light-dark(#FEFEFE, #333) border-radius: 999rem border: 1px dashed light-dark(#777, #999) text-align: center font-size: 1.25rem</style>Form Example
Section titled “Form Example”Can't click it until you fill out the form! (╯•̀ὤ•́)╯
<template> <div class="relative w-full flex justify-center example-wrap"> <div class="max-w-[20rem] flex flex-col gap-4"> <base-input v-model="form.username" :label="t('帳號 *')" class="w-full" />
<base-input v-model="form.password" type="password" :label="t('密碼 *')" class="w-full" />
<div class="mt-3 flex justify-center"> <btn-naughty :label="t('送出表單')" :disabled z-index="30" @click="handleSubmit" > <template #rubbing> <div class="rubbing"> {{ t('請完成表單') }} </div> </template> </btn-naughty> </div> </div>
<transition name="opacity"> <div v-if="isSubmitted" class="absolute inset-0 z-[40] flex flex-col items-center justify-center gap-6 rounded-xl bg-slate-600 bg-opacity-90 text-white" @click="reset" > <span class="text-xl tracking-wide"> {{ t('表單已送出!(*´∀`)~♥') }} </span>
<span class="cursor-pointer text-xs"> {{ t('點一下再來一次') }} </span> </div> </transition> </div></template>
<script setup lang="ts">import { computed, ref } from 'vue'import { useI18n } from 'vue-i18n'import BaseInput from '../../base-input.vue'import BtnNaughty from '../btn-naughty.vue'
const { t } = useI18n()
const form = ref({ username: '', password: '',})const disabled = computed(() => { return form.value.username === '' || form.value.password === ''})
const isSubmitted = ref(false)function handleSubmit() { if (disabled.value) { return } isSubmitted.value = true}
function reset() { isSubmitted.value = false
form.value = { username: '', password: '', }}</script>
<style lang="sass" scoped>.rubbing padding: 0.75rem color: light-dark(#ff7530, #ff9660) opacity: 0.8 border: 1px dashed light-dark(#ff7530, #ff9660) border-radius: 0.2rem white-space: nowrap text-align: center
.opacity-enter-active, .opacity-leave-active transition-duration: 0.4s.opacity-enter-from, .opacity-leave-to opacity: 0 !important</style>How it Works
Section titled “How it Works”When the mouse touches the button, it calculates the unit vector from the mouse position to the button's center. Based on this vector, it moves a distance equivalent to the button's size.
If the button moves off-screen, it automatically returns to its original position. This is implemented using IntersectionObserver.
📚 What is IntersectionObserver?
Source Code
Section titled “Source Code”interface Props { /** 按鈕內文字 */ label?: string; /** 是否停用 */ disabled?: boolean; /** 同 CSS z-index */ zIndex?: number | string; /** 最大移動距離,為按鈕尺寸倍數 */ maxDistanceMultiple?: number; /** 同 html tabindex */ tabindex?: number | string;}const emit = defineEmits<{ (e: 'click'): void; /** 開始移動時 */ (e: 'run'): void; /** 開始返回時 */ (e: 'back'): void;}>()Methods
Section titled “Methods”interface Expose { /** 移動 */ run: () => void; /** 返回原點 */ back: () => void; /** 是否正在移動 */ isRunning: Ref<boolean>;}defineSlots<{ /** 按鈕 */ default?: (props: { isRunning: boolean }) => unknown; /** 拓印 */ rubbing?: (props: { isRunning: boolean }) => unknown;}>()If you have any suggestions or questions, feel free to email me( ´ ▽ ` )ノ
If you like cool components, feel free to " Buy me a cup of coffee " to show your support!