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

Text Characters Transition text

Give every character its own enter and leave animation. ( •̀ ω •́ )✧

Usage Examples

Basic Usage

The default is the classic fade in and fade out. ( •̀ ω •́ )✧

I am Codfish 🐟

A very long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long long piece of text

View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <base-checkbox
      v-model="visible"
      :label="t('show')"
      class="border rounded p-4"
    />

    <div class="flex flex-col gap-2">
      <text-characters-transition
        :visible="visible"
        :label="t('iAmCodfish')"
        class="text-2xl tracking-wider"
      />

      <text-characters-transition
        :visible="visible"
        :label="t('longText')"
        :enter="(i) => ({
          delay: i * 5,
        })"
        :leave="(i) => ({
          delay: i * 5,
        })"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import TextCharactersTransition from '../text-characters-transition.vue'

const { t } = useI18n()

const visible = ref(false)
</script>

Custom Splitter

You can set your own text splitting logic or provide pre-split text.

A fish that loves programming, but has no fingers to type on a keyboard, and can't even buy a computer that works underwater.

Codfishis a kind ofvery oilyvery oilyfat fish

View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
    <base-checkbox
      v-model="visible"
      :label="t('show')"
      class="border rounded p-4"
    />

    <div class="flex flex-col gap-2">
      <text-characters-transition
        :visible="visible"
        :label="t('fishStory')"
        :splitter="/(,)/"
      />

      <text-characters-transition
        :visible="visible"
        :label="codfishLabelList"
        :enter="(i) => ({
          delay: i * 200,
        })"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import TextCharactersTransition from '../text-characters-transition.vue'

const { t } = useI18n()

const visible = ref(false)

const codfishLabelList = computed(() => [
  t('codfish'),
  t('isAKindOf'),
  t('veryOily'),
  t('veryOily'),
  t('de'),
  t('fatFish'),
])
</script>

Transition Types

The component comes with some simple built-in effects. Give them a try! ◝( •ω• )◟

(Click any block below to start the transition)

A demo text for display

clip-right

A demo text for display

random-spin

A demo text for display

landing

A demo text for display

flicker

A demo text for display

converge

A demo text for display

whirling

A demo text for display

gather

A demo text for display

