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

如何在NextJS 13+TailwindCSS实现Airbnb式滚动固定组件

Airbnb详情页预订框滚动问题解决方案

问题根源

你遇到的跳转问题,本质是fixed定位脱离文档流导致的:当元素设为fixed时,它不再占据原文档流位置;切回relative时,浏览器会强制让它回到文档流里的初始位置,所以出现跳变。另外硬编码scrollY阈值(210、600)也不够灵活,适配性差。

解决思路

  1. 用useRef获取元素和父容器的DOM引用,动态计算滚动触发固定/取消固定的阈值
  2. 记录元素进入固定状态时的偏移量,取消固定时通过transform属性维持当前位置,实现平滑过渡
  3. 在useEffect中管理滚动监听,组件卸载时移除监听避免内存泄漏
  4. 适配NextJS 13的客户端组件规则,添加'use client'指令

修改后的代码示例

1. 改进的StickySide组件

'use client'
import { useState, useRef, useEffect } from 'react';

function StickySide() {
  const [isFixed, setIsFixed] = useState(false);
  const [offsetTop, setOffsetTop] = useState(0);
  const stickyRef = useRef(null);
  const parentRef = useRef(null);

  useEffect(() => {
    const handleScroll = () => {
      const scrollY = window.scrollY;
      const element = stickyRef.current;
      const parent = parentRef.current;

      if (!element || !parent) return;

      // 计算元素进入视口的滚动阈值(元素顶部距离页面顶部的距离)
      const elementTop = element.getBoundingClientRect().top + scrollY;
      // 计算取消固定的阈值(父容器底部离开视口的位置)
      const parentBottom = parent.getBoundingClientRect().bottom + scrollY - window.innerHeight;

      if (scrollY >= elementTop && scrollY < parentBottom) {
        // 进入固定状态,记录此时元素的偏移量
        if (!isFixed) {
          setOffsetTop(elementTop - scrollY);
        }
        setIsFixed(true);
      } else {
        setIsFixed(false);
      }
    };

    window.addEventListener('scroll', handleScroll);
    // 初始执行一次,确保状态正确
    handleScroll();

    // 组件卸载时移除监听
    return () => window.removeEventListener('scroll', handleScroll);
  }, [isFixed]);

  return (
    <div ref={parentRef} className="relative">
      <div
        ref={stickyRef}
        className={`w-72 h-72 bg-red-300 transition-all duration-300 ${
          isFixed ? 'fixed right-20 bottom-10' : 'relative'
        }`}
        style={!isFixed ? { transform: `translateY(${offsetTop}px)` } : {}}
      >
        预订框内容
      </div>
    </div>
  );
}

export default StickySide;

2. 配合使用的DetailSection组件

'use client'
import StickySide from './StickySide';

function DetailSection({ home }) {
  return (
    <div className="pt-5 flex justify-between min-h-screen">
      <div className="drop-shadow-md w-3/6">
        <h1 className="text-lg font-light tracking-wide">{home.name}</h1>
        <h2 className="font-thin tracking-wide text-sm">
          4 bedroom <span>|</span> 2 bathroom <span>|</span> 200 sqm
        </h2>
        {/* 模拟长内容,让页面可滚动 */}
        <div className="mt-10 space-y-8">
          {Array.from({ length: 20 }).map((_, idx) => (
            <div key={idx} className="h-24 bg-gray-100 rounded-md"></div>
          ))}
        </div>
      </div>
      <StickySide />
    </div>
  );
}

export default DetailSection;

关键细节说明

  • 动态阈值计算:不再硬编码scrollY数值,而是基于元素和父容器的实际位置计算,适配不同屏幕尺寸和布局
  • 偏移量记录:offsetTop记录元素进入固定状态时相对于视口的位置,取消固定时用transform: translateY维持位置,避免跳转
  • 滚动监听清理:在useEffect的返回函数中移除监听,防止内存泄漏
  • 过渡动画:添加transition-all duration-300让状态切换更平滑,贴近Airbnb的交互体验

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 23:25:17