Skip to main content
This guide covers advanced patterns for building complex CustomTkinter applications, including custom scrollable frames, image handling, multi-frame navigation, and theme customization.

Custom Scrollable Frame Components

Create reusable scrollable components by extending CTkScrollableFrame.

Scrollable CheckBox Frame

Build a custom scrollable frame containing multiple checkboxes with add/remove functionality.
import customtkinter

class ScrollableCheckBoxFrame(customtkinter.CTkScrollableFrame):
    def __init__(self, master, item_list, command=None, **kwargs):
        super().__init__(master, **kwargs)
        
        self.command = command
        self.checkbox_list = []
        for i, item in enumerate(item_list):
            self.add_item(item)
    
    def add_item(self, item):
        checkbox = customtkinter.CTkCheckBox(self, text=item)
        if self.command is not None:
            checkbox.configure(command=self.command)
        checkbox.grid(row=len(self.checkbox_list), column=0, pady=(0, 10))
        self.checkbox_list.append(checkbox)
    
    def remove_item(self, item):
        for checkbox in self.checkbox_list:
            if item == checkbox.cget("text"):
                checkbox.destroy()
                self.checkbox_list.remove(checkbox)
                return
    
    def get_checked_items(self):
        return [checkbox.cget("text") for checkbox in self.checkbox_list if checkbox.get() == 1]
Usage:
self.scrollable_checkbox_frame = ScrollableCheckBoxFrame(
    master=self, 
    width=200, 
    command=self.checkbox_frame_event,
    item_list=[f"item {i}" for i in range(50)]
)
self.scrollable_checkbox_frame.grid(row=0, column=0, padx=15, pady=15, sticky="ns")
self.scrollable_checkbox_frame.add_item("new item")

def checkbox_frame_event(self):
    print(f"Selected: {self.scrollable_checkbox_frame.get_checked_items()}")

Scrollable RadioButton Frame

Create a scrollable frame with radio buttons for selecting a single option from many.
class ScrollableRadiobuttonFrame(customtkinter.CTkScrollableFrame):
    def __init__(self, master, item_list, command=None, **kwargs):
        super().__init__(master, **kwargs)
        
        self.command = command
        self.radiobutton_variable = customtkinter.StringVar()
        self.radiobutton_list = []
        for i, item in enumerate(item_list):
            self.add_item(item)
    
    def add_item(self, item):
        radiobutton = customtkinter.CTkRadioButton(
            self, 
            text=item, 
            value=item, 
            variable=self.radiobutton_variable
        )
        if self.command is not None:
            radiobutton.configure(command=self.command)
        radiobutton.grid(row=len(self.radiobutton_list), column=0, pady=(0, 10))
        self.radiobutton_list.append(radiobutton)
    
    def remove_item(self, item):
        for radiobutton in self.radiobutton_list:
            if item == radiobutton.cget("text"):
                radiobutton.destroy()
                self.radiobutton_list.remove(radiobutton)
                return
    
    def get_checked_item(self):
        return self.radiobutton_variable.get()

Scrollable Label-Button Frame

Combine labels and buttons in a scrollable list.
class ScrollableLabelButtonFrame(customtkinter.CTkScrollableFrame):
    def __init__(self, master, command=None, **kwargs):
        super().__init__(master, **kwargs)
        self.grid_columnconfigure(0, weight=1)
        
        self.command = command
        self.radiobutton_variable = customtkinter.StringVar()
        self.label_list = []
        self.button_list = []
    
    def add_item(self, item, image=None):
        label = customtkinter.CTkLabel(
            self, 
            text=item, 
            image=image, 
            compound="left", 
            padx=5, 
            anchor="w"
        )
        button = customtkinter.CTkButton(self, text="Command", width=100, height=24)
        if self.command is not None:
            button.configure(command=lambda: self.command(item))
        label.grid(row=len(self.label_list), column=0, pady=(0, 10), sticky="w")
        button.grid(row=len(self.button_list), column=1, pady=(0, 10), padx=5)
        self.label_list.append(label)
        self.button_list.append(button)
    
    def remove_item(self, item):
        for label, button in zip(self.label_list, self.button_list):
            if item == label.cget("text"):
                label.destroy()
                button.destroy()
                self.label_list.remove(label)
                self.button_list.remove(button)
                return
Custom scrollable frames automatically handle scrolling when content exceeds the frame height.

