Flutter for Single-Page Scrollable Websites with Navigator 2.0 — Part 2: Scroll To Position

In the first part of this series, we discussed the options when building a scrollable widget in a single-page scrollable website (SPSW) and explained the sample apps that we will be building. In this article, we will explore building an SPSW using a ListView
whose sections are built lazily (on-demand) and have equal height.
- Part 1: Introduction
- Part 2: Scroll to Position
- Part 3: Scroll to Page
- Part 4: Ensure Visible
- Part 5: Scroll to Index
- Part 6: Navigation
- Part 7: Query Params

HomeScreen
HomeScreen
has a Column
with two main components: a sticky top navigation menu and an expanded scrollable list. We provide colors
, colorCodeNotifier
, and shapeBorderTypeNotifier
to HomeScreen
as constructor parameters.
Note that although shapeBorderTypeNotifier
and colorCodeNotifier
are known to be instances of ValueNotifier
, we pass them as ValueListenable
which is the parent class of ValueNotifier
. It is a good practice because while the ValueNotifier
instances can notify their listeners, ValueListenable
instances cannot. Inside HomeScreen
widget, we won’t notify the listeners, but its children widgets will do. Hence, it is safer to include their type as ValueListenable
in HomeScreen
.
TopNavigationMenu
TopNavigationMenu
widget wraps all the NavigationMenuButton
widgets in a horizontal run. The number of runs varies depending on the screen width.
This widget both listens and notifies the colorCodeNotifier
:
- It listens to the color code values coming from different sources and fills the background of the button if the current color code is the same as the button text.

- It notifies the color code listeners on the user button press. When the user clicks a color button, the button’s
Color
is converted to aString
hex color code and the listeners ofcolorCodeNotifier
are notified.

Let’s first analyze the NavigationMenuButton
. It is a StatelessWidget
that includes either a TextButton
or an ElevatedButton
depending on the selected
parameter.
- If the
selected
isfalse
, then it is aTextButton
whose text color is the same as thecolor
parameter. - If the
selected
istrue
, then it is anElevatedButton
whose background color is the same as thecolor
parameter. We set the text color black or white according to the estimated brightness of the color.
TopNavigationMenu
is a StatefulWidget
. It listens colorCodeNotifier
and whenever its value is updated, setState
method is called so that NavigationMenuButton
widgets are rebuilt according to new selected
state. The button whose text value equals to the hex color code value in the colorCodeNotifier
will be the selected
one.
On second thought, the TopNavigationMenu
widget can be a stateless widget thanks to Flutter's ValueListenableBuilder
widget which helps the content to stay synced with a ValueListenable
. In our case, we will wrap the content with a ValueListenableBuilder
for colorCodeNotifier
. Whenever the value that the colorCodeNotifier
holds is updated, the widget provided to the builder
field is rebuilt.
ColorSections
ColorSections
is a scrollable widget that lists all the color sections. This widget both listens for the color code changes and notifies the color code listeners:
- It listens to the color code values coming from different sources and scrolls to the corresponding color section if the source of the value update is not scrolling.
- It notifies the color code listeners when the first visible section (
trailingIndex
) changes and also when a shaped button is pressed.
In the recording below, when a shaped button is clicked, all the ColorCode
listeners will be notified. The listener in the ColorSections
widget will trigger a scroll to the section that has the shaped button.

We build the ListView
lazily using ListView.builder
constructor. Instead of building all the list items (color sections) at once, the items will be built when they are visible on the screen. Obviously, this helps from the performance point but comes with a cost as mentioned in the introduction part of this series.
Since all the items are not laid off at once (built lazily), we cannot know the offset of all items unless we have their size information before they are built. As a result, when a color code is selected from the TopNavigationMenu
we cannot jump to the corresponding color section if the sections have unpredictable heights.
In this sample, we assume that all the list items have the same height according to this formula: If the available height (visible listview area, viewport height) is smaller than _minItemHeight
(700 px), it is equal to _minItemHeight
, otherwise the available height.
Listen to Color Code Update
In ColorSections
, we listen to colorCodeNotifier
to scroll to a color section. Note that the colorCodeNotier
value can be set from different sources. We will not programmatically scroll to a section if the color is updated due to scroll event.
When the value that colorCodeNotifier
holds changes and the source of the change is not scrolling, we do the following:
- Find the index of the color code from the
colors
list - Calculate the scroll offset by multiplying the index with the item height. We can get the available height information from the
size
property of theColorSections
'context
on a button click because theColorSections
widget is already laid out. Hence, the framework can determine its size. - Call
ScrollController.animateTo
method to programmatically scroll to the section.
Notify a Color Code Update
While the user is scrolling, we want to update the colorCodeNotifier
listeners as the first visible color code section (trailingIndex
) changes. Note that when we are setting the colorCodeNotifier
notifier value, we set its source field as fromScroll
.

