Плавный scroll с помощью window.scrollBy()

Всем привет. Подскажите пожалуйста, как сделать плавный scroll c помощью window.scrollBy().

const btnPort = document.getElementById('port');


btnPort.addEventListener('click', (e) => {
  e.preventDefault();

  window.scrollBy(0, 800);
});
.final {
  margin-top: 800px;
}
    <div class="promo">
      <div class="promo__btns">
        <a class="promo__link  btn" href="#" id="port">НАЖМИ</a>
      </div>
    </div>

  
  <div class="final">
      <h1 class="final__block">ФИНАЛ<h1>
  <div>


Ответы (2 шт):

Автор решения: Евгений Николаев

В scrollBy можно передать options объект, в котором параметр behavior отвечает за поведение прокрутки.

const btnPort = document.getElementById('port');


btnPort.addEventListener('click', (e) => {
  e.preventDefault();

  window.scrollBy({
    top: 800,
    behavior : "smooth"
  });
});
.final {
  margin-top: 800px;
}
<div class="promo">
      <div class="promo__btns">
        <a class="promo__link  btn" href="#" id="port">НАЖМИ</a>
      </div>
    </div>

  
  <div class="final">
      <h1 class="final__block">ФИНАЛ<h1>
  <div>

→ Ссылка
Автор решения: ᅠhᅠ

Поддержка сафари + некоторые настройки.

class OptionsParser {
  constructor(str = '', defaultOptions) {
    this.defaultOptions = defaultOptions
    this.options = {}
    
    str && this.parseOptions(str)
    this.applyOptions()
  }
  
  parseOptions(str) {
    str
      .split(',')
      .map(option => option.trim())
      .filter(option => !!option)
      .forEach(option => {
      
      const parts = option
        .split('=')
        .map(option => option.trim())
        .filter(option => !!option)
      
      if(parts[1] === 'false') {
        this.options[parts[0]] = false
      }

      else if(parts[1] === 'true') {
        this.options[parts[0]] = true
      }

      else if(isNaN(parts[1])) {
        this.options[parts[0]] = parts[1] ? parts[1] : true
      }
  
      else if(!isNaN(parts[1])){
        this.options[parts[0]] = +parts[1]
      }
    })

  }
  
  applyOptions() {
    for(const member in this.defaultOptions) {
      this.options[member] = this.options[member] || this.defaultOptions[member]
    }
  }
}

utils = {}

utils.cumulativeTop = function(element) {
  let top = 0
  do {
    top += element.offsetTop || 0
    element = element.offsetParent
  } while (element)
  return top
}

utils.easings = {
  linear: t => t,
  easeInQuad: t => t*t,
  easeOutQuad: t => t*(2-t),
  easeInOutQuad: t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
  easeInCubic: t => t*t*t,
  easeOutCubic: t => (--t)*t*t+1,
  easeInOutCubic: t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
  easeInQuart: t => t*t*t*t,
  easeOutQuart: t => 1-(--t)*t*t*t,
  easeInOutQuart: t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t,
  easeInQuint: t => t*t*t*t*t,
  easeOutQuint: t => 1+(--t)*t*t*t*t,
  easeInOutQuint: t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t,
}

class ScrollTo extends OptionsParser{
  constructor(trigger) {
    super(
      trigger.getAttribute('data-options'),
      {
        align: 0,
        durationStretch: 1,
        ease: 'easeInOutCubic',
        offsetFrom: null
      }
    )
    
    this.trigger = trigger
    if(!this.trigger) return;
    
    this.targetName = this.trigger.getAttribute('data-scroll-to')
    this.target = document.querySelector(this.targetName) || document.body
    
    this.options.ease = utils.easings[this.options.ease] || utils.easings.easeOutCubic
    this.options.offsetFrom = document.querySelector(this.options.offsetFrom) || null
    
    this.start = this.start.bind(this)
    this.move = this.move.bind(this)
    this.stop = this.stop.bind(this)
    
    this.frameId = null
    this.currentScroll = 0
    this.to = 0
    this.align = 0
    this.offsetFrom = 0
    this.startTime = 0
    
    this.listen()
   
  }
  
  listen() {
    this.trigger.addEventListener('click', this.start)
  }
  
  start() {
    document.addEventListener('wheel', this.stop)
    document.addEventListener('touchstart', this.stop)
    
    setTimeout(() => {
      this.offsetFrom = this.options.offsetFrom ? -this.options.offsetFrom.offsetHeight : 0
      this.align = this.options.align ? this.options.align * innerHeight - this.target.offsetHeight / 2 : 0
      this.currentScroll = scrollY
      this.to = this.currentScroll - (utils.cumulativeTop(this.target) + this.offsetFrom) + this.align
      this.startTime = Date.now()
      this.frameId = requestAnimationFrame(this.move)
    }, 0)
  }
  
  move() {
    this.frameId = requestAnimationFrame(this.move)
    
    let t = (Date.now() - this.startTime) / (1000 * this.options.durationStretch)
    t = this.options.ease(t)
    window.scrollTo(0, Math.abs(this.currentScroll - t * this.to))
    
    if(t >= 1) this.stop()
  }
  
  stop() {
    document.removeEventListener('wheel', this.stop)
    document.removeEventListener('touchstart', this.stop)
    cancelAnimationFrame(this.frameId)
  }
}

document.querySelectorAll('[data-scroll-to]').forEach(trigger => new ScrollTo(trigger))
body {
  margin: 0;
  height: 1000vh;
}

.header-placeholder {
  height: 10vh;
}

header {
  position: fixed;
  height: 10vh;
  width: 100%;
  top: 0;
  left: 0;
  background-color: lightpink;
}

.tomato {
  background: tomato;
}

.lightblue {
  background: lightblue;
}

.lightgreen {
  background: lightgreen;
}

section {
  height: 50vh;
  margin-top: 100vh;
}

.buttons {
  position: fixed;
  top: 10vh;
}
<div class="header-placeholder"></div>
<header></header>

<div class="buttons">
  <button data-scroll-to=".section-1" data-options="offsetFrom = .header-placeholder" class="tomato">click</button>
  <button data-scroll-to=".section-2" data-options="durationStretch = 2, align = 0.5, ease = easeOutQuint" class="lightblue">click</button>
  <button data-scroll-to=".section-3"  data-options="align = 0.75" class="lightgreen">click</button>
</div>

<section class="section-1 tomato"></section>
<section class="section-2 lightblue"></section>
<section class="section-3 lightgreen"></section>

→ Ссылка