You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在@react-spring useTransition中实现height从0到'auto'的过渡?

解决useTransition动态高度过渡动画问题

当使用useTransition时直接设置height: 'auto'会丢失动画效果,因为CSS过渡不支持从具体数值到auto的平滑过渡。以下是几种可行的解决方案:

方法一:通过ref获取实际高度过渡(推荐)

利用ref获取元素的真实内容高度,先过渡到该具体数值,动画结束后再设置为auto,既保留动画又支持动态高度自适应。

import { useTransition, animated } from 'react-spring';
import { useRef } from 'react';

function AnimatedDynamicList({ items }) {
  const transitions = useTransition(items, {
    keys: item => item.id,
    from: { height: 0, opacity: 0 },
    enter: async (_, state) => {
      // 先让元素可见,确保能获取内容高度
      await state.set({ opacity: 1 });
      const realHeight = state.ref.current.scrollHeight;
      // 过渡到真实高度
      await state.set({ height: realHeight });
      // 动画结束后恢复auto,支持后续内容动态变化
      state.set({ height: 'auto' });
    },
    leave: { height: 0, opacity: 0 },
    config: { duration: 300 },
    // 绑定ref到每个过渡元素
    ref: ref => ref ? (state.ref = ref) : null,
  });

  return (
    <div className="list-container">
      {transitions((style, item, state) => (
        <animated.div
          ref={state.ref}
          style={{
            ...style,
            overflow: 'hidden',
            margin: '8px 0',
            padding: '12px',
            border: '1px solid #eee',
          }}
        >
          {/* 动态高度的子内容 */}
          <h3>{item.title}</h3>
          <p>{item.description}</p>
        </animated.div>
      ))}
    </div>
  );
}

原理说明

  1. 先通过opacity: 1让元素渲染内容,此时能通过scrollHeight获取到内容的真实高度;
  2. 过渡到该真实高度数值,实现平滑动画;
  3. 动画完成后设置height: auto,确保后续内容变化时容器能自动适应高度。

方法二:使用max-height替代height

如果场景中内容高度变化范围不大,可以用max-height代替height,设置一个足够大的目标值(只要超过内容可能的最大高度即可)。

import { useTransition, animated } from 'react-spring';

function SimpleAnimatedList({ items }) {
  const transitions = useTransition(items, {
    keys: item => item.id,
    from: { maxHeight: 0, opacity: 0 },
    enter: { maxHeight: '2000px', opacity: 1 },
    leave: { maxHeight: 0, opacity: 0 },
    config: { duration: 300 },
  });

  return (
    <div className="list-container">
      {transitions((style, item) => (
        <animated.div
          style={{
            ...style,
            overflow: 'hidden',
            margin: '8px 0',
            padding: '12px',
            border: '1px solid #eee',
          }}
        >
          <h3>{item.title}</h3>
          <p>{item.description}</p>
        </animated.div>
      ))}
    </div>
  );
}

优缺点

  • 优点:实现简单,无需ref;
  • 缺点:若内容高度差异过大,动画后期会出现速度变快的不自然效果(因为max-height从实际高度到设定的最大值这段是无效过渡)。

方法三:监听内容变化重新计算高度

当子元素内容动态更新时,通过useLayoutEffect重新获取高度并触发过渡,确保内容变化时也能有平滑动画。

import { useTransition, animated } from 'react-spring';
import { useRef, useState, useLayoutEffect } from 'react';

function AnimatedUpdatableItem({ children }) {
  const ref = useRef(null);
  const [targetHeight, setTargetHeight] = useState(0);
  const [isVisible, setIsVisible] = useState(true);

  useLayoutEffect(() => {
    if (ref.current && isVisible) {
      const height = ref.current.scrollHeight;
      setTargetHeight(height);
      // 动画结束后恢复auto
      const timer = setTimeout(() => setTargetHeight('auto'), 300);
      return () => clearTimeout(timer);
    }
  }, [children, isVisible]);

  const transitions = useTransition(isVisible, {
    from: { height: 0, opacity: 0 },
    enter: { height: targetHeight, opacity: 1 },
    leave: { height: 0, opacity: 0 },
    config: { duration: 300 },
  });

  return (
    <div>
      <button onClick={() => setIsVisible(!isVisible)}>
        {isVisible ? '收起' : '展开'}
      </button>
      {transitions((style) => (
        <animated.div
          ref={ref}
          style={{
            ...style,
            overflow: 'hidden',
            margin: '8px 0',
            padding: '12px',
            border: '1px solid #eee',
          }}
        >
          {children}
        </animated.div>
      ))}
    </div>
  );
}

内容的提问来源于stack exchange,提问作者alex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 05:25:36