Tkinter Designer

← tkinter how-to

How do I make a scrollable frame in tkinter?

A Frame has no view to scroll, so it cannot be given a scrollbar directly. What works is putting the frame inside a Canvas, which can scroll, and keeping the canvas informed of how big the frame has become.

import tkinter as tk
from tkinter import ttk

root = tk.Tk()

canvas = tk.Canvas(root)
bar = ttk.Scrollbar(root, orient="vertical", command=canvas.yview)
inner = tk.Frame(canvas)                       # your widgets go in here

canvas.configure(yscrollcommand=bar.set)
canvas.pack(side="left", fill="both", expand=True)
bar.pack(side="right", fill="y")
canvas.create_window((0, 0), window=inner, anchor="nw")

# without this the canvas never learns how tall the frame is
inner.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))

for i in range(40):
    tk.Label(inner, text=f"row {i}").pack()

root.mainloop()

Why nothing scrolls without the Configure binding

A canvas only scrolls within its scrollregion, and that starts out empty. Build the whole thing without the binding and you get a canvas that is convinced there is nothing to see: the scrollbar sits full-length and the content below the fold is simply unreachable.

The <Configure> binding fires whenever the inner frame changes size, and canvas.bbox("all") measures everything on the canvas — so the region grows as you add rows, without you tracking heights yourself.

The frame goes on the canvas, not in it

create_window is what puts the frame onto the canvas, and it is not the same as packing the frame into it. Packing a frame into a canvas appears to work and then refuses to scroll, because a packed child is managed by the geometry manager rather than being a canvas item.

Use anchor="nw" so the frame starts at the top left; the default centres it, which looks like a mysterious gap above your first row.

Mouse wheel support is separate

The wheel is not wired up for you. Bind it to scroll the canvas, remembering that the event differs by platform — Windows and macOS send <MouseWheel> with a delta, X11 sends <Button-4> and <Button-5>.

canvas.bind_all("<MouseWheel>", lambda e: canvas.yview_scroll(int(-e.delta / 120), "units"))

The modern toolkits do this for you

CustomTkinter has CTkScrollableFrame and ttkbootstrap has ScrolledFrame, and both are a single widget you drop widgets into — no canvas, no binding, no scroll region. If you are already using either, there is no reason to hand-build the recipe above.

import customtkinter as ctk
panel = ctk.CTkScrollableFrame(root, label_text="Rows")

import ttkbootstrap as ttkb
panel = ttkb.ScrolledFrame(root, auto_hide=True)

Doing it without the lookup

Drop a Scrollable frame on and put widgets inside it. On CustomTkinter and ttkbootstrap the export uses their native scrolling widget, and on plain tkinter it writes the canvas recipe above with the binding already in place — so the same design runs on all three.

Lay the window out visually and read the generated Python as you go.

Open the tkinter designer →

Related questions