Scroll State Playground

交互式体验 CSS @container scroll-state() 查询,学习如何使用纯 CSS 实现基于滚动状态的动态样式切换。

浏览器支持
  • Chrome 133+ · 完全支持
  • Edge 133+ · 完全支持
  • Safari · 暂不支持
  • Firefox · 暂不支持

此组件使用 CSS @container scroll-state() 容器查询。如使用不支持此功能的浏览器,部分效果可能无法正常显示。

什么是 @container scroll-state()?

@container scroll-state() 是 CSS 容器查询的一种,可以根据滚动状态来应用不同的样式。它可以检测三种滚动状态:

  • stuck: 使用 position: sticky 的元素被固定时
  • scrollable: 元素可以向某个方向滚动时
  • snapped: 使用 scroll-snap-type 的元素停靠完成时

通过这些查询,你可以实现很多以前需要 JavaScript 才能完成的滚动交互效果。

示例演示

1. 智能导航栏 (Stuck Navigation)

导航栏未吸顶时仅显示"联系我们"按钮,吸顶后所有菜单项平滑展开。

CSS Container Queries

scroll-state()

当导航栏吸附到视口顶部时,使用容器查询自动调整布局状态

01

公司简介

我们是一家专注于创新的科技公司,致力于为客户提供最优质的数字化解决方案。 自成立以来,我们始终秉承"创新、品质、服务"的理念,帮助众多企业实现数字化转型。

02

制作事例

我们拥有丰富的项目经验,从企业官网到复杂的Web应用, 每一个项目都倾注了我们的专业与热情。 探索我们的作品集,了解我们如何帮助客户实现目标。

03

人才招聘

我们正在寻找富有激情和创造力的伙伴加入我们的团队。 提供具有竞争力的薪酬、完善的福利体系和良好的职业发展机会。 让我们一起创造更多可能。

04

联系我们

无论您有任何问题或需求,欢迎随时与我们联系。 我们的专业团队将为您提供及时的回复和最合适的解决方案。

展开查看源码
import { ArrowRight, Briefcase, Building2, Users } from 'lucide-react';

