Scrollbar in Tk (tkinter)

The scrollbar is a Tk widget that allows you to modify the visible area (called viewport) of other widgets. A scrollbar can be vertical or horizontal, and is typically attached to widgets that display multiple elements, lines, or columns, such as listboxes (tk.Listbox), tree views (ttk.Treeview) or multi-line textboxes (tk.Text). In this post you will learn how to create and configure scrollbars.

/images/scrollbar-in-tk-tkinter/listbox-with-scrollbar.gif

To introduce the necessary concepts to understand scrollbars, we will work with one of the widgets that lets us display multiple items: the listbox. The same concepts apply to the other widgets supporting scrolling. Here is a simple program that creates a window and a listbox with 100 elements:

import tkinter as tk
root = tk.Tk()
root.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Element {i}" for i in range(100)))
listbox.pack()
root.mainloop()

After running the program, you will see that the listbox only manages to display a handful of elements, although Tk doesn't add a scrollbar automatically. However, you can scroll the listbox vertically using the middle mouse button by placing the cursor on top of the widget:

/images/scrollbar-in-tk-tkinter/listbox.gif

This means that the procedure to move the visible area of a widget is already built in Tk. We just need to create the scrollbar and connect it with the proper widget. The visible area of a widget, called the viewport, is the part of the widget that is displayed to the user. The visible area of a widget can be equal to the total area if the size of the widget is large enough to display its whole content. In the previous image, we see that the listbox has a total of 100 elements, but the visible area only shows 10 items at a time, so the visible area represents one tenth (1/10) of the total area. However, the visible area will match the total area when the list has 10 items or less, or if we increase the size of the listbox.

Internally, Tk records the visible area of each of widget, for which it uses two ranges: one for the vertical area, the other for the horizontal area. For simplicity, let's now deal only with the vertical area. Ranges managing the visible area of a widget extend, at most, from 0.0 to 1.0. When the range is 0.0-1.0, the visible area matches the total area of the widget, i.e., no element is left out of the visible area. The first number of the vertical range indicates the upper bound of the widget's total area, while the second number specifies the lower bound. Thus, as soon as we execute the above code, the listbox's visible area is 0.0-0.1:

/images/scrollbar-in-tk-tkinter/scrollbar-vertical-range.png

The range being 0.0-0.1 means that only the first 10% of the total area of the widget is visible. Because of the size of this specific listbox, the visible area is always 10% of the total area. However, the range changes as we scroll down: both limits increase until they reach 0.9-1.0, in which range the last 10 items of the listbox are displayed. Nevertheless, the ratio doesn't change as long as the size of the widget remains always the same: the difference between the second number and the first is always 0.1 (10%). With the following code we can see how the range changes as we scroll through the listbox:

import tkinter as tk
from tkinter import ttk
def display_range(a, b):
      label["text"] = f"Range: {a}-{b}"
root = tk.Tk()
root.geometry("400x300")
listbox = tk.Listbox(yscrollcommand=display_range)
listbox.insert(tk.END, *(f"Element {i}" for i in range(100)))
listbox.pack()
label = ttk.Label()
label.pack()
root.mainloop()
/images/scrollbar-in-tk-tkinter/scrollbar-display-vertical-range.gif

The tk.Listbox, tk.Text, tk.Canvas, and ttk.Treeview widgets include the yscrollcommand argument, which lets us pass a function that will be invoked every time the visible area of the widget changes. Thus, our display_range() function receives the two bounds of the range as arguments (a and b) each time the user scrolls through the listbox.

Conversely, the yview() can be used to change the visible area of a widget. For example:

import tkinter as tk
root = tk.Tk()
root.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Element {i}" for i in range(100)))
# Move the viewport to 0.82.
listbox.yview("moveto", 0.82)
listbox.pack()
root.mainloop()