Advanced Image Handling

Use CTkImage for advanced image features like appearance-aware images and dynamic scaling.

Loading Images with CTkImage

import customtkinter
import os
from PIL import Image

class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()
        
        image_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_images")
        
        # Simple image with size
        self.logo_image = customtkinter.CTkImage(
            Image.open(os.path.join(image_path, "logo.png")), 
            size=(26, 26)
        )
        
        # Large display image
        self.large_test_image = customtkinter.CTkImage(
            Image.open(os.path.join(image_path, "large_test_image.png")), 
            size=(500, 150)
        )

Appearance Mode-Aware Images

Provide different images for light and dark modes.
# Image that changes based on appearance mode
self.home_image = customtkinter.CTkImage(
    light_image=Image.open(os.path.join(image_path, "home_dark.png")),
    dark_image=Image.open(os.path.join(image_path, "home_light.png")), 
    size=(20, 20)
)

self.chat_image = customtkinter.CTkImage(
    light_image=Image.open(os.path.join(image_path, "chat_dark.png")),
    dark_image=Image.open(os.path.join(image_path, "chat_light.png")), 
    size=(20, 20)
)
Images automatically switch when the user changes appearance mode. No additional code required!

Using Images in Widgets

# Label with image and text
self.navigation_frame_label = customtkinter.CTkLabel(
    self.navigation_frame, 
    text="  Image Example", 
    image=self.logo_image,
    compound="left", 
    font=customtkinter.CTkFont(size=15, weight="bold")
)
self.navigation_frame_label.grid(row=0, column=0, padx=20, pady=20)

# Button with image only
self.home_frame_button_1 = customtkinter.CTkButton(
    self.home_frame, 
    text="", 
    image=self.image_icon_image
)

# Button with image and text (different compound options)
self.home_frame_button_2 = customtkinter.CTkButton(
    self.home_frame, 
    text="CTkButton", 
    image=self.image_icon_image, 
    compound="right"  # Options: "left", "right", "top", "bottom"
)
self.home_frame_button_2.grid(row=2, column=0, padx=20, pady=10)

self.home_frame_button_3 = customtkinter.CTkButton(
    self.home_frame, 
    text="CTkButton", 
    image=self.image_icon_image, 
    compound="top"
)
self.home_frame_button_3.grid(row=3, column=0, padx=20, pady=10)

self.home_frame_button_4 = customtkinter.CTkButton(
    self.home_frame, 
    text="CTkButton", 
    image=self.image_icon_image, 
    compound="bottom", 
    anchor="w"
)
self.home_frame_button_4.grid(row=4, column=0, padx=20, pady=10)

Multi-Frame Navigation

Implement frame switching for multi-page applications.
1

Create navigation frame with buttons

# Create navigation frame
self.navigation_frame = customtkinter.CTkFrame(self, corner_radius=0)
self.navigation_frame.grid(row=0, column=0, sticky="nsew")
self.navigation_frame.grid_rowconfigure(4, weight=1)

# Add navigation buttons
self.home_button = customtkinter.CTkButton(
    self.navigation_frame, 
    corner_radius=0, 
    height=40, 
    border_spacing=10, 
    text="Home",
    fg_color="transparent", 
    text_color=("gray10", "gray90"), 
    hover_color=("gray70", "gray30"),
    image=self.home_image, 
    anchor="w", 
    command=self.home_button_event
)
self.home_button.grid(row=1, column=0, sticky="ew")

self.frame_2_button = customtkinter.CTkButton(
    self.navigation_frame, 
    corner_radius=0, 
    height=40, 
    border_spacing=10, 
    text="Frame 2",
    fg_color="transparent", 
    text_color=("gray10", "gray90"), 
    hover_color=("gray70", "gray30"),
    image=self.chat_image, 
    anchor="w", 
    command=self.frame_2_button_event
)
self.frame_2_button.grid(row=2, column=0, sticky="ew")
2

Create content frames

# Create home frame
self.home_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent")
self.home_frame.grid_columnconfigure(0, weight=1)

# Create second frame
self.second_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent")

# Create third frame
self.third_frame = customtkinter.CTkFrame(self, corner_radius=0, fg_color="transparent")

# Select default frame
self.select_frame_by_name("home")
3

Implement frame switching logic

