Skip to main content

Smooth Scroll

MonoTerm implements pixel-level scroll accumulation for fluid scrolling experience, especially on trackpads.

The Problem: Row-Based Scrolling

Traditional terminals scroll by whole rows. This creates jerky movement on trackpads that send sub-row pixel deltas.
╔═══════════════════════════════════════════════════════════════════════════════╗
║  TRADITIONAL ROW-BASED SCROLLING                                              ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  Trackpad sends: deltaY = 3px, 5px, 2px, 4px, 6px...                         ║
║                                                                               ║
║  Row height = 20px                                                            ║
║                                                                               ║
║  ┌─────────────────────────────────────────────────────────────────────────┐  ║
║  │                                                                         │  ║
║  │  Input:   3px   5px   2px   4px   6px   (total: 20px)                   │  ║
║  │            │     │     │     │     │                                    │  ║
║  │            ▼     ▼     ▼     ▼     ▼                                    │  ║
║  │  Result:  NONE  NONE  NONE  NONE  JUMP 1 ROW                            │  ║
║  │                                                                         │  ║
║  │           ────────────────────────┬────                                 │  ║
║  │                   Nothing         │ Sudden jump                         │  ║
║  │                                                                         │  ║
║  └─────────────────────────────────────────────────────────────────────────┘  ║
║                                                                               ║
║  User experience: Laggy, then sudden jump                                     ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

The Solution: Pixel Accumulation

MonoTerm accumulates sub-row pixel deltas until a full row is reached.
╔═══════════════════════════════════════════════════════════════════════════════╗
║  PIXEL ACCUMULATION SCROLLING                                                 ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  Trackpad sends: deltaY = 3px, 5px, 2px, 4px, 6px...                         ║
║                                                                               ║
║  Row height = 20px                                                            ║
║                                                                               ║
║  ┌─────────────────────────────────────────────────────────────────────────┐  ║
║  │                                                                         │  ║
║  │  pendingY accumulator:                                                  │  ║
║  │                                                                         │  ║
║  │  Input:    3px    5px    2px    4px    6px                              │  ║
║  │             │      │      │      │      │                               │  ║
║  │             ▼      ▼      ▼      ▼      ▼                               │  ║
║  │  pendingY:  3  →   8  →  10  →  14  →  20                               │  ║
║  │             │      │      │      │      │                               │  ║
║  │             ▼      ▼      ▼      ▼      ▼                               │  ║
║  │  Action:  wait   wait   wait   wait   SCROLL 1 ROW                      │  ║
║  │                                       pendingY = 0                      │  ║
║  │                                                                         │  ║
║  └─────────────────────────────────────────────────────────────────────────┘  ║
║                                                                               ║
║  User experience: Smooth, predictable scrolling                               ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

State Interface

interface SmoothScrollState {
    pendingY: number;      // Accumulated pixels not yet scrolled
    rowHeight: number;     // Current row height in pixels
    isAnimating: boolean;  // Animation in progress
    animationId: number;   // RAF handle for cancellation
}

Trackpad vs Mouse Detection

MonoTerm detects input device type and applies different scroll behavior.
╔═══════════════════════════════════════════════════════════════════════════════╗
║  INPUT DEVICE DETECTION                                                       ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  Detection logic:                                                             ║
║  ─────────────────                                                            ║
║                                                                               ║
║  isPrecision = Math.abs(ev.deltaY) < 10 &&                                    ║
║                ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL                    ║
║                                                                               ║
║                                                                               ║
║  ┌─────────────────────────────┐    ┌─────────────────────────────┐          ║
║  │                             │    │                             │          ║
║  │  TRACKPAD (isPrecision)     │    │  MOUSE WHEEL (!isPrecision) │          ║
║  │                             │    │                             │          ║
║  │  deltaY: 1-9 pixels         │    │  deltaY: 100+ pixels        │          ║
║  │  deltaMode: PIXEL           │    │  deltaMode: LINE or PAGE    │          ║
║  │                             │    │                             │          ║
║  │  ┌───────────────────────┐  │    │  ┌───────────────────────┐  │          ║
║  │  │ Pixel accumulation    │  │    │  │ Direct row scroll     │  │          ║
║  │  │ + Bezier animation    │  │    │  │ (traditional behavior)│  │          ║
║  │  └───────────────────────┘  │    │  └───────────────────────┘  │          ║
║  │                             │    │                             │          ║
║  └─────────────────────────────┘    └─────────────────────────────┘          ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

Bezier Animation