The "moveto" first argument means that we want to move the visible area of the widget, while the second argument is a number between 0.0 and 1.0 indicating the first number in the range of the visible area. The second number in the range is automatically calculated based on the size of the widget. Based on the current size of our widget, the range of the visible area will be 0.82 - 0.92. To see it better, we might add a textbox (ttk.Entry) to let us set the listbox's viewport when pressing Enter:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Element {i}" for i in range(100)))
listbox.pack()
entry = ttk.Entry()
entry.bind("<Return>", lambda ev: listbox.yview("moveto", entry.get()))
entry.pack()
root.mainloop()
/images/scrollbar-in-tk-tkinter/listbox-editable-range.gif

Note that the first number in the range matches the element number: if the range starts at 0.0, the first element displayed is element 0; if the range starts at 0.25, element 25 is the first to be displayed; and so on. But this (intentional) coincidence happens only because the listbox has exactly 100 elements. Had it 50 elements, a range starting at 0.8 would display element 40 as the first element in the viewport (because 80% of 50 is 40, 50*0.8 == 40), while a range starting at 0.40 would display element 20, and so on.

Furthermore, the yview() method allows you to scroll the visible area of a widget by "units" when the first argument is "scroll. The exact definition of what a unit is depends on the widget being scrolled. In listboxes, a unit is equivalent to an element. Positive unit numbers move the visible area down, while negative numbers scroll it up. For example, the following code uses two buttons to let the user scroll the listbox's viewport up and down:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Element {i}" for i in range(100)))
listbox.pack()
button_up = ttk.Button(
     text="Up",
     command=lambda: listbox.yview("scroll", "-1", "units")
)
button_up.pack()
button_down = ttk.Button(
     text="Down",
     command=lambda: listbox.yview("scroll", "1", "units")
)
button_down.pack()
root.mainloop()
/images/scrollbar-in-tk-tkinter/listbox-with-scroll-buttons.gif

Horizontal scrolling works exactly the same by using the xview() method and xscrollcommand parameter.

Scrollbar Widget

Now that you know the fundamental concepts related to scrolling the visible area of a widget, let's take a look at the scrollbar widget itself. Tk provides two widgets with the same functionality: tk.Scrollbar and ttk.Scrollbar. They just differ in the way their instances can be styled. However, what we have seen so far and what we will explain below are valid for both classes. The scrollbar is the preferred widget for controlling the visible area of a listbox, table, text box, or any other widget that supports scrolling. It can be vertically or horizontally oriented, and is generally made up of two fixed buttons placed in the edges and one button that can be dragged across a channel or trough. The draggable button is known as thumb. Under some operating systems, scrollbars have no fixed buttons, but just a thumb. On Windows 11, for example, the fixed buttons are only visible while hovering the scrollbar.

/images/scrollbar-in-tk-tkinter/scrollbar-vertical-horizontal.png

The scrollbar works, just like the widgets in the previous section, with a range between 0.0 and 1.0 indicating what the position and size of the thumb are. When the range is 0.0-1.0, the thumb spans along the entire trough. As the first number in the range increases, the thumb moves down (in vertical scrollbars) or to the right (horizontal). The difference between the second and the first number of the range indicates how big the thumb is. So, if we set the range of a scrollbar to 0.2-0.5, the size of the thumb will be 30% (since 0.5-0.2 == 0.3) of the size of the trough (i.e., 30% of the size of the scrollbar, without the fixed buttons):

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry("400x300")
scrollbar = ttk.Scrollbar(orient=tk.VERTICAL)
scrollbar.set(0.2, 0.5)
scrollbar.place(x=50, y=50, height=200)
root.mainloop()

The orient argument specifies the orientation of the scrollbar: tk.VERTICAL or tk.HORIZONTAL. The set() method sets the range of the scrollbar. This is the result:

/images/scrollbar-in-tk-tkinter/vertical-scrollbar.png

It's easy to see that the fixed buttons do the job of our former button_up and button_down widgets in the previous section. We can even associate the press event of those buttons via the command parameter and we will see that they generate the same three arguments that we had passed to listbox.yview() to scroll the listbox's viewport by units:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry("400x300")
scrollbar = ttk.Scrollbar(orient=tk.VERTICAL, command=print)
scrollbar.set(0.2, 0.5)
scrollbar.place(x=50, y=50, height=200)
root.mainloop()
/images/scrollbar-in-tk-tkinter/scrollbar-command.gif