def select_frame_by_name(self, name):
    # Set button color for selected button
    self.home_button.configure(
        fg_color=("gray75", "gray25") if name == "home" else "transparent"
    )
    self.frame_2_button.configure(
        fg_color=("gray75", "gray25") if name == "frame_2" else "transparent"
    )
    self.frame_3_button.configure(
        fg_color=("gray75", "gray25") if name == "frame_3" else "transparent"
    )
    
    # Show selected frame
    if name == "home":
        self.home_frame.grid(row=0, column=1, sticky="nsew")
    else:
        self.home_frame.grid_forget()
    if name == "frame_2":
        self.second_frame.grid(row=0, column=1, sticky="nsew")
    else:
        self.second_frame.grid_forget()
    if name == "frame_3":
        self.third_frame.grid(row=0, column=1, sticky="nsew")
    else:
        self.third_frame.grid_forget()

def home_button_event(self):
    self.select_frame_by_name("home")

def frame_2_button_event(self):
    self.select_frame_by_name("frame_2")

def frame_3_button_event(self):
    self.select_frame_by_name("frame_3")

Background Images

Create applications with full-window background images.
import customtkinter
from PIL import Image
import os

class App(customtkinter.CTk):
    width = 900
    height = 600
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        self.title("CustomTkinter Background Image")
        self.geometry(f"{self.width}x{self.height}")
        self.resizable(False, False)
        
        # Load and create background image
        current_path = os.path.dirname(os.path.realpath(__file__))
        self.bg_image = customtkinter.CTkImage(
            Image.open(current_path + "/test_images/bg_gradient.jpg"),
            size=(self.width, self.height)
        )
        self.bg_image_label = customtkinter.CTkLabel(self, image=self.bg_image)
        self.bg_image_label.grid(row=0, column=0)
        
        # Create login frame on top of background
        self.login_frame = customtkinter.CTkFrame(self, corner_radius=0)
        self.login_frame.grid(row=0, column=0, sticky="ns")
Place the background image label first, then overlay other frames on top using the same grid cell.

Login/Main Frame Switching

Implement authentication flows with frame switching.
def __init__(self):
    # ... background setup ...
    
    # Create login frame
    self.login_frame = customtkinter.CTkFrame(self, corner_radius=0)
    self.login_frame.grid(row=0, column=0, sticky="ns")
    self.login_label = customtkinter.CTkLabel(
        self.login_frame, 
        text="CustomTkinter\nLogin Page",
        font=customtkinter.CTkFont(size=20, weight="bold")
    )
    self.login_label.grid(row=0, column=0, padx=30, pady=(150, 15))
    self.username_entry = customtkinter.CTkEntry(
        self.login_frame, 
        width=200, 
        placeholder_text="username"
    )
    self.username_entry.grid(row=1, column=0, padx=30, pady=(15, 15))
    self.password_entry = customtkinter.CTkEntry(
        self.login_frame, 
        width=200, 
        show="*", 
        placeholder_text="password"
    )
    self.password_entry.grid(row=2, column=0, padx=30, pady=(0, 15))
    self.login_button = customtkinter.CTkButton(
        self.login_frame, 
        text="Login", 
        command=self.login_event, 
        width=200
    )
    self.login_button.grid(row=3, column=0, padx=30, pady=(15, 15))
    
    # Create main frame
    self.main_frame = customtkinter.CTkFrame(self, corner_radius=0)
    self.main_frame.grid_columnconfigure(0, weight=1)
    self.main_label = customtkinter.CTkLabel(
        self.main_frame, 
        text="CustomTkinter\nMain Page",
        font=customtkinter.CTkFont(size=20, weight="bold")
    )
    self.main_label.grid(row=0, column=0, padx=30, pady=(30, 15))
    self.back_button = customtkinter.CTkButton(
        self.main_frame, 
        text="Back", 
        command=self.back_event, 
        width=200
    )
    self.back_button.grid(row=1, column=0, padx=30, pady=(15, 15))

def login_event(self):
    print("Login pressed - username:", self.username_entry.get(), 
          "password:", self.password_entry.get())
    
    self.login_frame.grid_forget()  # Remove login frame
    self.main_frame.grid(row=0, column=0, sticky="nsew", padx=100)  # Show main frame

def back_event(self):
    self.main_frame.grid_forget()  # Remove main frame
    self.login_frame.grid(row=0, column=0, sticky="ns")  # Show login frame

