Smooth scroll implementation on website

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Implementing Smooth Scroll on Website

Smooth scroll is not just scroll-behavior: smooth in CSS. It's about controlling scroll speed and character: inertia, easing, section snap, animation synchronization. The native CSS variant provides one fixed smoothness value without customization capability.

CSS Approach: When It's Enough

For simple cases—anchor links within page, back to top—native CSS is sufficient:

html {
  scroll-behavior: smooth;
}

/* Offset for fixed header */
[id] {
  scroll-margin-top: 80px;
}
// Anchor navigation
document.querySelectorAll('a[href^="#"]').forEach((link) => {
  link.addEventListener('click', (e) => {
    e.preventDefault()
    const target = document.querySelector(link.getAttribute('href')!)
    if (target) {
      target.scrollIntoView({ behavior: 'smooth', block: 'start' })
    }
  })
})

Limitations: can't customize speed, no start/end events, uneven browser support, doesn't work with custom scroll container.

JavaScript Implementation without Libraries

When you need control: custom easing, callbacks, programmatic scroll with varying speed.

type EasingFn = (t: number) => number

const easings: Record<string, EasingFn> = {
  linear: (t) => t,
  easeInOutCubic: (t) => t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2,
  easeOutQuart: (t) => 1 - (1 - t) ** 4,
  easeInOutExpo: (t) => t === 0 ? 0 : t === 1 ? 1
    : t < 0.5 ? 2 ** (20 * t - 10) / 2
    : (2 - 2 ** (-20 * t + 10)) / 2,
}

function scrollTo(
  target: number | HTMLElement,
  options: {
    duration?: number
    easing?: keyof typeof easings
    offset?: number
    onComplete?: () => void
  } = {}
) {
  const {
    duration = 800,
    easing = 'easeInOutCubic',
    offset = 0,
    onComplete,
  } = options

  const startY = window.scrollY
  const endY = typeof target === 'number'
    ? target
    : target.getBoundingClientRect().top + window.scrollY + offset

  const distance = endY - startY
  const easeFn = easings[easing]
  let startTime: number | null = null

  function step(timestamp: number) {
    if (!startTime) startTime = timestamp
    const elapsed = timestamp - startTime
    const progress = Math.min(elapsed / duration, 1)
    const eased = easeFn(progress)

    window.scrollTo(0, startY + distance * eased)

    if (progress < 1) {
      requestAnimationFrame(step)
    } else {
      onComplete?.()
    }
  }

  requestAnimationFrame(step)
}

Scroll Snap

CSS Scroll Snap binds scroll to sections. Works natively and efficiently, requires no JS.

.scroll-container {
  height: 100vh;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;  /* or proximity */
}

.scroll-section {
  height: 100vh;
  scroll-snap-align: start;
  scroll-snap-stop: always;  /* prevent "flying over" section */
}

/* Horizontal snap for slider */
.slider {
  display: flex;
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
}

.slide {
  flex: 0 0 100%;
  scroll-snap-align: center;
}

Tracking Scroll Position

Reactive tracking for animations on scroll. Throttle via requestAnimationFrame instead of setTimeout:

class ScrollTracker {
  private scrollY = 0
  private ticking = false
  private listeners: Set<(y: number, direction: 'up' | 'down') => void> = new Set()
  private lastY = 0

  constructor() {
    window.addEventListener('scroll', this.onScroll, { passive: true })
  }

  private onScroll = () => {
    this.scrollY = window.scrollY

    if (!this.ticking) {
      requestAnimationFrame(() => {
        const direction = this.scrollY > this.lastY ? 'down' : 'up'
        this.listeners.forEach(fn => fn(this.scrollY, direction))
        this.lastY = this.scrollY
        this.ticking = false
      })
      this.ticking = true
    }
  }

  subscribe(fn: (y: number, direction: 'up' | 'down') => void) {
    this.listeners.add(fn)
    return () => this.listeners.delete(fn)
  }

  destroy() {
    window.removeEventListener('scroll', this.onScroll)
  }
}

// Usage
const tracker = new ScrollTracker()

tracker.subscribe((y, dir) => {
  // Hide header on scroll down, show on scroll up
  header.classList.toggle('header--hidden', dir === 'down' && y > 100)
})

Intersection Observer for Animations on Appearance

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('in-view')
        // Unsubscribe if animation is one-time
        observer.unobserve(entry.target)
      }
    })
  },
  {
    threshold: 0.15,       // 15% of element should be visible
    rootMargin: '0px 0px -50px 0px',  // bottom margin
  }
)

document.querySelectorAll('[data-animate]').forEach((el) => {
  observer.observe(el)
})
[data-animate] {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

[data-animate].in-view {
  opacity: 1;
  transform: translateY(0);
}

/* Staggered delay for groups */
[data-animate]:nth-child(1) { transition-delay: 0ms; }
[data-animate]:nth-child(2) { transition-delay: 100ms; }
[data-animate]:nth-child(3) { transition-delay: 200ms; }

Accessibility

prefers-reduced-motion — system flag that should be respected. Users with vestibular disorders disable animations in system settings.

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  [data-animate] {
    transition: none;
    opacity: 1;
    transform: none;
  }
}
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches

// If animation not needed — instant scroll
function safeScrollTo(target: HTMLElement) {
  if (prefersReduced) {
    target.scrollIntoView()
  } else {
    scrollTo(target, { duration: 800 })
  }
}

Timeline

Anchor navigation + Intersection Observer animations — half day. Custom smooth scroll with easing, scroll-tracker for header and staggered animations — 1 day.