Transition

Transition 组件为元素或组件提供进入/离开的 CSS 过渡动画。它可以配合条件渲染(v-if)、显示隐藏(v-show)、DynamicFreeze 等使用。

基本用法

tsx
import { Transition, ref } from 'vitarx'

function App() {
  const show = ref(true)

  return (
    <div>
      <button
        onClick={() => {
          show.value = !show.value
        }}
      >
        {show.value ? '隐藏' : '显示'}
      </button>
      <Transition>{show.value && <div class="box">我会淡入淡出</div>}</Transition>
    </div>
  )
}

对应的 CSS:

css
/* 进入动画 */
.v-enter-from {
  opacity: 0;
  transform: translateY(-10px);
}
.v-enter-active {
  transition: all 0.3s ease;
}
.v-enter-to {
  opacity: 1;
  transform: translateY(0);
}

/* 离开动画 */
.v-leave-from {
  opacity: 1;
  transform: translateY(0);
}
.v-leave-active {
  transition: all 0.3s ease;
}
.v-leave-to {
  opacity: 0;
  transform: translateY(-10px);
}

过渡类名

Transition 使用 6 个 CSS 类名来控制过渡动画,默认前缀为 v

类名阶段说明
v-enter-from进入开始元素插入时的初始状态,只持续一帧
v-enter-active进入过程整个进入动画期间生效,用于定义 transition
v-enter-to进入结束进入动画的最终状态
v-leave-from离开开始离开动画的初始状态
v-leave-active离开过程整个离开动画期间生效,用于定义 transition
v-leave-to离开结束离开动画的最终状态

动画流程如下:

text
进入:v-enter-from → v-enter-active → v-enter-to
离开:v-leave-from → v-leave-active → v-leave-to

自定义过渡类名

name 属性

通过 name 属性可以修改类名前缀,避免样式冲突:

tsx
<Transition name="fade">{show.value && <div>淡入淡出</div>}</Transition>

对应的 CSS 类名变为 fade-enter-fromfade-enter-active 等:

css
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

自定义类名属性

你也可以单独指定每个阶段的类名,这在结合第三方动画库(如 Animate.css)时很有用:

tsx
<Transition
  enterFromClass="animate__animated animate__fadeIn"
  enterActiveClass="animate__animated animate__fadeIn"
  enterToClass="animate__animated animate__fadeIn"
  leaveFromClass="animate__animated animate__fadeOut"
  leaveActiveClass="animate__animated animate__fadeOut"
  leaveToClass="animate__animated animate__fadeOut"
>
  {show.value && <div>动画效果</div>}
</Transition>

可用的自定义类名属性:

属性说明
enterFromClass进入开始状态类名
enterActiveClass进入过程类名
enterToClass进入结束状态类名
leaveFromClass离开开始状态类名
leaveActiveClass离开过程类名
leaveToClass离开结束状态类名
appearFromClass首次出现开始状态类名
appearActiveClass首次出现过程类名
appearToClass首次出现结束状态类名

过渡模式

当两个元素切换时(如 show ? <A /> : <B />),可以通过 mode 属性控制进入和离开动画的顺序:

模式行为
默认(无 mode)进入和离开动画同时进行
out-in当前元素先离开,新元素后进入
in-out新元素先进入,当前元素后离开
tsx
{
  /* out-in:先离开再进入,更常用 */
}
;<Transition name="fade" mode="out-in">
  {show.value ? <div key="a">内容 A</div> : <div key="b">内容 B</div>}
</Transition>

appear — 首次渲染动画

默认情况下,Transition 不会在组件首次渲染时触发动画。设置 appear={true} 可以让首次渲染也触发进入动画:

tsx
<Transition name="fade" appear>
  <div>首次渲染也会有淡入效果</div>
</Transition>

JavaScript 钩子

除了 CSS 过渡,你还可以通过 JavaScript 钩子来控制动画。这在需要更复杂的动画效果(如使用 GSAP 等动画库)时很有用:

tsx
<Transition
  css={false}
  onBeforeEnter={(el) => {
    // 进入动画开始前
    el.style.opacity = '0'
  }}
  onEnter={(el, done) => {
    // 进入动画开始,需手动调用 done() 表示完成
    el.style.transition = 'opacity 0.3s'
    el.style.opacity = '1'
    setTimeout(done, 300)
  }}
  onAfterEnter={(el) => {
    // 进入动画完成后
    console.log('进入动画完成')
  }}
  onBeforeLeave={(el) => {
    // 离开动画开始前
  }}
  onLeave={(el, done) => {
    // 离开动画开始,需手动调用 done() 表示完成
    el.style.transition = 'opacity 0.3s'
    el.style.opacity = '0'
    setTimeout(done, 300)
  }}
  onAfterLeave={(el) => {
    // 离开动画完成后
    console.log('离开动画完成')
  }}
>
  {show.value && <div>JS 动画</div>}
</Transition>