Using CTkFont for Custom Typography

# Create custom fonts
title_font = customtkinter.CTkFont(size=20, weight="bold")
label_font = customtkinter.CTkFont(size=15, weight="bold")

# Apply to widgets
self.logo_label = customtkinter.CTkLabel(
    self.sidebar_frame, 
    text="CustomTkinter", 
    font=title_font
)

self.navigation_frame_label = customtkinter.CTkLabel(
    self.navigation_frame, 
    text="  Image Example", 
    image=self.logo_image,
    compound="left", 
    font=label_font
)
Font sizes in CustomTkinter v5.0.0+ are measured in pixels, not points. Adjust your sizes accordingly when migrating from older versions.

Combining Multiple Advanced Features

Here’s an example combining scrollable frames, images, and custom components:
current_dir = os.path.dirname(os.path.abspath(__file__))
self.scrollable_label_button_frame = ScrollableLabelButtonFrame(
    master=self, 
    width=300, 
    command=self.label_button_frame_event, 
    corner_radius=0
)
self.scrollable_label_button_frame.grid(row=0, column=2, padx=0, pady=0, sticky="nsew")

# Add items with images
for i in range(20):
    self.scrollable_label_button_frame.add_item(
        f"image and item {i}", 
        image=customtkinter.CTkImage(
            Image.open(os.path.join(current_dir, "test_images", "chat_light.png"))
        )
    )

def label_button_frame_event(self, item):
    print(f"label button frame clicked: {item}")

Dynamic Widget Resizing

Control widget dimensions and resizing behavior.
# Create with initial size
self.scrollable_radiobutton_frame = ScrollableRadiobuttonFrame(
    master=self, 
    width=500, 
    command=self.radiobutton_frame_event,
    item_list=[f"item {i}" for i in range(100)],
    label_text="ScrollableRadiobuttonFrame"
)
self.scrollable_radiobutton_frame.grid(row=0, column=1, padx=15, pady=15, sticky="ns")

# Reconfigure width dynamically
self.scrollable_radiobutton_frame.configure(width=200)
import customtkinter
import os
from PIL import Image

class AdvancedApp(customtkinter.CTk):
    def __init__(self):
        super().__init__()
        
        self.title("Advanced CustomTkinter App")
        self.grid_rowconfigure(0, weight=1)
        self.columnconfigure(2, weight=1)
        
        customtkinter.set_appearance_mode("dark")
        
        # Create scrollable checkbox frame
        self.scrollable_checkbox_frame = ScrollableCheckBoxFrame(
            master=self, 
            width=200, 
            command=self.checkbox_frame_event,
            item_list=[f"item {i}" for i in range(50)]
        )
        self.scrollable_checkbox_frame.grid(row=0, column=0, padx=15, pady=15, sticky="ns")
        
        # Create scrollable radiobutton frame
        self.scrollable_radiobutton_frame = ScrollableRadiobuttonFrame(
            master=self, 
            width=500, 
            command=self.radiobutton_frame_event,
            item_list=[f"item {i}" for i in range(100)],
            label_text="Options"
        )
        self.scrollable_radiobutton_frame.grid(row=0, column=1, padx=15, pady=15, sticky="ns")
        
        # Create scrollable label and button frame with images
        current_dir = os.path.dirname(os.path.abspath(__file__))
        self.scrollable_label_button_frame = ScrollableLabelButtonFrame(
            master=self, 
            width=300, 
            command=self.label_button_frame_event, 
            corner_radius=0
        )
        self.scrollable_label_button_frame.grid(row=0, column=2, padx=0, pady=0, sticky="nsew")
        
        for i in range(20):
            self.scrollable_label_button_frame.add_item(
                f"item {i}", 
                image=customtkinter.CTkImage(
                    Image.open(os.path.join(current_dir, "test_images", "icon.png"))
                )
            )
    
    def checkbox_frame_event(self):
        print(f"checkbox frame: {self.scrollable_checkbox_frame.get_checked_items()}")
    
    def radiobutton_frame_event(self):
        print(f"radiobutton frame: {self.scrollable_radiobutton_frame.get_checked_item()}")
    
    def label_button_frame_event(self, item):
        print(f"button clicked: {item}")

if __name__ == "__main__":
    app = AdvancedApp()
    app.mainloop()

Build docs developers (and LLMs) love