The Scrollable
widgets notify their ancestors about the scroll events with ScrollNotificaton
. A ScrollNotificaton
will be traversed up in the tree until a NotificationListener
handles it. TheNotificationListener
widget doesn’t have to be the parent of the source Scrollable
widget, but it has to be an ancestor widget that specifies the Notification
type as ScrollNotificaton
.
In our case, we wrap the ListView
widget with a NotificationListener
widget whose onNotification
callback property will be invoked by two scroll event sources:
- Programmatic scroll caused by a color button press in the
TopNavigationMenu
- User-generated scroll with a touch, trackpad, or mouse event.
We need to distinguish the scroll event source and notify the colorCodeNotier
listeners only for the user-generated ones. Hence, we check the runtimeType
using the is
operator and handle the notification if only its Type
is UserScrollNotification
.
When we receive the UserScrollNotification
, we get the offset (pixels
) from the notification and calculate the first visible index (trailing index) by dividing the offset by the list item height. We can get the available height information from the context
because the ColorSections
widget is already laid out during user scroll.
pixels is the number of pixels to offset the children in the opposite of the axis direction. For example, if the axis direction is down, then the pixel value represents the number of logical pixels to move the children up the screen …
By returning true
in the onNotification
method, we stop the bubbling of the UserScrollNotification
in the widget tree.
As mentioned before, we can’t get the item height from the BuildContext
during a build. If we do so, we will get the following error:
The size of this render object has not yet been determined because the framework is still in the process of building widgets, which means the render tree for this frame has not yet been determined. The size getter should only be called from paint callbacks or interaction event handlers (e.g. gesture callbacks).
If you need some sizing information during build to decide which widgets to build, consider using a LayoutBuilder widget, which can tell you the layout constraints at a given location in the tree.
Hence, we determine the section height and calculate the initial offset for the ScrollController
with the help of LayoutBuilder
widget.
Sections
In the ShapedBorderListView
we list 5 buttons with the following ShapeBorder
classes: BeveledRectangleBorder
, RoundedReactangleBorder
, ContinousRectangleBorder
, StadiumBorder
, and the CircleBorder
.
We set the physics
property to NeverScrollableScrollPhyscis
so that the ShapedBorderListView
is not allowed to be scrolled. Instead, the user scroll events will be intercepted by the parent ColorSections
.
When the user clicks a button in the list, we set the values in colorCodeNotifier
and shapeBorderTypeNotifier
accordingly and inform their listeners.
You may wonder why there is no code related to showing the dialog on click gesture. It is because showing a dialog page is handled by the Router
widget declaratively. We will discuss it more in the navigation part of this series.
Resizing the Browser Window
When I was working on this series, one of the most challenging problems I faced was the inconsistent behavior of the ScrollController
on browser window resize. This is the reason why this series has waited for a long time in draft mode.
For the sake of performance, I avoided rebuilding and recreating the ColorSections
widget and instantiating the ScrollController
in the build method. However, when the browser window size changed, the scroll controller couldn’t jump to the correct offset unless the current offset is set to 0 again.

I realized that there are two open PRs in Flutter repository (#85221 and #82687) to address this issue. ScrollMetricsNotification
is introduced and integrated in Scrollbar
. Hopefully, this is the right direction that also addresses this problem.
[ScrollMetricsNotification] A notification that a scrollable widget's [ScrollMetrics] has changed.
For example, when the content of a scrollable is altered, making it larger or smaller, this notification will be dispatched. Similarly, if the size of the window or parent changes, the scrollable can notify of these changes in dimensions.
The above behaviors usually do not trigger [ScrollNotification] events, so this is useful for listening to [ScrollMetrics] changes that are not caused by the user scrolling.
I found out two ways of solving this problem before this feature is landed:
- The first one comes with a performance cost. Each time the window size is changed, I re-created the
_ColorSectionsState
state and theScrollController
. Then there was another problem: When the browser window height changes, the widget doesn’t call thebuild
method of theColorSections
because Flutter does this intentionally for performance gain. In order to force the state re-creation, I added aValueKey
to the_ColorSections
widget so that each time the window height changes, the framework creates a new_ColorSectionsState
. - The second solution is using a
PageView
. This widget has aPageController
which extends theScrollController
. Apparently, thePageController
adapts the viewport size changes better and doesn’t have any problem with resizing the browser window.
Conclusion
In this article, we explored lazily building the sections of a single page scrollable website using a ListView
widget. In the third part of this series, we will build a similar UI using a PageView
widget.
You can run the sample app by right clicking on the main_003.01.dart
file in the project source code.

If you liked this article, please press the clap button, and star the Github repository of the sample apps.