This is no coincidence: the event raised when pressing the scrollbar's fixed buttons is precisely intended to be connected to the yview() or xview() methods of the widget whose visible area you want to alter. Thus, with the following code we have a partially-working bar to scroll elements in a listbox:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Element {i}" for i in range(100)))
listbox.place(x=10, y=10, width=200, height=180)
scrollbar = ttk.Scrollbar(orient=tk.VERTICAL, command=listbox.yview)
scrollbar.place(x=220, y=10, height=180)
root.mainloop()
/images/scrollbar-in-tk-tkinter/listbox-scrollbar.gif

However, you might have noticed that the thumb does not move and has always the same size. This happens because we have not called scrollbar.set(), so the scrollbar does not know what the range of the visible area of the listbox is. Fortunately, we saw that the tk.Listbox class constructor supports the yscrollcommand parameter, which receives a function that will be called whenever the visible area of the widget is altered. So, if we connect that parameter with the scrollbar.set() method, we'll get the scrollbar's thumb to update its size and position every time the listbox's viewport changes:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry("400x300")
listbox = tk.Listbox()
listbox.insert(tk.END, *(f"Element {i}" for i in range(100)))
listbox.place(x=10, y=10, width=200, height=180)
scrollbar = ttk.Scrollbar(orient=tk.VERTICAL, command=listbox.yview)
listbox.config(yscrollcommand=scrollbar.set)
scrollbar.place(x=220, y=10, height=180)
root.mainloop()
/images/scrollbar-in-tk-tkinter/listbox-scrollbar-2.gif

That's great! We not only get the thumb to update every time the visible area of the listbox changes, but also the visible area to change when dragging the thumb of the scrollbar. This is possible because Tk also calls the function passed to the command parameter when the user drags the thumb around the channel, but in this case passing "moveto" as the first argument and as the second argument a number between 0.0 and 1.0 indicating the start of the range of the visible area. This was the first way we saw that the yview() method could scroll the visible area of a widget.

Better organized with classes

Putting all the pieces together, we can implement a handful of classes that allow us to create listboxes, tree views, and text boxes with horizontal and vertical scrollbars and insert them into a window without disturbing remaining widgets.

Listbox with horizontal and vertical scrollbar:

import tkinter as tk
from tkinter import ttk
class ListboxFrame(ttk.Frame):
     def __init__(self, *args, **kwargs):
          super().__init__(*args, **kwargs)
          self.hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
          self.vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
          self.listbox = tk.Listbox(
               self,
               xscrollcommand=self.hscrollbar.set,
               yscrollcommand=self.vscrollbar.set
          )
          self.hscrollbar.config(command=self.listbox.xview)
          self.hscrollbar.pack(side=tk.BOTTOM, fill=tk.X)
          self.vscrollbar.config(command=self.listbox.yview)
          self.vscrollbar.pack(side=tk.RIGHT, fill=tk.Y)
          self.listbox.pack()
root = tk.Tk()
root.title("Listbox With Scrollbars")
root.geometry("400x300")
listbox_frame = ListboxFrame()
listbox_frame.listbox.insert(
     tk.END,
     *(f"Element {i} with a large text" for i in range(100))
)
listbox_frame.pack()
root.mainloop()
/images/scrollbar-in-tk-tkinter/listbox-with-scrollbars.png

Table or tree view:

import pathlib
import sys
import tkinter as tk
from tkinter import ttk
class TreeviewFrame(ttk.Frame):
     def __init__(self, *args, **kwargs):
          super().__init__(*args, **kwargs)
          self.hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
          self.vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
          self.treeview = ttk.Treeview(
               self,
               xscrollcommand=self.hscrollbar.set,
               yscrollcommand=self.vscrollbar.set
          )
          self.hscrollbar.config(command=self.treeview.xview)
          self.hscrollbar.pack(side=tk.BOTTOM, fill=tk.X)
          self.vscrollbar.config(command=self.treeview.yview)
          self.vscrollbar.pack(side=tk.RIGHT, fill=tk.Y)
          self.treeview.pack()