emerge
View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <div class="flex flex-col items-center gap-2 text-3xl font-bold tracking-wider">
      <div
        v-for="(item, i) in list"
        :key="i"
        class="clickable-box relative border px-10 py-4"
        :class="{ 'border-x-4': item.visible }"
        @click="toggleVisible(item)"
      >
        <text-characters-transition
          :label="t('demoText')"
          v-bind="item"
          class="pointer-events-none"
        />

        <div class="absolute bottom-0 left-0 p-2 px-3 text-sm font-normal tracking-normal opacity-20">
          {{ item.name }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { Writable } from 'type-fest'
import type { ExtractComponentProps } from '../../../types'
import { addProp, map } from 'remeda'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import TextCharactersTransition from '../text-characters-transition.vue'

type Param = Writable<ExtractComponentProps<typeof TextCharactersTransition>>
type Item = Pick<Param, 'name' | 'visible'>

const { t } = useI18n()

const list = ref(
  map(
    [
      { name: 'clip-right' },
      { name: 'random-spin' },
      { name: 'landing' },
      { name: 'flicker' },
      { name: 'converge' },
      { name: 'whirling' },
      { name: 'gather' },
      { name: 'emerge' },
    ] satisfies Item[],
    addProp('visible', false),
  ),
)

function toggleVisible(item: Pick<Param, 'visible'>) {
  item.visible = !item.visible
}
</script>

<style scoped lang="sass">
.clickable-box
  cursor: pointer
  transition-duration: 0.4s
  &:active
    transition-duration: 0.1s
    scale: 0.98
</style>

Custom Transitions

All parameters are customizable. See the anime.js documentation for details.

Create all kinds of unique transition effects! (≖‿ゝ≖)✧

(Click any block below to start the transition)

Code written like poetry, so beautiful no one understands

People and programs, as long as they can run it's fine

They say codfish weighsnotover 100

Codfish: "That 'no' appeared way too late! Σ(ˊДˋ;)"

View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4">
    <div class="flex flex-col flex-1 items-center justify-around gap-6 text-xl">
      <div
        v-for="(item, i) in list"
        :key="i"
        class="clickable-box border px-9 py-6"
        :class="{ 'border-x-4': item.visible }"
        @click="toggleVisible(item)"
      >
        <text-characters-transition v-bind="item" />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import type { Writable } from 'type-fest'
import type { ExtractComponentProps } from '../../../types'
import anime from 'animejs'
import { sample } from 'remeda'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import TextCharactersTransition from '../text-characters-transition.vue'

type Param = Writable<ExtractComponentProps<typeof TextCharactersTransition>>
type Item = Pick<Param, 'label' | 'enter' | 'leave' | 'visible'> & {
  class?: string;
}

const { t } = useI18n()

const negativeList = [1, -1] as const
const randomNegative = () => sample(negativeList, 1)[0]

const list = ref<Item[]>([
  {
    visible: false,
    label: t('poeticCode'),
    class: 'font-wenkai tracking-[0.2rem]',
    enter: (i) => ({
      opacity: [0, 1],
      filter: ['blur(20px)', 'blur(0px)'],
      translateX: () => [
        anime.random(40, 60) * randomNegative(),
        0,
      ],
      translateY: () => [
        anime.random(50, 60) * randomNegative(),
        0,
      ],
      delay: i * 100,
      duration: 1600,
      easing: 'easeOutCirc',
    }),
    leave: (i) => ({
      opacity: 0,
      filter: 'blur(20px)',
      delay: i * 50,
      duration: 900,
      easing: 'easeInCirc',
    }),
  },
  {
    visible: false,
    class: 'tracking-[0.2rem] perspective',
    label: [t('people'), t('and'), t('programs'), t('comma'), t('asLongAs'), t('canRun'), t('itsFine')],
    enter: (i) => ({
      opacity: [0, 1],
      rotateX: [anime.random(180, 90), 0],
      rotateY: [270, 0],
      rotateZ: [anime.random(-90, 90), 0],
      scaleX: [0.5, 1],
      easing: 'easeOutCirc',
      duration: 1000,
      delay: i * 300,
    }),
    leave: (i) => ({
      opacity: 0,
      rotateX: anime.random(-180, -90),
      rotateY: anime.random(-90, 90),
      rotateZ: anime.random(-90, 90),
      scaleX: 0.5,
      easing: 'easeInExpo',
      duration: 1400,
      delay: i * 100,
    }),
  },
  {
    visible: false,
    class: 'flex flex-nowrap justify-center items-center font-bold tracking-wider w-[18rem] h-[2.8rem]',
    label: [
      {
        value: t('codfishWeight'),
        enter: () => ({
          opacity: [0, 1],
        }),
        leave: () => ({
          opacity: 0,
          delay: 500,
        }),
      },
      {
        value: t('didNot'),
        enter: () => ({
          fontSize: [
            '0rem',
            '1.25rem',
          ],
          delay: 2000,
        }),
        leave: () => ({
          opacity: 0,
          delay: 500,
        }),
      },
      {
        value: t('exceed100'),
        enter: () => ({
          opacity: [0, 1],
          fontSize: [
            { value: '3rem' },
            { value: '1.25rem' },
          ],
          color: [
            { value: '#f00' },
            { value: '#000' },
          ],
          rotate: [
            { value: '10deg' },
            { value: '0deg' },
          ],
          delay: 500,
        }),
        leave: () => ({
          opacity: [
            { value: 1 },
            { value: 0 },
          ],
          fontSize: [
            { value: '3rem' },
            { value: '1.25rem' },
          ],
          color: [
            { value: '#f0f' },
            { value: '#000' },
          ],
          rotate: [
            { value: '-10deg' },
            { value: '0deg' },
          ],
        }),
      },
    ],
  },
])

function toggleVisible(item: Pick<Param, 'visible'>) {
  item.visible = !item.visible
}
</script>

<style scoped lang="sass">
.font-wenkai
  text-shadow: 0 0 10px rgba(#111, 0.1)

.perspective
  perspective: 100px
  transform-style: preserve-3d

.clickable-box
  cursor: pointer
  transition-duration: 0.4s
  &:active
    transition-duration: 0.1s
    scale: 0.98
</style>

How It Works

After splitting the text, each segment is assigned a unique ID, and anime.js is used to implement the animation effects.

For detailed explanation, see this article.

Source Code

API

Props

interface Props {
  visible?: boolean;
  label: string | string[] | Array<{
    value: string;
    enter: AnimeFuncParam;
    leave: AnimeFuncParam;
  }>;

  /** html tag
   *
   * @default 'p'
   */
  tag?: string;

  /** 如何切割文字
   *
   * 只有在 label 為 string 時有效
   *
   * @default /.*?/u
   */
  splitter?: RegExp | ((label: string) => string[]);

  /** 過場名稱。使用預設內容 */
  name?: `${TransitionName}`;
  /** 進入動畫設定 */
  enter?: AnimeFuncParam;
  /** 離開動畫設定 */
  leave?: AnimeFuncParam;
}

Emits

const emit = defineEmits<{
  beforeEnter: [];
  afterEnter: [];
  beforeLeave: [];
  afterLeave: [];
}>()

v0.60.0