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
<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
<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
<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
<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! ᕕ( ゚ ∀。)ᕗ
View example source code
<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;
}