const StuckNavigationDemo = () => {
  return (
    <div className="rainbow-border max-h-150 overflow-y-auto">
      {/* Hero Section */}
      <section className="flex items-center justify-center bg-[linear-gradient(135deg,#6366f1_0%,#4f46e5_100%)] py-20">
        <div className="space-y-6 pb-20 text-center">
          <span className="rounded-full bg-gray-3 px-5 py-2 font-500 text-white">CSS Container Queries</span>
          <h1 className="text-3xl text-white">scroll-state()</h1>
          <p className="text-base-m text-white-6">当导航栏吸附到视口顶部时,使用容器查询自动调整布局状态</p>
        </div>
      </section>

      {/* Navigation Container with scroll-state container-type */}
      <nav className="nav-container sticky top-0 z-100 -mt-40 py-6">
        <ul className="nav-list mx-auto grid w-140 grid-cols-4 rounded-full bg-white shadow-xs transition-all duration-400 ease-[cubic-bezier(0.32,1.3,0.54,1)]">
          <li className="flex items-center justify-center overflow-hidden">
            <a href="#" className="flex items-center gap-2 text-nowrap px-4 py-2 font-500 text-gray-8 hover:bg-#fafafa">
              <Building2 className="flex-shrink-0" size={18} />
              <span>公司简介</span>
            </a>
          </li>
          <li className="flex items-center justify-center overflow-hidden">
            <a href="#" className="flex items-center gap-2 text-nowrap px-4 py-2 font-500 text-gray-8 hover:bg-#fafafa">
              <Briefcase className="flex-shrink-0" size={18} />
              <span>制作事例</span>
            </a>
          </li>
          <li className="flex items-center justify-center overflow-hidden">
            <a href="#" className="flex items-center gap-2 text-nowrap px-4 py-2 font-500 text-gray-8 hover:bg-#fafafa">
              <Users className="flex-shrink-0" size={18} />
              <span>人才招聘</span>
            </a>
          </li>
          <li className="flex items-center justify-center overflow-hidden">
            <a href="#" className="flex items-center gap-2 text-nowrap px-4 py-2 font-500 text-gray-8 hover:bg-#fafafa">
              <span>联系我们</span>
              <ArrowRight className="flex-shrink-0" size={18} />
            </a>
          </li>
        </ul>
      </nav>

      {/* Content Sections */}
      <section className="flex items-center justify-center bg-[linear-gradient(180deg,#249d7d_0%,#179d77_100%)] py-10">
        <div className="max-w-150 text-center">
          <span className="flex-inline rounded bg-gray-3 px-3 py-1 font-bold text-gray-6">01</span>
          <h2 className="mb-4 text-4xl text-white">公司简介</h2>
          <p className="text-base text-white-6">
            我们是一家专注于创新的科技公司,致力于为客户提供最优质的数字化解决方案。
            自成立以来,我们始终秉承"创新、品质、服务"的理念,帮助众多企业实现数字化转型。
          </p>
        </div>
      </section>

      <section className="flex items-center justify-center bg-[linear-gradient(180deg,#2d86e1_0%,#3a5bc8_100%)] py-10">
        <div className="max-w-150 text-center">
          <span className="flex-inline rounded bg-gray-3 px-3 py-1 font-bold text-gray-6">02</span>
          <h2 className="mb-4 text-4xl text-white">制作事例</h2>
          <p className="text-base text-white-6">
            我们拥有丰富的项目经验,从企业官网到复杂的Web应用, 每一个项目都倾注了我们的专业与热情。
            探索我们的作品集,了解我们如何帮助客户实现目标。
          </p>
        </div>
      </section>

      <section className="flex items-center justify-center bg-[linear-gradient(180deg,#e7a569_0%,#bc6a0e_100%)] py-10">
        <div className="max-w-150 text-center">
          <span className="flex-inline rounded bg-gray-3 px-3 py-1 font-bold text-gray-6">03</span>
          <h2 className="mb-4 text-4xl text-white">人才招聘</h2>
          <p className="text-base text-white-6">
            我们正在寻找富有激情和创造力的伙伴加入我们的团队。 提供具有竞争力的薪酬、完善的福利体系和良好的职业发展机会。
            让我们一起创造更多可能。
          </p>
        </div>
      </section>

      <section className="flex items-center justify-center bg-[linear-gradient(180deg,#b175df_0%,#8337c2_100%)] py-10">
        <div className="max-w-150 text-center">
          <span className="flex-inline rounded bg-gray-3 px-3 py-1 font-bold text-gray-6">04</span>
          <h2 className="mb-4 text-4xl text-white">联系我们</h2>
          <p className="text-base text-white-6">
            无论您有任何问题或需求,欢迎随时与我们联系。 我们的专业团队将为您提供及时的回复和最合适的解决方案。
          </p>
        </div>
      </section>

      <style>{`
        /* Navigation */
        .nav-container {
          container-type: scroll-state;
        }

        /* Scroll-state Container Queries - 关键效果 */
        @container not scroll-state(stuck: top) {
          .nav-list {
            grid-template-columns: 0 0 0 1fr;
            width: 160px;
            padding: 0 8px;
          }
        }

        @container scroll-state(not stuck: top) {
          .nav-link {
            opacity: 1;
            transition: opacity 0.2s;
          }
        }
      `}</style>
    </div>
  );
};

export default StuckNavigationDemo;
💡 原理说明

使用 container-type: scroll-stateposition: sticky 配合。未吸顶时,通过 grid-template-columns: 0 0 0 1fr 隐藏前三个导航项,仅显示"联系我们"。吸顶后,Grid 列恢复为 1fr 1fr 1fr 1fr,所有菜单项配合 transition-delay 依次展开,创造优雅的交互体验。这种模式特别适合需要精简导航的长页面。


横向滚动图片轮播,滚动中的卡片缩小,停靠后恢复正常大小。