root = tk.Tk()
root.title("Table With Scrollbars")
root.geometry("400x300")
treeview_frame = TreeviewFrame()
treeview_frame.pack()
treeview_frame.treeview.config(columns=("name", "size"), show="headings")
treeview_frame.treeview.heading("name", text="File name")
treeview_frame.treeview.heading("size", text="Size")
for file in pathlib.Path(sys.executable).parent.iterdir():
     treeview_frame.treeview.insert(
          "", tk.END, values=(file.name, file.stat().st_size))
root.mainloop()
/images/scrollbar-in-tk-tkinter/treeview-with-scrollbars.png

Multi-line textbox (tk.Text):

import pathlib
import sys
import tkinter as tk
from tkinter import ttk
class TextFrame(ttk.Frame):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
        self.vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
        self.text = tk.Text(
            self,
            xscrollcommand=self.hscrollbar.set,
            yscrollcommand=self.vscrollbar.set,
            wrap=tk.NONE
        )
        self.hscrollbar.config(command=self.text.xview)
        self.hscrollbar.pack(side=tk.BOTTOM, fill=tk.X)
        self.vscrollbar.config(command=self.text.yview)
        self.vscrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.text.pack()
root = tk.Tk()
root.title("Text Widget With Scrollbars")
root.geometry("400x300")
text_frame = TextFrame()
text_frame.text.insert(
    "1.0",
    # If this doesn't work, put the path to a large text file.
    (pathlib.Path(sys.executable).parent / "news.txt").read_text("utf8")
)
text_frame.pack()
root.mainloop()
/images/scrollbar-in-tk-tkinter/text-widget-with-scrollbars.png

Styles

Styling a scrollbar is possible under some operating systems and themes.

To customize the appearance of ttk.Scrollbar instances, we need to configure the TScrollbar style. We might also discriminate between vertical or horizontal scrollbars by using the specific Vertical.TScrollbar and Horizontal.TScrollbar style names. For example:

import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.geometry("400x300")
root.title("Tk Scrollbar")
style = ttk.Style()
style.configure(
    "TScrollbar",
    # Color of the fixed buttons' arrow.
    arrowcolor="#0000ff",
    # Size of the fixed buttons' arrow.
    arrowsize=10,
    # Background color of the fixed buttons and the thumb.
    background="#00ff00",
    # Border color of the fixed buttons and the thumb.
    bordercolor="#ffffff",
    # Color of the dark part of the 3D relief.
    darkcolor="#ff0000",
    # Color of the light part of the 3D relief.
    lightcolor="#ff0000",
    # Front color.
    foreground="#ffff00",
    # Number of lines in the thumb.
    gripcount=5,
    # Color of the trough which the thumb is scrolled through.
    troughcolor="#ff00ff"
)
vlabel = ttk.Label(text="Vertical")
vlabel.place(x=50, y=20)
vscrollbar = ttk.Scrollbar(orient=tk.VERTICAL)
vscrollbar.place(x=50, y=50, height=200)
vscrollbar.set("0.0", "0.1")
hlabel = ttk.Label(text="Horizontal")
hlabel.place(x=150, y=70)
hscrollbar = ttk.Scrollbar(orient=tk.HORIZONTAL)
hscrollbar.place(x=150, y=100, width=200)
hscrollbar.set("0.5", "0.6")
root.mainloop()

Note that:

  • Some configuration options might not be available in certain themes.

  • The theme that Tk uses by default in Windows does not support styling as it works with native widgets.

The tk.Scrollbar classic widget supports the following arguments for styling:

scrollbar = tk.Scrollbar(
      # Color of the thumb and fixed buttons when they
      # are active (they have the mouse over or are being clicked).
      activebackground="#ffff00",
      # Background color.
      background="#ff0000",
      # Border thickness.
      borderwidth=5,
      # Border color.
      highlightbackground="#00ff00",
      # Border color when the widget has focus.
      highlightcolor="#0000ff",
      # Border color when the widget has focus.
      highlightthickness=5,
      # Color of the trough which the thumb is scrolled through.
      troughcolor="#ff00ff"
)

Neither of these options is recognized in Windows.

Comments