设置 css={false} 可以跳过 CSS 类名的检测,只使用 JavaScript 钩子。

完整的钩子列表:

钩子参数说明
onBeforeEnter(el)进入动画开始前
onEnter(el, done)进入动画开始,需调用 done()
onAfterEnter(el)进入动画完成后
onEnterCancelled(el)进入动画被取消时
onBeforeLeave(el)离开动画开始前
onLeave(el, done)离开动画开始,需调用 done()
onAfterLeave(el)离开动画完成后
onLeaveCancelled(el)离开动画被取消时
onBeforeAppear(el)首次出现动画开始前
onAppear(el, done)首次出现动画开始,需调用 done()
onAfterAppear(el)首次出现动画完成后
onAppearCancelled(el)首次出现动画被取消时

TransitionGroup — 列表过渡

TransitionGroup 用于为列表项添加进入、离开和移动的过渡动画。它基于 For 组件,支持 For 的所有属性,并额外提供了过渡动画能力。

tsx
import { TransitionGroup, reactive } from 'vitarx'

function App() {
  const items = reactive([
    { id: 1, text: '项目 1' },
    { id: 2, text: '项目 2' },
    { id: 3, text: '项目 3' }
  ])

  let nextId = 4

  const addItem = () => {
    items.push({ id: nextId++, text: `项目 #123;nextId - 1}` })
  }

  const removeItem = (id: number) => {
    const index = items.findIndex((item) => item.id === id)
    if (index !== -1) items.splice(index, 1)
  }

  return (
    <div>
      <button onClick={addItem}>添加</button>
      <TransitionGroup each={items} key="id" name="list" tag="ul">
        {(item) => <li onClick={() => removeItem(item.id)}>{item.text}(点击删除)</li>}
      </TransitionGroup>
    </div>
  )
}

对应的 CSS:

css
/* 进入和离开动画 */
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}

/* 移动动画 — 关键!让列表项移动时有平滑过渡 */
.list-move {
  transition: transform 0.5s ease;
}

TransitionGroup 的额外属性:

属性类型说明
tagstring包裹子节点的标签名,不传则不创建包裹元素
moveClassstring元素移动时的 CSS 类名,默认为 ${name}-move
propsobject传递给包裹元素的属性

重要TransitionGroup 的移动动画需要在 CSS 中定义 ${name}-move 类,否则列表项位置变化时不会有动画效果。

完整示例

下面是一个包含条件渲染过渡和列表过渡的完整示例:

tsx
import { Transition, TransitionGroup, ref, reactive } from 'vitarx'

/** 条件渲染过渡示例 */
function ToggleDemo() {
  const show = ref(true)

  return (
    <div style="margin-bottom: 32px;">
      <h2>条件渲染过渡</h2>
      <button
        onClick={() => {
          show.value = !show.value
        }}
      >
        {show.value ? '隐藏' : '显示'}
      </button>
      <Transition name="slide-fade" mode="out-in">
        {show.value ? (
          <div key="visible" class="box">
            我可见
          </div>
        ) : (
          <div key="hidden" class="box hidden-state">
            我已隐藏
          </div>
        )}
      </Transition>
    </div>
  )
}

/** 列表过渡示例 */
function ListDemo() {
  const items = reactive([
    { id: 1, text: '学习 Vitarx' },
    { id: 2, text: '编写组件' },
    { id: 3, text: '部署项目' }
  ])
  let nextId = 4

  const addItem = () => {
    items.push({ id: nextId++, text: `新任务 #123;nextId - 1}` })
  }

  const removeItem = (id: number) => {
    const index = items.findIndex((item) => item.id === id)
    if (index !== -1) items.splice(index, 1)
  }

  return (
    <div>
      <h2>列表过渡</h2>
      <button onClick={addItem}>添加任务</button>
      <TransitionGroup each={items} key="id" name="list" tag="ul">
        {(item) => (
          <li onClick={() => removeItem(item.id)} style="cursor: pointer; padding: 4px 0;">
            {item.text}
          </li>
        )}
      </TransitionGroup>
    </div>
  )
}

function App() {
  return (
    <div style="padding: 20px;">
      <ToggleDemo />
      <ListDemo />
    </div>
  )
}

对应的 CSS:

css
/* slide-fade 过渡 */
.slide-fade-enter-from {
  opacity: 0;
  transform: translateX(-20px);
}
.slide-fade-enter-active {
  transition: all 0.3s ease;
}
.slide-fade-leave-from {
  opacity: 1;
}
.slide-fade-leave-active {
  transition: all 0.3s ease;
}
.slide-fade-leave-to {
  opacity: 0;
  transform: translateX(20px);
}

/* 列表过渡 */
.list-enter-from,
.list-leave-to {
  opacity: 0;
  transform: translateX(30px);
}
.list-enter-active,
.list-leave-active {
  transition: all 0.5s ease;
}
.list-move {
  transition: transform 0.5s ease;
}

下一步