Naughty Button button
A button that runs further and further away when disabled. ᕕ( ゚ ∀。)ᕗ
The more you try to catch it, the farther it runs, just like your cat. (._.`)
Examples
Basic Usage
When the button is disabled and you hover, click, or press Enter on it, it starts running away!
View Example Source Code
<template>
<div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
<div class="flex flex-col gap-4 border rounded p-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
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.
View Example Source Code
<template>
<div class="w-full flex flex-col items-center justify-center gap-4 border border-gray-300 p-6">
<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
Besides its default behavior, you can also directly call methods to trigger actions.
View Example Source Code
<template>
<div class="w-full flex flex-col items-center justify-center gap-4 border border-gray-300 p-6">
<div class="w-full flex gap-4 border rounded p-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
You can use the default
slot to customize the button's appearance.
View Example Source Code
<template>
<div class="flex justify-center border border-gray-300 p-6">
<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
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.
View Example Source Code
<template>
<div class="flex justify-center border border-gray-300 p-6">
<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: #FEFEFE
border-radius: 999rem
border: 1px dashed #777
text-align: center
</style>
Slot Props
Using slot props allows for even more creative possibilities.
View Example Source Code
<template>
<div class="flex justify-center border border-gray-300 p-6">
<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: #FEFEFE
border-radius: 999rem
border: 1px dashed #777
text-align: center
font-size: 1.25rem
</style>
Form Example
Can't click it until you fill out the form! (╯•̀ὤ•́)╯
View Example Source Code
<template>
<div class="relative w-full flex justify-center border border-gray-300 p-6">
<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: #ff7530
opacity: 0.8
border: 1px dashed #ff7530
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
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?
Caution! Σ(ˊДˋ;)
Please don't set overflow
to hidden
, otherwise the button will just poof disappear the moment it moves.
Source Code
API
Props
interface Props {
/** 按鈕內文字 */
label?: string;
/** 是否停用 */
disabled?: boolean;
/** 同 CSS z-index */
zIndex?: number | string;
/** 最大移動距離,為按鈕尺寸倍數 */
maxDistanceMultiple?: number;
/** 同 html tabindex */
tabindex?: number | string;
}
Emits
const emit = defineEmits<{
(e: 'click'): void;
/** 開始移動時 */
(e: 'run'): void;
/** 開始返回時 */
(e: 'back'): void;
}>()
Methods
defineExpose({
/** 移動 */
run,
/** 返回原點 */
back,
/** 是否正在移動 */
isRunning,
})
Slots
defineSlots<{
/** 按鈕 */
default?: (props: { isRunning: boolean }) => unknown;
/** 拓印 */
rubbing?: (props: { isRunning: boolean }) => unknown;
}>()