Smithery Logo
MCPsSkillsDocsPricing
Login
NewFlame, an assistant that learns and improves. Available onTelegramSlack
    Manuelvillarvieites

    butter-smooth-scrolling

    Manuelvillarvieites/butter-smooth-scrolling
    Design

    About

    SKILL.md

    Install

    • Telegram
      Telegram
    • Slack
      Slack
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    • Download skill
    ├─
    ├─
    └─
    Smithery Logo

    Give agents more agency

    Resources

    DocumentationPrivacy PolicySystem Status

    Company

    PricingAboutBlog

    Connect

    © 2026 Smithery. All rights reserved.

    About

    Add cinematic lerp-based smooth scrolling for all scroll inputs (mouse wheel, trackpad, touch). Creates butter-smooth momentum scrolling like premium agency websites...

    SKILL.md

    Butter Smooth Scrolling

    Add cinematic, momentum-based smooth scrolling that interpolates scroll position using requestAnimationFrame. Creates the "butter smooth" feel seen on premium agency websites.

    Important: How This Differs from CSS Smooth Scroll

    Feature CSS scroll-behavior: smooth Butter Smooth Scrolling
    Anchor clicks Yes No (use existing smooth-scroll)
    Mouse wheel No Yes
    Trackpad No Yes
    Touch No Yes
    Momentum feel No Yes
    Configurable smoothness No Yes
    Performance impact None Minimal (RAF optimized)

    This skill complements the existing smooth-scroll skill - they work together, not as replacements.

    Prerequisites

    • Next.js App Router
    • React 19
    • Existing smooth-scroll skill for anchor links (optional but recommended)

    Workflow

    1. Create ButterScroll Component - Create components/ui/butter-scroll.tsx
    2. Add to Layout - Include component in locale layout (alongside PageTransition)
    3. Configure Options - Adjust lerp factor for desired smoothness
    4. Test - Verify smooth scrolling on all inputs
    5. Accessibility - Confirm reduced motion preference is respected

    Implementation

    Step 1: Create ButterScroll Component

    Create website/components/ui/butter-scroll.tsx:

    "use client";
    
    import { useEffect, useRef, useCallback } from "react";
    
    interface ButterScrollOptions {
      /**
       * Lerp factor (0-1). Lower = smoother but slower.
       * 0.06 = very smooth (cinematic)
       * 0.12 = balanced (default)
       * 0.18 = snappy
       */
      lerp?: number;
      /**
       * Scroll sensitivity multiplier.
       * 1.0 = normal, 0.5 = slower, 1.5 = faster
       */
      sensitivity?: number;
      /**
       * Disable butter scroll for trackpad (use native)
       */
      disableTrackpad?: boolean;
      /**
       * Disable butter scroll for touch (use native)
       */
      disableTouch?: boolean;
      /**
       * Minimum velocity threshold to stop animation (performance)
       */
      threshold?: number;
    }
    
    const defaultOptions: Required<ButterScrollOptions> = {
      lerp: 0.12,
      sensitivity: 1.0,
      disableTrackpad: false,
      disableTouch: false,
      threshold: 0.5,
    };
    
    export function ButterScroll(props: ButterScrollOptions = {}) {
      const options = { ...defaultOptions, ...props };
    
      const targetScrollY = useRef(0);
      const currentScrollY = useRef(0);
      const rafId = useRef<number | null>(null);
      const isScrolling = useRef(false);
      const lastWheelTime = useRef(0);
      const touchStartY = useRef(0);
      const lastTouchY = useRef(0);
    
      // Linear interpolation function
      const lerp = useCallback((start: number, end: number, factor: number) => {
        return start + (end - start) * factor;
      }, []);
    
      // Get max scroll position
      const getMaxScroll = useCallback(() => {
        return document.documentElement.scrollHeight - window.innerHeight;
      }, []);
    
      // Clamp target scroll to valid range
      const clampScroll = useCallback(
        (value: number) => {
          return Math.max(0, Math.min(getMaxScroll(), value));
        },
        [getMaxScroll]
      );
    
      // Animation loop
      const animate = useCallback(() => {
        // Calculate new scroll position
        currentScrollY.current = lerp(
          currentScrollY.current,
          targetScrollY.current,
          options.lerp
        );
    
        // Apply scroll position
        window.scrollTo(0, currentScrollY.current);
    
        // Check if we should continue animating
        const diff = Math.abs(targetScrollY.current - currentScrollY.current);
    
        if (diff > options.threshold) {
          rafId.current = requestAnimationFrame(animate);
        } else {
          // Snap to final position
          window.scrollTo(0, targetScrollY.current);
          currentScrollY.current = targetScrollY.current;
          isScrolling.current = false;
          rafId.current = null;
        }
      }, [lerp, options.lerp, options.threshold]);
    
      // Start animation if not running
      const startAnimation = useCallback(() => {
        if (!isScrolling.current) {
          isScrolling.current = true;
          currentScrollY.current = window.scrollY;
          rafId.current = requestAnimationFrame(animate);
        }
      }, [animate]);
    
      // Detect if wheel event is from trackpad
      const isTrackpadEvent = useCallback((e: WheelEvent) => {
        const now = Date.now();
        const timeDelta = now - lastWheelTime.current;
        lastWheelTime.current = now;
    
        // Trackpad typically has smaller, more frequent deltas
        return Math.abs(e.deltaY) < 50 && timeDelta < 50;
      }, []);
    
      // Wheel event handler (mouse wheel + trackpad)
      const handleWheel = useCallback(
        (e: WheelEvent) => {
          // Check if trackpad and should be disabled
          if (options.disableTrackpad && isTrackpadEvent(e)) {
            return;
          }
    
          // Prevent default scroll
          e.preventDefault();
    
          // Normalize delta across browsers
          const normalizedDelta =
            e.deltaMode === 1
              ? e.deltaY * 20 // Line mode (Firefox)
              : e.deltaY; // Pixel mode (Chrome, Safari)
    
          // Update target scroll position
          targetScrollY.current = clampScroll(
            targetScrollY.current + normalizedDelta * options.sensitivity
          );
    
          // Start animation
          startAnimation();
        },
        [
          clampScroll,
          isTrackpadEvent,
          options.disableTrackpad,
          options.sensitivity,
          startAnimation,
        ]
      );
    
      // Touch start handler
      const handleTouchStart = useCallback(
        (e: TouchEvent) => {
          if (options.disableTouch) return;
    
          touchStartY.current = e.touches[0].clientY;
          lastTouchY.current = e.touches[0].clientY;
    
          // Sync current scroll position
          if (!isScrolling.current) {
            targetScrollY.current = window.scrollY;
            currentScrollY.current = window.scrollY;
          }
        },
        [options.disableTouch]
      );
    
      // Touch move handler
      const handleTouchMove = useCallback(
        (e: TouchEvent) => {
          if (options.disableTouch) return;
    
          const currentTouchY = e.touches[0].clientY;
          const deltaY = lastTouchY.current - currentTouchY;
          lastTouchY.current = currentTouchY;
    
          // Prevent default to take over scroll
          e.preventDefault();
    
          // Update target (inverted because dragging down = scroll up)
          targetScrollY.current = clampScroll(
            targetScrollY.current + deltaY * options.sensitivity
          );
    
          // Start animation
          startAnimation();
        },
        [clampScroll, options.disableTouch, options.sensitivity, startAnimation]
      );
    
      // Touch end handler - add momentum
      const handleTouchEnd = useCallback(() => {
        // Animation continues naturally due to lerp
      }, []);
    
      // Sync scroll position on resize or programmatic scroll
      const syncScroll = useCallback(() => {
        if (!isScrolling.current) {
          targetScrollY.current = window.scrollY;
          currentScrollY.current = window.scrollY;
        }
      }, []);
    
      useEffect(() => {
        // Check for reduced motion preference
        const prefersReducedMotion = window.matchMedia(
          "(prefers-reduced-motion: reduce)"
        ).matches;
    
        if (prefersReducedMotion) {
          // Skip all butter scroll behavior for accessibility
          return;
        }
    
        // Initialize scroll position
        targetScrollY.current = window.scrollY;
        currentScrollY.current = window.scrollY;
    
        // Add wheel listener with passive: false to allow preventDefault
        window.addEventListener("wheel", handleWheel, { passive: false });
    
        // Add touch listeners if not disabled
        if (!options.disableTouch) {
          window.addEventListener("touchstart", handleTouchStart, {
            passive: true,
          });
          window.addEventListener("touchmove", handleTouchMove, { passive: false });
          window.addEventListener("touchend", handleTouchEnd, { passive: true });
        }
    
        // Sync on scroll events (for anchor links, programmatic scrolls)
        window.addEventListener("scroll", syncScroll, { passive: true });
    
        // Sync on resize
        window.addEventListener("resize", syncScroll, { passive: true });
    
        return () => {
          window.removeEventListener("wheel", handleWheel);
          window.removeEventListener("touchstart", handleTouchStart);
          window.removeEventListener("touchmove", handleTouchMove);
          window.removeEventListener("touchend", handleTouchEnd);
          window.removeEventListener("scroll", syncScroll);
          window.removeEventListener("resize", syncScroll);
    
          if (rafId.current) {
            cancelAnimationFrame(rafId.current);
          }
        };
      }, [
        handleWheel,
        handleTouchStart,
        handleTouchMove,
        handleTouchEnd,
        syncScroll,
        options.disableTouch,
      ]);
    
      // This component renders nothing
      return null;
    }
    

    Step 2: Add to Layout

    Add ButterScroll to app/[locale]/layout.tsx:

    import { PageTransition } from "@/components/ui/page-transition";
    import { ButterScroll } from "@/components/ui/butter-scroll";
    import { Navbar2 } from "@/components/navbar2";
    import { Footer2 } from "@/components/footer2";
    
    export default async function LocaleLayout({
      children,
      params,
    }: Props) {
      // ... existing code ...
    
      return (
        <NextIntlClientProvider messages={messages}>
          <PageTransition />
          <ButterScroll lerp={0.12} />
          <Navbar2 />
          <main>{children}</main>
          <Footer2 />
        </NextIntlClientProvider>
      );
    }
    

    Important:

    • ButterScroll is a sibling component, not a wrapper
    • Place it early in the component tree (after PageTransition)
    • It renders nothing to the DOM

    Step 3: Configure Smoothness

    Adjust the lerp prop based on desired feel:

    // Ultra smooth (cinematic, slow response)
    <ButterScroll lerp={0.06} />
    
    // Smooth (default, balanced)
    <ButterScroll lerp={0.12} />
    
    // Responsive (faster, less momentum)
    <ButterScroll lerp={0.18} />
    
    // Quick (minimal smoothing, snappy)
    <ButterScroll lerp={0.25} />
    

    Step 4: Additional Options

    // Default: all inputs enabled
    <ButterScroll lerp={0.12} />
    
    // Disable for trackpad only (use native trackpad scroll)
    <ButterScroll lerp={0.12} disableTrackpad />
    
    // Disable for touch only (use native touch scroll)
    <ButterScroll lerp={0.12} disableTouch />
    
    // Custom configuration
    <ButterScroll
      lerp={0.1}              // Smoothness factor
      sensitivity={1.2}       // Scroll speed multiplier
      disableTrackpad={false} // Enable for trackpad
      disableTouch={false}    // Enable for touch
      threshold={0.3}         // Animation stop threshold
    />
    

    How It Works

    1. User scrolls (wheel/trackpad/touch)
       ↓
    2. Event captured (preventDefault)
       ↓
    3. Target scroll position updated (current + delta)
       ↓
    4. requestAnimationFrame loop starts
       ↓
    5. Each frame: currentScroll = lerp(currentScroll, targetScroll, 0.12)
       ↓
    6. window.scrollTo(0, currentScroll)
       ↓
    7. Loop continues until difference < threshold
       ↓
    8. Snap to final position, stop animation
    

    Lerp (Linear Interpolation) Explained

    lerp(start, end, factor) = start + (end - start) * factor
    
    Example with factor 0.12:
    - Frame 1: 0 + (100 - 0) * 0.12 = 12
    - Frame 2: 12 + (100 - 12) * 0.12 = 22.56
    - Frame 3: 22.56 + (100 - 22.56) * 0.12 = 31.85
    - ... gradually approaches 100
    

    Lower factor = more frames to reach target = smoother feel

    Performance Considerations

    1. GPU Acceleration: window.scrollTo is optimized by browsers
    2. requestAnimationFrame: Syncs with display refresh rate (60fps)
    3. Threshold cutoff: Stops animation when movement imperceptible
    4. Reduced motion: Completely disabled for accessibility
    5. No DOM manipulation: Scroll position only, no transform hacks

    Accessibility

    The component automatically respects prefers-reduced-motion:

    const prefersReducedMotion = window.matchMedia(
      "(prefers-reduced-motion: reduce)"
    ).matches;
    
    if (prefersReducedMotion) {
      return; // Skip all butter scroll behavior
    }
    

    Users with reduced motion preference get native browser scrolling.

    Compatibility with Other Features

    Feature Compatibility
    Smooth scroll anchor links Works (sync on scroll event)
    Page transitions Works (component sibling)
    Framer Motion animations Works (no conflict)
    Fixed navbar Works (scroll position accurate)
    Lazy loading images Works (native scroll events fire)
    Intersection Observer Works (scroll position updates)

    What This Skill Does NOT Do

    • Handle keyboard scrolling (arrow keys, Page Up/Down stay native)
    • Provide scroll-linked animations (use Framer Motion)
    • Work when reduced motion is enabled (accessibility)

    Troubleshooting

    Scroll feels choppy

    • Increase lerp factor: lerp={0.15}
    • Check for heavy JavaScript blocking main thread

    Anchor links don't work

    • Ensure smooth-scroll skill is active
    • Check syncScroll is being called on scroll events

    Scroll is too slow

    • Increase sensitivity: sensitivity={1.5}
    • Increase lerp: lerp={0.18}

    Touch feels wrong on specific device

    • Try disableTouch={true} to use native touch scroll

    Checklist

    • ButterScroll component created at components/ui/butter-scroll.tsx
    • Component added to app/[locale]/layout.tsx
    • Lerp factor configured (default 0.12)
    • Test: Mouse wheel scrolling feels smooth and momentum-like
    • Test: Trackpad scrolling feels smooth
    • Test: Touch scrolling feels smooth on mobile
    • Test: Anchor links still work (smooth-scroll skill)
    • Test: Page transitions work (page-transitions skill)
    • Test: Reduced motion preference disables effect
    • Test: 60fps maintained (Chrome DevTools Performance)

    Output

    After running this skill:

    • components/ui/butter-scroll.tsx - Lerp scroll component
    • app/[locale]/layout.tsx - Updated with ButterScroll
    • Cinematic butter-smooth scrolling on all inputs
    Recommended Servers
    Browser tool
    Browser tool
    Memory Tool
    Memory Tool
    GroundRoute: Web Search for AI Agents across 6 Engines ($10 free credit)
    GroundRoute: Web Search for AI Agents across 6 Engines ($10 free credit)
    Repository
    manuelvillarvieites/n8n-and-claude
    Files