Skip to content

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
vue
<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
vue
<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
vue
<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.

Custom Button
View Example Source Code
vue
<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.

It ran away
Custom Button
View Example Source Code
vue
<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.

😗
Click here to win
View Example Source Code
vue
<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! (╯•̀ὤ•́)╯

Unfinished
View Example Source Code
vue
<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;
}>()

v0.38.7