返回动效库
滚动高级

滚动剥离

滚动驱动的页面剥离效果,当前页面像贴纸一样从右上角卷起剥离,露出下一层内容。

滚动时continuous

交互预览

动效定义与代码

{
  "slug": "scroll-peel-away",
  "name": "滚动剥离",
  "nameEn": "Scroll Peel Away",
  "description": "滚动驱动的页面剥离效果,当前页面像贴纸一样从右上角卷起剥离,露出下一层内容。配合阴影和 3D 旋转营造真实的纸张卷曲质感。",
  "descriptionEn": "Scroll-driven peel-away effect where the current section curls up from the corner like a sticker being peeled off, revealing the next layer beneath. Combined with shadow and 3D rotation for realistic paper-curl texture.",
  "category": "scroll",
  "tags": [
    "scroll",
    "3d",
    "peel",
    "curl",
    "paper",
    "sticky",
    "apple"
  ],
  "trigger": "on-scroll",
  "difficulty": "advanced",
  "duration": "continuous",
  "easing": "cubic-bezier(0.22, 1, 0.36, 1)",
  "cssProperties": [
    "transform",
    "clip-path",
    "box-shadow",
    "opacity"
  ],
  "isGPUAccelerated": true,
  "previewBg": "light",
  "keywords": [
    "peel",
    "curl",
    "paper",
    "sticker",
    "scroll animation",
    "3D",
    "apple",
    "page transition"
  ],
  "useCases": [
    "Creative portfolio page transitions",
    "Magazine-style article navigation",
    "Product reveal sequences",
    "Interactive storytelling experiences"
  ],
  "relatedAnimations": [
    "scroll-page-turn",
    "scroll-reveal",
    "flip-card"
  ],
  "recommendedStyles": [
    "apple-style",
    "editorial",
    "full-page-scroll",
    "magazine-grid"
  ],
  "codeSnippets": [
    {
      "label": "CSS Keyframes",
      "language": "css",
      "code": "/* === Scroll Peel Away ===\n * The current section peels from the top-right corner,\n * curling diagonally to reveal the section beneath.\n * Driven by scroll progress via JS or scroll-timeline.\n */\n\n.peel-container {\n  position: relative;\n  overflow: hidden;\n}\n\n.peel-section {\n  position: sticky;\n  top: 0;\n  width: 100%;\n  height: 100vh;\n  will-change: transform, clip-path;\n  transform-origin: bottom left;\n}\n\n/* The curling corner overlay — a pseudo-element that\n   simulates the underside of the peeled paper */\n.peel-section::before {\n  content: \"\";\n  position: absolute;\n  top: 0;\n  right: 0;\n  width: 50%;\n  height: 50%;\n  background: linear-gradient(\n    135deg,\n    rgba(255, 255, 255, 0.1) 0%,\n    rgba(0, 0, 0, 0.15) 100%\n  );\n  transform-origin: top right;\n  pointer-events: none;\n  opacity: 0;\n  transition: opacity 0.15s linear;\n  border-radius: 0 0 0 40%;\n}\n\n/* Fold shadow along the diagonal crease */\n.peel-section::after {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  pointer-events: none;\n  background: linear-gradient(\n    135deg,\n    transparent 40%,\n    rgba(0, 0, 0, 0.25) 50%,\n    transparent 60%\n  );\n  opacity: 0;\n  transition: opacity 0.15s linear;\n}\n\n.peel-section[data-peeling=\"true\"]::before,\n.peel-section[data-peeling=\"true\"]::after {\n  opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .peel-section {\n    transform: none !important;\n    clip-path: none !important;\n    transition: none;\n  }\n  .peel-section::before,\n  .peel-section::after {\n    display: none;\n  }\n}"
    },
    {
      "label": "Tailwind CSS",
      "language": "tsx",
      "code": "\"use client\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\n\nfunction useScrollProgress() {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const [progress, setProgress] = useState(0);\n\n  useEffect(() => {\n    const el = containerRef.current;\n    if (!el) return;\n\n    function onScroll() {\n      const rect = el!.getBoundingClientRect();\n      const scrollable = el!.scrollHeight - window.innerHeight;\n      const scrolled = -rect.top;\n      setProgress(Math.max(0, Math.min(1, scrolled / scrollable)));\n    }\n\n    window.addEventListener(\"scroll\", onScroll, { passive: true });\n    onScroll();\n    return () => window.removeEventListener(\"scroll\", onScroll);\n  }, []);\n\n  return { containerRef, progress };\n}\n\ninterface PeelAwayProps {\n  pages: ReactNode[];\n  className?: string;\n}\n\nexport function PeelAway({ pages, className }: PeelAwayProps) {\n  const { containerRef, progress } = useScrollProgress();\n  const total = pages.length;\n  const containerHeight = `${total * 100}vh`;\n\n  return (\n    <div\n      ref={containerRef}\n      className={className}\n      style={{ height: containerHeight, position: \"relative\" }}\n    >\n      <div className=\"sticky top-0 h-screen w-full overflow-hidden\">\n        {pages.map((page, i) => {\n          const start = i / total;\n          const end = (i + 1) / total;\n          const local = Math.max(\n            0,\n            Math.min(1, (progress - start) / (end - start))\n          );\n\n          // Peel from top-right: rotate around bottom-left origin\n          const rotateX = local * -15;\n          const rotateZ = local * 8;\n          const translateY = local * -20;\n          const scale = 1 - local * 0.05;\n          const clipInset = local * 100; // percentage from top-right\n\n          // Diagonal clip-path: shrinks from top-right corner\n          const clipPath =\n            local > 0\n              ? `polygon(0% 0%, ${100 - clipInset}% 0%, 0% ${100 - clipInset}%)`\n              : \"none\";\n\n          return (\n            <div\n              key={i}\n              className=\"absolute inset-0\"\n              style={{\n                transformOrigin: \"bottom left\",\n                transform: `perspective(1200px) rotateX(${rotateX}deg) rotateZ(${rotateZ}deg) translateY(${translateY}%) scale(${scale})`,\n                clipPath: local > 0.01 ? clipPath : \"none\",\n                zIndex: total - i,\n                boxShadow:\n                  local > 0.01\n                    ? `-8px 8px ${30 * local}px rgba(0,0,0,${0.3 * local})`\n                    : \"none\",\n              }}\n            >\n              {page}\n            </div>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n\n// Usage:\n// <PeelAway pages={[\n//   <div className=\"h-screen bg-white flex items-center justify-center\">\n//     <h1 className=\"text-6xl font-bold text-slate-900\">First</h1>\n//   </div>,\n//   <div className=\"h-screen bg-amber-50 flex items-center justify-center\">\n//     <h1 className=\"text-6xl font-bold text-amber-900\">Second</h1>\n//   </div>,\n//   <div className=\"h-screen bg-sky-50 flex items-center justify-center\">\n//     <h1 className=\"text-6xl font-bold text-sky-900\">Third</h1>\n//   </div>,\n// ]} />"
    },
    {
      "label": "Framer Motion",
      "language": "tsx",
      "code": "import { motion, useScroll, useTransform } from \"framer-motion\";\nimport { useRef, type ReactNode } from \"react\";\n\n// Tip: disable 3D transforms for reduced motion users\n// import { useReducedMotion } from \"framer-motion\";\n\ninterface PeelPageProps {\n  children: ReactNode;\n  index: number;\n  total: number;\n}\n\nfunction PeelPage({ children, index, total }: PeelPageProps) {\n  const ref = useRef(null);\n  const { scrollYProgress } = useScroll({\n    target: ref,\n    offset: [\"start start\", \"end start\"],\n  });\n\n  const rotateX = useTransform(scrollYProgress, [0, 1], [0, -15]);\n  const rotateZ = useTransform(scrollYProgress, [0, 1], [0, 8]);\n  const y = useTransform(scrollYProgress, [0, 1], [\"0%\", \"-20%\"]);\n  const scale = useTransform(scrollYProgress, [0, 1], [1, 0.95]);\n  const opacity = useTransform(scrollYProgress, [0, 0.6, 1], [1, 0.8, 0]);\n  const shadow = useTransform(\n    scrollYProgress,\n    [0, 1],\n    [\n      \"-0px 0px 0px rgba(0,0,0,0)\",\n      \"-12px 12px 40px rgba(0,0,0,0.35)\",\n    ]\n  );\n\n  return (\n    <div ref={ref} style={{ height: \"100vh\" }}>\n      <motion.div\n        style={{\n          position: \"sticky\",\n          top: 0,\n          height: \"100vh\",\n          width: \"100%\",\n          transformOrigin: \"bottom left\",\n          rotateX,\n          rotateZ,\n          y,\n          scale,\n          opacity,\n          boxShadow: shadow,\n          zIndex: total - index,\n        }}\n      >\n        {children}\n      </motion.div>\n    </div>\n  );\n}\n\n// Usage:\n// <div style={{ perspective: \"1200px\" }}>\n//   <PeelPage index={0} total={3}>\n//     <div className=\"h-screen bg-white grid place-items-center\">\n//       <h1 className=\"text-6xl font-bold text-slate-900\">First</h1>\n//     </div>\n//   </PeelPage>\n//   <PeelPage index={1} total={3}>\n//     <div className=\"h-screen bg-amber-50 grid place-items-center\">\n//       <h1 className=\"text-6xl font-bold text-amber-900\">Second</h1>\n//     </div>\n//   </PeelPage>\n//   <PeelPage index={2} total={3}>\n//     <div className=\"h-screen bg-sky-50 grid place-items-center\">\n//       <h1 className=\"text-6xl font-bold text-sky-900\">Third</h1>\n//     </div>\n//   </PeelPage>\n// </div>"
    }
  ]
}