展开查看源码
const SnappedCarouselDemo = () => {
  const images = [
    { id: 1, title: '山川湖海', desc: '壮丽的自然风光', gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
    { id: 2, title: '城市夜景', desc: '繁华都市之美', gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' },
    { id: 3, title: '森林秘境', desc: '神秘的绿色世界', gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' },
    { id: 4, title: '沙漠奇观', desc: '金色的沙海', gradient: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)' },
    { id: 5, title: '极光之夜', desc: '梦幻的光影', gradient: 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' },
  ];

  return (
    <div className="snapped-carousel-demo rainbow-border">
      <ul className="carousel">
        {images.map((image) => (
          <li key={image.id} className="carousel-item">
            <span className="image-wrapper">
              <span className="image-placeholder" style={{ background: image.gradient }}>
                <span className="image-content">
                  <h3 className="image-title">{image.title}</h3>
                  <p className="image-desc">{image.desc}</p>
                  <span className="image-number">{String(image.id).padStart(2, '0')}</span>
                </span>
              </span>
            </span>
          </li>
        ))}
      </ul>

      <style>{`
        .snapped-carousel-demo {
          .carousel {
            display: flex;
            gap: 20px;
            width: 100%;
            max-width: 100%;
            padding: 32px 20px;
            margin: 0;
            overflow-x: auto;
            list-style: none;
            scroll-snap-type: x mandatory;
            scroll-behavior: smooth;
            scrollbar-width: thin;
            scrollbar-color: #d9d9d9 transparent;
          }

          .carousel::-webkit-scrollbar {
            height: 10px;
          }

          .carousel::-webkit-scrollbar-track {
            background: #f5f5f5;
            border-radius: 5px;
          }

          .carousel::-webkit-scrollbar-thumb {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 5px;
          }

          .carousel::-webkit-scrollbar-thumb:hover {
            background: linear-gradient(135deg, #5568d3 0%, #65408b 100%);
          }

          .carousel-item {
            container-type: scroll-state;
            flex: 0 0 auto;
            width: 400px;
            scroll-snap-align: center;
            scroll-snap-stop: always;
          }

          .image-wrapper {
            display: block;
            width: 100%;
            height: 100%;
          }

          .image-placeholder {
            display: block;
            width: 100%;
            height: 280px;
            overflow: hidden;
            border-radius: 16px;
            box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
            transition: scale 0.3s;
            transition-delay: 0.2s;
          }

          @container not scroll-state(snapped: inline) {
            .image-placeholder {
              scale: 0.85;
              box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
              transition-delay: 0s;
            }
          }

          .image-content {
            position: relative;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            width: 100%;
            height: 100%;
            padding: 32px;
            color: white;
            text-align: center;
          }

          .image-title {
            margin: 0 0 12px 0;
            font-size: 32px;
            font-weight: 700;
            text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
          }

          .image-desc {
            margin: 0;
            font-size: 16px;
            font-weight: 400;
            opacity: 0.95;
            text-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
          }

          .image-number {
            position: absolute;
            right: 24px;
            bottom: 24px;
            font-size: 72px;
            font-weight: 800;
            color: rgba(255, 255, 255, 0.15);
            line-height: 1;
          }
        }
      `}</style>
    </div>
  );
};

export default SnappedCarouselDemo;
💡 原理说明

每个轮播项声明 container-type: scroll-state,父容器使用 scroll-snap-type: x mandatoryscroll-snap-align: center 实现横向停靠。图片默认正常大小,通过 @container not scroll-state(snapped: inline) 检测未停靠状态,滚动中缩小到 85%。inline 表示检测内联方向(横向)的停靠。停靠后添加 transition-delay: 0.2s,创造延迟恢复的优雅效果。适合图片画廊、产品展示等场景。


核心概念

container-type: scroll-state

这是使用滚动状态查询的前提,必须在容器元素上声明:

.container {
  container-type: scroll-state;
}

stuck: 检测 sticky 固定状态

@container scroll-state(stuck: top) {
  /* 元素固定到顶部时的样式 */
}

方向值: top, bottom, left, right, block-start, block-end, inline-start, inline-end

scrollable: 检测可滚动状态

@container scroll-state(scrollable: top) {
  /* 容器可以向上滚动时的样式 */
}

方向值: top, bottom, left, right, block, inline

snapped: 检测停靠状态

@container scroll-state(snapped: inline) {
  /* 横向滚动停靠时的样式 */
}

方向值: x, y, block, inline

not 查询: 检测相反状态

@container not scroll-state(snapped: x) {
  /* 未停靠时的样式 */
}

实战技巧

1. 负边距技巧

当 sticky 元素需要占据空间但又不影响初始布局时:

.nav {
  margin-top: -72px; /* 抵消导航栏高度 */
}

2. Grid 动态列

通过改变 Grid 列定义实现元素显隐:

.nav-list {
  grid-template-columns: 1fr 1fr 1fr 1fr; /* 吸顶时 */
}

@container not scroll-state(stuck: top) {
  .nav-list {
    grid-template-columns: 0 0 0 1fr; /* 未吸顶时隐藏前三列 */
  }
}

3. 过渡延迟

为不同元素设置延迟,创造序列动画:

.item {
  transition: all 0.3s;
  transition-delay: 0.2s; /* 停靠后延迟恢复 */
}

@container not scroll-state(snapped) {
  .item {
    transition-delay: 0s; /* 滚动中立即缩小 */
  }
}

参考资料