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

Next.js SSR中如何实现滚动时div宽度自动展开与收缩

原有代码问题
  • 事件绑定和取值对象不匹配:监听window滚动事件,却读取目标div自身的scrollTop,如果目标div不是独立滚动容器、是跟随页面整体滚动,它的scrollTop值始终为0,拿不到有效滚动数据
  • 逻辑缺失:仅判断了是否滚动到底部,没有做滚动进度的比例映射,无法实现宽度随滚动连续变化的效果
  • 闭包与生命周期问题:useEffect依赖项设置为adjustRef.current,ref值变化不会触发组件重渲染,容易导致事件回调拿到过期的闭包值
  • SSR兼容问题:没有做客户端环境判断,服务端渲染阶段直接访问window对象会触发hydration不匹配报错
实现逻辑说明

宽度变化分两个连续阶段,和滚动位置完全绑定:

  • 初始状态:目标div顶部刚接触视口顶部时,宽度为90%
  • 展开阶段:向下滚动过程中,宽度随滚动进度从90%线性增长到100%,直到div底部距离视口顶部还有一个视口高度的位置
  • 收缩阶段:继续向下滚动到div底部离开视口的过程中,宽度从100%线性收回到90%
  • 向上滚动时逻辑完全对称,不需要额外编写反向判断,进度值会随滚动方向自动计算
可直接运行的代码实现

适配Next.js SSR模式,添加了性能优化逻辑,避免滚动卡顿:

'use client'
import { useState, useRef, useEffect, useCallback } from 'react';
import styles from './YourComponent.module.css';
import MyContent from './MyContent';

export default function AdjustContainer({ myContent }) {
  const [adjustWidth, setAdjustWidth] = useState('90%');
  const adjustRef = useRef(null);
  const isInViewRef = useRef(false);

  const handleScroll = useCallback(() => {
    if (!adjustRef.current || !isInViewRef.current) return;
    const rect = adjustRef.current.getBoundingClientRect();
    const viewportHeight = window.innerHeight;
    const elementHeight = rect.height;

    // 元素完全离开视口时终止计算
    if (rect.top > viewportHeight || rect.bottom < 0) return;

    let width;
    const expandEndPoint = -(elementHeight - viewportHeight);
    // 第一阶段:宽度从90%展开到100%
    if (rect.top >= expandEndPoint) {
      const progress = Math.abs(rect.top) / (elementHeight - viewportHeight);
      const validProgress = Math.min(Math.max(progress, 0), 1);
      width = `${90 + validProgress * 10}%`;
    } else {
      // 第二阶段:宽度从100%收缩回90%
      const shrinkRange = viewportHeight;
      const shrinkProgress = Math.abs(rect.top - expandEndPoint) / shrinkRange;
      const validProgress = Math.min(Math.max(shrinkProgress, 0), 1);
      width = `${100 - validProgress * 10}%`;
    }

    setAdjustWidth(width);
  }, []);

  useEffect(() => {
    // 跳过服务端执行阶段,避免window访问报错
    if (typeof window === 'undefined') return;

    // 用IntersectionObserver判断元素是否在视口,减少无效计算
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        isInViewRef.current = entry.isIntersecting;
        if (entry.isIntersecting) handleScroll();
      });
    }, { threshold: 0 });

    if (adjustRef.current) observer.observe(adjustRef.current);
    // 绑定滚动事件,加passive优化滚动性能
    window.addEventListener('scroll', handleScroll, { passive: true });
    // 初始化时计算一次初始宽度
    handleScroll();

    return () => {
      observer.disconnect();
      window.removeEventListener('scroll', handleScroll);
    };
  }, [handleScroll]);

  return (
    <div 
      className={styles.AdjustContainer} 
      ref={adjustRef}
      style={{ 
        width: adjustWidth, 
        transition: 'width 0.1s linear',
        margin: '0 auto'
      }}
    >
      {myContent.map((module, i) => (
        <MyContent {...module} key={i} />
      ))}
    </div>
  );
}
调优说明
  • 滚动事件添加passive: true配置,不阻塞主线程,保证滚动流畅
  • 所有进度值都做了0-1的边界截断,宽度始终控制在90%-100%区间,不会出现异常值
  • 元素不在视口范围内时终止计算,减少不必要的性能消耗
  • 宽度过渡时长可以根据实际交互手感调整,建议不超过0.2s,避免滚动和宽度变化不同步
  • 所有浏览器API访问都放在客户端useEffect生命周期内,完全兼容SSR模式,不会出现hydration报错

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 10:33:12