Skip to content
Welcome to vote for your favorite component! You can also tell me anything you want to say! (*´∀`)~♥

Cat Ear Wrapper wrapper

Wrap any element and it'll sprout cat ears—let everything be cute! (^・ω・^ )✧

Passerby: "What on earth is this? (っ´Ι`)っ"

Codfish: "I heard you get likes just by having a cat? ( •̀ ω •́ )✧"

Passerby: "Just ears doesn't count as a cat. (˘・_・˘)"

Usage Examples

Basic Usage

After wrapping with the component, everything will sprout cat ears and become more adorable (?

The ears even auto-resize! ˋ( ° ▽、° )

View example source code
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-16 border border-gray-200 rounded-xl py-16">
    <wrapper-cat-ear main-color="#AAA">
      <base-btn
        class=""
        :label="t('button')"
      />
    </wrapper-cat-ear>

    <wrapper-cat-ear main-color="#AAA">
      <div class="">
        <img
          class="w-80 rounded"
          src="/codfish.webp"
        >
      </div>
    </wrapper-cat-ear>

    <wrapper-cat-ear main-color="#AAA">
      <div class="border border-[#AAA] rounded">
        <select class="px-10 py-2">
          <option
            disabled
            selected
            :label="t('selectCat')"
          />
          <option label="(>'-'<)ノ" />
          <option label="~(=^‥^)" />
          <option label="/ᐠ。ꞈ。ᐟ\" />
          <option label="(^._.^)ノ" />
          <option label="(^・ω・^ )" />
        </select>
      </div>
    </wrapper-cat-ear>
  </div>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import BaseBtn from '../../base-btn.vue'
import WrapperCatEar from '../wrapper-cat-ear.vue'

const { t } = useI18n()
</script>

Switch Action

You can switch between various different actions.

View example source code
vue
<template>
  <div class="h-[50vh] w-full flex flex-col items-center justify-center gap-16 border border-gray-200 rounded-xl">
    <wrapper-cat-ear
      :action="action"
      main-color="#999"
    >
      <div class="border-2 border-[#999] rounded">
        <select
          v-model="action"
          class="p-2"
        >
          <option
            v-for="option in options"
            :key="option"
            :value="option"
          >
            {{ option }}
          </option>
        </select>
      </div>
    </wrapper-cat-ear>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { ActionName } from '..'
import WrapperCatEar from '../wrapper-cat-ear.vue'

const action = ref<`${ActionName}`>('relaxed')
const options = Object.values(ActionName)
</script>

Mix Colors

Adjust the fur color to personalize your cat.

◕ ω ◕
◕ ω ◕
◕ ω ◕
> ◕ ω ◕ <

9 out of 10 orange cats are chonky, and the 10th is mega chonky. (o゚v゚)ノ

View example source code
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-10 border border-gray-200 rounded-xl py-10">
    <div class="flex items-center border border-gray-200 rounded-xl rounded p-10">
      <input
        v-model="mainColor"
        type="color"
      >

      <input
        v-model="innerColor"
        type="color"
      >

      <wrapper-cat-ear
        :main-color="mainColor"
        :inner-color="innerColor"
        class="ml-10"
      >
        <div
          class="rounded p-2 px-3 text-white"
          :style="{ backgroundColor: mainColor }"
          v-text="`◕ ω ◕`"
        />
      </wrapper-cat-ear>
    </div>

    <wrapper-cat-ear
      main-color="#3b3b3b"
      inner-color="#ffc2b8"
    >
      <div
        class="rounded bg-[#3b3b3b] p-2 px-3 text-white"
        v-text="`◕ ω ◕`"
      />
    </wrapper-cat-ear>

    <wrapper-cat-ear
      main-color="#03a1fc"
      inner-color="#8f003e"
    >
      <div
        class="rounded bg-[#03a1fc] p-2 px-3 text-white"
        v-text="`◕ ω ◕`"
      />
    </wrapper-cat-ear>

    <wrapper-cat-ear
      main-color="#ff852e"
      inner-color="#ffc2b8"
      class="mt-10"
    >
      <div
        class="rounded bg-[#ff852e] px-20 py-12 text-xl text-white"
        v-text="`> ◕ ω ◕ <`"
      />
    </wrapper-cat-ear>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import WrapperCatEar from '../wrapper-cat-ear.vue'

const mainColor = ref('#b38546')
const innerColor = ref('#ffc2b8')
</script>

Interactive Effect

Design various logic based on ear actions to make interactions more fun.

Try slowly moving the mouse toward the button from afar. (. ❛ ᴗ ❛.)

◕ ω ◕
View example source code
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-16 border border-gray-200 rounded-xl py-20">
    <wrapper-cat-ear
      :action="action"
      main-color="#666"
      class="cat border-2 border-[#666] rounded"
    >
      <div
        ref="catRef"
        class="dra flex items-center justify-center bg-white px-16 py-8"
      >
        <transition
          name="cat"
          mode="out-in"
        >
          <div
            :key="face"
            class="absolute select-none text-nowrap text-2xl text-gray-800 font-bold"
            v-text="face"
          />
        </transition>
      </div>
    </wrapper-cat-ear>
  </div>
</template>

<script setup lang="ts">
import type { ActionName } from '..'
import { throttleFilter, useMouseInElement, useMousePressed } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { getVectorLength } from '../../../common/utils'
import WrapperCatEar from '../wrapper-cat-ear.vue'

const { t } = useI18n()

const faceMap = computed<Record<ActionName, string>>(() => ({
  peekaboo: t('submit'),
  relaxed: '◕ ω ◕',
  fear: '´•̥̥̥ ω •̥̥̥`',
  displeased: '˘・ ω ・˘',
  shake: '≧ X ≦',
}))

const catRef = ref()
const {
  elementX,
  elementY,
  elementWidth,
  elementHeight,
  isOutside,
} = useMouseInElement(catRef, {
  eventFilter: throttleFilter(35),
})
const { pressed } = useMousePressed()

/** 滑鼠與貓的距離 */
const distance = computed(() => getVectorLength({
  x: elementX.value - elementWidth.value / 2,
  y: elementY.value - elementHeight.value / 2,
}))
/** 煩躁的距離 */
const displeasedDistance = computed(
  () => Math.max(elementWidth.value, elementHeight.value),
)
/** 變成貓的距離 */
const catDistance = computed(
  () => displeasedDistance.value * 2,
)

const action = computed<`${ActionName}`>(() => {
  if (!isOutside.value) {
    return pressed.value ? 'shake' : 'fear'
  }

  if (distance.value > catDistance.value) {
    return 'peekaboo'
  }

  if (distance.value < displeasedDistance.value) {
    return 'displeased'
  }

  return 'relaxed'
})
const face = computed(() => faceMap.value[action.value])
</script>

<style lang="sass">
.cat
  transition-duration: 0.4s
  box-shadow: 3px 3px 0px 0px rgba(#AAA, 0.8)
  &:hover
    transition-duration: 0.3s
    scale: 1.02
    box-shadow: 5px 5px 0px 0px rgba(#AAA, 0.5)
  &:active
    transition-duration: 0.1s
    scale: 0.95
    animation: shake 0.3s infinite ease-in-out
    box-shadow: 0px 0px 0px 0px rgba(#AAA, 1)

.cat-enter-active, .cat-leave-active
  transition-duration: 0.2s
.cat-enter-from, .cat-leave-to
  transform: translateY(10px)
  opacity: 0 !important
.cat-leave-to
  transform: translateY(-10px)

@keyframes shake
  35%
    rotate: -3deg
  70%
    rotate: 3deg
</style>

Naughty Cat

Paired with a naughty button, the cat factor goes even higher! ᕕ( ゚ ∀。)ᕗ

Save
View example source code
vue
<template>
  <div class="w-full flex flex-col items-center justify-center gap-16 border border-gray-200 rounded-xl py-10">
    <div class="flex flex-col gap-4 border rounded p-4">
      <base-checkbox
        v-model="disabled"
        :label="t('disable')"
      />
    </div>

    <btn-naughty
      ref="btnRef"
      :disabled="disabled"
      class="cat-btn select-none"
    >
      <wrapper-cat-ear
        :action="currentAction"
        main-color="#777"
      >
        <div
          class="h-[3rem] w-[6rem] flex-center border border-[#777] rounded bg-white text-xl text-gray-800 font-bold"
          v-text="face"
        />
      </wrapper-cat-ear>
    </btn-naughty>
  </div>
</template>

<script setup lang="ts">
import type { ActionName } from '..'
import { refAutoReset, whenever } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import BtnNaughty from '../../btn-naughty/btn-naughty.vue'
import WrapperCatEar from '../wrapper-cat-ear.vue'

const { t } = useI18n()

const btnRef = ref<InstanceType<typeof BtnNaughty>>()

const disabled = ref(false)
const action = refAutoReset<`${ActionName}`>('relaxed', 700)
const face = refAutoReset(t('save'), 700)

const currentAction = computed(() => {
  if (!disabled.value) {
    return 'peekaboo'
  }

  return action.value
})

whenever(() => btnRef.value?.isRunning, () => {
  face.value = '˘・ ω ・˘'
  action.value = 'displeased'
})
</script>

<style lang="sass">
.cat-btn
  transition-duration: 0.4s
  &:active
    transition-duration: 0.01s
    scale: 0.95

.flex-center
  display: flex
  justify-content: center
  align-items: center
</style>

How It Works

Uses SVG to draw the ears, controlled by anime.js for ear animations.

📚 anime.js

Why anime.js?

Because GSAP, the most mainstream option, charges for SVG animations.

Motion One, the coolest option, had some minor issues and very little documentation.

In the end, anime.js was chosen for its decent documentation and good SVG support.

If anyone has better library suggestions, please recommend them to me. (´▽`ʃ♡ƪ)

Source Code

API

ActionName

Props

interface Props {
  /** 目前動作 */
  action?: `${ActionName}`;
  /** 主要毛色 */
  mainColor?: string;
  /** 耳朵內部的顏色 */
  innerColor?: string;
}

v0.60.0