Learn how to create scrollable containers for your widgets without the complexity of manual Canvas and Scrollbar linking.
One of the biggest challenges in standard Tkinter is creating a scrollable area. Normally, you would have to embed a Frame inside a Canvas and write custom logic to update the scroll region.
The ttkbootstrap.scrolled module provides the ScrolledFrame, a high-level widget that handles all of this automatically. It is perfect for settings pages, long forms, or dashboards where the content exceeds the window size.
import ttkbootstrap as ttk
from ttkbootstrap.scrolled import ScrolledFrame
root = ttk.Window(themename="cosmo")
root.title("plus2net ScrolledFrame Tutorial")
root.geometry("400x300")
# 1. Create the ScrolledFrame
sf = ScrolledFrame(root, autohide=True)
sf.pack(fill="both", expand=True, padx=10, pady=10)
# 2. Add widgets to the ScrolledFrame (not the root!)
for i in range(1, 21):
btn = ttk.Button(sf, text=f"Item {i}")
btn.pack(fill="x", pady=5)
root.mainloop()
The ScrolledFrame widget is designed to solve the common layout limitations of standard Tkinter.
Here are the professional features that make it essential for modern GUIs:
autohide=True parameter, scrollbars only appear when the
content exceeds the visible area, keeping your interface clean.
ScrolledFrame comes with pre-configured
event bindings for mousewheel scrolling on Windows, macOS, and Linux.
ScrolledFrame object instead of managing a complex Canvas/Frame stack.
fill and expand
parameters to True when packing the ScrolledFrame to ensure it responds to window resizing.
In real-world applications, you often need to add items to a list dynamically—such as adding tasks to a To-Do list or rows to a data entry form. The ScrolledFrame is perfect for this because it re-calculates the scrollable area every time a new widget is added.
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.scrolled import ScrolledFrame
def delete_row(frame_to_remove):
# 1. Destroy the widget
frame_to_remove.destroy()
# 2. Force the ScrolledFrame to refresh its scrollable area
sf.update_idletasks()
def add_row():
row_frame = ttk.Frame(sf)
row_frame.pack(fill="x", pady=2)
lbl = ttk.Label(row_frame, text=f"User Record #{var_count.get()}")
lbl.pack(side=LEFT, padx=10)
# Pass the frame itself into the delete function
btn_del = ttk.Button(
row_frame,
text="Delete",
bootstyle="danger-outline",
command=lambda f=row_frame: delete_row(f)
)
btn_del.pack(side=RIGHT, padx=20)
var_count.set(var_count.get() + 1)
root = ttk.Window(themename="flatly")
root.title("plus2net Dynamic ScrolledFrame")
root.geometry("500x400")
var_count = ttk.IntVar(value=1)
# Control Frame at the top
top_bar = ttk.Frame(root, padding=10)
top_bar.pack(fill="x")
ttk.Button(top_bar, text="Add New User", command=add_row, bootstyle="success").pack()
# ScrolledFrame for the list content
sf = ScrolledFrame(root, autohide=True)
sf.pack(fill="both", expand=True, padx=10, pady=10)
root.mainloop()
row_frame.destroy() works seamlessly. When a widget is removed, the ScrolledFrame immediately updates the scrollbar position to reflect the new total height.
When building interactive applications, you often need to remove items from a list. While standard Tkinter allows you to destroy a widget, the ScrolledFrame needs to be explicitly notified that its internal "height" has changed so it can adjust the scrollbar accordingly.
The following function ensures that when a row is deleted, the scrollbar updates its length and position instantly, without waiting for the user to move the mouse.
destroy(): Removes the row frame and all widgets inside it (labels, buttons, etc.) from the memory and the screen.
update_idletasks(): Tells Python to pause for a microsecond and finish the "task" of deleting the widget before moving to the next line of code.
sf.container.event_generate("<Configure>"): This is the "secret sauce." It manually triggers the internal resize logic of the ScrolledFrame, forcing the scrollbar to recalculate its size based on the new, shorter list of items.
def delete_row(frame_to_remove):
# 1. Destroy the widget
frame_to_remove.destroy()
# 2. Force the window to process the destruction
sf.update_idletasks()
# 3. Trigger a configuration event on the internal container
# This is the "Pro" way to force a scrollbar refresh
sf.container.event_generate("<Configure>")
Full code is here
import ttkbootstrap as ttk
from ttkbootstrap.constants import *
from ttkbootstrap.scrolled import ScrolledFrame
def delete_row(frame_to_remove):
# 1. Destroy the widget
frame_to_remove.destroy()
# 2. Force the window to process the destruction
sf.update_idletasks()
# 3. Trigger a configuration event to refresh scrollbar
sf.container.event_generate("<Configure>")
def add_row():
row_id = var_count.get()
row_frame = ttk.Frame(sf)
row_frame.pack(fill="x", pady=5)
lbl = ttk.Label(row_frame, text=f"User Record #{row_id}")
lbl.pack(side=LEFT, padx=10)
btn_del = ttk.Button(
row_frame, text="Delete",
bootstyle="danger-outline",
command=lambda f=row_frame: delete_row(f)
)
btn_del.pack(side=RIGHT, padx=20)
var_count.set(row_id + 1)
root = ttk.Window(themename="flatly")
root.title("plus2net Dynamic ScrolledFrame")
root.geometry("500x400")
var_count = ttk.IntVar(value=1)
top_bar = ttk.Frame(root, padding=10)
top_bar.pack(fill="x")
ttk.Button(top_bar, text="Add New Record", command=add_row).pack()
sf = ScrolledFrame(root, autohide=True)
sf.pack(fill="both", expand=True, padx=10, pady=10)
root.mainloop()
When building dynamic lists, you often need a "Reset" or "Clear All" button. Instead of deleting each row individually, we can iterate through all widgets inside the ScrolledFrame and destroy them in a single loop.
After clearing the widgets, it is essential to reset your counters and refresh the scrollbar logic to ensure the interface returns to its original state.
def clear_all():
# 1. Iterate through all child widgets of the ScrolledFrame
for widget in sf.winfo_children():
widget.destroy()
# 2. Reset the record counter
var_count.set(1)
# 3. Force the scrollbar to recalculate (shrinking it to zero)
sf.update_idletasks()
sf.container.event_generate("<Configure>")
winfo_children() is safer and faster than maintaining a manual list of widgets. It ensures that even if you added complex sub-frames, every single element is removed from the screen and memory.
You can add this button to your top control bar alongside the "Add" button:
ttk.Button(
top_bar,
text="Clear All Records",
command=clear_all,
bootstyle="danger"
).pack(side=RIGHT, padx=5)
| Feature | Standard Tkinter | ttkbootstrap ScrolledFrame |
|---|---|---|
| Setup Complexity | High (Canvas + Frame + Scrollbar) | Low (Single Widget) |
| Mousewheel Support | Manual (Requires OS-specific code) | Built-in (Native behavior) |
| Auto-Hide | Requires custom logic | Enabled via autohide=True |
| Resizing Content | Manual scrollregion updates |
Automatic via Event Generation |
Author
🎥 Join me live on YouTubePassionate about coding and teaching, I publish practical tutorials on PHP, Python, JavaScript, SQL, and web development. My goal is to make learning simple, engaging, and project‑oriented with real examples and source code.