For smooth deceleration, MonoTerm applies easing functions to scroll animation.
╔═══════════════════════════════════════════════════════════════════════════════╗
║  BEZIER EASING FUNCTIONS                                                      ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  Available easing curves:                                                     ║
║                                                                               ║
║                                                                               ║
║  ease-out (default)              ease-in                 ease-in-out          ║
║  ─────────────────               ────────                ───────────          ║
║  f(t) = 1 - (1-t)³               f(t) = t³               cubic-bezier         ║
║                                                                               ║
║       │    xxxxxxx                    │         x             │      xxx      ║
║       │  xx                           │        x              │    xx   xx    ║
║       │ x                             │       x               │   x       x   ║
║       │x                              │     xx                │  x         x  ║
║       │x                              │   xx                  │ x           x ║
║       │x                              │ xx                    │x             x║
║       └───────────                    └───────────            └───────────    ║
║                                                                               ║
║  Fast start,                     Slow start,              Smooth both         ║
║  smooth stop                     fast end                 ends                ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

Configuration

Users can customize scroll behavior via settings:
SettingDefaultDescription
terminalSmoothScrollDuration150msAnimation duration
terminalSmoothScrollEasingease-outEasing function
╔═══════════════════════════════════════════════════════════════════════════════╗
║  SCROLL BEHAVIOR FLOW                                                         ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  Wheel Event                                                                  ║
║       │                                                                       ║
║       ▼                                                                       ║
║  ┌─────────────────────┐                                                      ║
║  │ Check alt buffer    │──── YES ───▶ Direct scroll (vim/tmux mode)          ║
║  └─────────┬───────────┘                                                      ║
║            │ NO                                                               ║
║            ▼                                                                  ║
║  ┌─────────────────────┐                                                      ║
║  │ isPrecision?        │──── NO ────▶ Traditional row scroll                 ║
║  └─────────┬───────────┘                                                      ║
║            │ YES (trackpad)                                                   ║
║            ▼                                                                  ║
║  ┌─────────────────────┐                                                      ║
║  │ Add to pendingY     │                                                      ║
║  └─────────┬───────────┘                                                      ║
║            │                                                                  ║
║            ▼                                                                  ║
║  ┌─────────────────────┐                                                      ║
║  │ |pendingY| >= row?  │──── NO ────▶ Wait for more input                    ║
║  └─────────┬───────────┘                                                      ║
║            │ YES                                                              ║
║            ▼                                                                  ║
║  ┌─────────────────────┐                                                      ║
║  │ Calculate rows      │                                                      ║
║  │ rows = pendingY/h   │                                                      ║
║  └─────────┬───────────┘                                                      ║
║            │                                                                  ║
║            ▼                                                                  ║
║  ┌─────────────────────┐                                                      ║
║  │ Animate scroll      │                                                      ║
║  │ (Bezier easing)     │                                                      ║
║  └─────────┬───────────┘                                                      ║
║            │                                                                  ║
║            ▼                                                                  ║
║  ┌─────────────────────┐                                                      ║
║  │ pendingY = remainder│                                                      ║
║  └─────────────────────┘                                                      ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

Alt Buffer Bypass

When in alternate screen mode (vim, tmux, less), smooth scroll is bypassed for native app handling.
╔═══════════════════════════════════════════════════════════════════════════════╗
║  ALT BUFFER DETECTION                                                         ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  Normal buffer (shell):                                                       ║
║  ──────────────────────                                                       ║
║  ┌─────────────────────────────────────┐                                      ║
║  │ $ ls -la                            │                                      ║
║  │ total 48                            │  ← Smooth scroll enabled             ║
║  │ drwxr-xr-x  12 user  staff   384    │                                      ║
║  │ -rw-r--r--   1 user  staff  1024    │                                      ║
║  └─────────────────────────────────────┘                                      ║
║                                                                               ║
║                                                                               ║
║  Alt buffer (vim/tmux):                                                       ║
║  ──────────────────────                                                       ║
║  ┌─────────────────────────────────────┐                                      ║
║  │ ~                                   │                                      ║
║  │ ~ VIM - file.txt                    │  ← Direct scroll to app             ║
║  │ ~                                   │    (vim handles scrolling)          ║
║  │ -- INSERT --                        │                                      ║
║  └─────────────────────────────────────┘                                      ║
║                                                                               ║
║                                                                               ║
║  Detection: term.buffer.active.type === 'alternate'                           ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

Implementation

Key code location: terminal-scrollbar.ts
private handleWheel(ev: WheelEvent): void {
    // Alt buffer bypass
    if (this.term.buffer.active.type === 'alternate') {
        return; // Let the app handle it
    }

    const isPrecision = Math.abs(ev.deltaY) < 10 &&
                        ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL;

    if (isPrecision) {
        // Trackpad: accumulate pixels
        this.state.pendingY += ev.deltaY;

        const rowHeight = this.getRowHeight();
        const rows = Math.trunc(this.state.pendingY / rowHeight);

        if (rows !== 0) {
            this.animateScroll(rows);
            this.state.pendingY %= rowHeight; // Keep remainder
        }
    } else {
        // Mouse wheel: direct scroll
        const rows = Math.sign(ev.deltaY) * 3; // 3 rows per notch
        this.term.scrollLines(rows);
    }
}

Sync Rendering

Immediate render for flicker-free display

xterm.js Renderer

How xterm.js is used as display component