Elevating user experience with turbo: maintaining scroll position made easy 

post cover picture

Turbo is an excellent tool for implementing dynamic user experiences that match common JavaScript framework behaviors, all without a single line of JavaScript code. Frequently in our applications, we need to submit a form or click a link while keeping the individual in the same position to ensure a seamless user experience. In this blog post, we'll explore how to maintain the scroll position using Turbo. 

To illustrate this, we'll use an example of a developer questionnaire within an app. Click here to watch a video demonstration of the functionality.


The scroll position challenge

As seen in the video, it's not ideal for users to navigate through the page and scroll back to continue filling out the form. In this simple implementation, when a user answers a question, we only save the value in the backend. However, forms often change based on user responses, and utilizing Turbo to update the form can spare us from the need to write complex JavaScript logic.

The implementation

The execution is pretty straightforward.  

We have a form with various questions:

<%= form_with model: @questionnaire, html: { 'x-ref': 'form' } do |form| %>
  <h3 class="text-center text-2xl mb-4">
    <span class="text-secondary font-bold text-blue-700">a.</span>
    Are you a full-stack developer?

  <div class="px-3 flex justify-center gap-3 mb-8">
    <%= form.radio_button :fullstack_developer, true, class: 'hidden', '@click': '$refs.form.requestSubmit()' %>
    <%= form.label :fullstack_developer, 'YES', value: true,
        class: "inline-block btn text-3xl font-bold flex border basis-1/4 mb-0 uppercase cursor-pointer #{ 'btn-primary text-white' if @questionnaire.fullstack_developer }" %>

    <%= form.radio_button :fullstack_developer, false, class: 'hidden', '@click': '$refs.form.requestSubmit()' %>
    <%= form.label :fullstack_developer, 'NO', value: false,
        class: "inline-block btn text-3xl font-bold flex border basis-1/4 mb-0 uppercase cursor-pointer #{ 'btn-primary text-white' if @questionnaire.fullstack_developer == false }" %>

<% end %>

Take into account we're using Alpine.js to trigger automatic form submission:

'@click': '$refs.form.requestSubmit()'


Our controller is also quite basic:

class DeveloperQuestionnairesController < ApplicationController
  before_action :load_questionnaire

  def edit; end

  def update
   redirect_to edit_developer_questionnaire_path(@questionnaire)


Potential solution: using turbo frames

The simplest solution is to wrap the form in a Turbo frame:

<%= turbo_frame_tag 'developer_questionnair' do %>
<% end %>


Turbo frames allow any form or link inside them to change only the content enclosed by that Turbo frame tag, keeping the scroll position intact.


Handling multiple updates

But what if we need to update multiple parts of our site upon form submission?

When we answer questions, we also need to update the sidebar's checked items. When using a Turbo frame, only the content within the frame changes, leaving the sidebar unmodified.

Below, two Hotwire solutions:

1) Using Turbo Stream. This approach involves using replacements to both the questionnaire's Turbo frame and the sidebar. However, I personally recommend avoiding Turbo Stream views that update more than one element at the same time, unless absolutely necessary.

2) Utilizing a broadcast to update the sidebar. While broadcasts can be powerful, they might be dangerous, as they come with complexity and numerous potential database queries if not managed carefully. 

Therefore, we'll exclude these two approaches.


Final solution: turbo-preserve-scroll 

Based on suggestions from the following issue, we'll use Turbo.navigator.currentVisit.scrolled to maintain the user's scroll position. 


We'll add the following JavaScript code:

window.shouldPreserveScroll = false;

document.addEventListener("submit", function(event) {
  if (event.target.hasAttribute("data-turbo-preserve-scroll")) {
    window.shouldPreserveScroll = true;

addEventListener("turbo:render", () => {
  if (window.shouldPreserveScroll) {
    Turbo.navigator.currentVisit.scrolled = true;
    window.shouldPreserveScroll = false;

Then, in our form, we'll include the data-turbo-preserve-scroll attribute:

<%= form_with model: @questionnaire, html: { 'x-ref': 'form' }, data: { 'turbo-preserve-scroll': true } do |form| %>



By implementing Turbo and the turbo-preserve-scrolll solution, we can enhance user experience by maintaining its scroll position during form submissions. This approach not only improves usability but also reduces the need for complex JavaScript code. Turbo offers a powerful yet elegant solution for dynamic web applications without the hassle of traditional JavaScript frameworks. Give it a try and elevate your UX to the next level.

We encourage you to delve into our blog to explore more enlightening solutions and a wide range of cutting-edge technologies to improve your UX effortlessly.

Want to know more about us?

post creator picture
Santiago Bertinat
October 05, 2023

Would you like to stay updated?