If you’ve tried to fit a customer form, an order list and a payment screen into one window, you’ve probably ended up with frames stacked on top of each other. A Tkinter notebook fixes that: ttk.Notebook gives each frame its own clickable tab.
Here’s the finished order desk I built for this post, running on Windows 11:

In this tutorial, I’ll build that Tkinter notebook one feature at a time on Python 3.12.5 with Tk 8.6, from adding tabs to styling them.
The sample app we’ll use: a coffee shop order desk
Every example uses the same small app for Bluebird Coffee, with three tabs:
- Customer: name (in
name_var) and phone. - Order: 2 Cold Brews at $4.75 and a $3.25 muffin.
- Payment: $12.75 due; we’ll hide and disable it later.
I lay out the Customer fields with grid(); if that’s new, see using a Tkinter Frame with grid first.
Method 1: Create tabs with ttk.Notebook and add() (Recommended)
Use this for any window that has a few separate screens. Each tab is a ttk.Frame whose parent is the notebook. Here’s the full program:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
root.title("Bluebird Coffee - Order desk")
root.geometry("640x320")
notebook = ttk.Notebook(root)
notebook.pack(fill="both", expand=True, padx=10, pady=10)
customer = ttk.Frame(notebook, padding=16)
order = ttk.Frame(notebook, padding=16)
payment = ttk.Frame(notebook, padding=16)
notebook.add(customer, text="Customer")
notebook.add(order, text="Order")
notebook.add(payment, text="Payment")
name_var = tk.StringVar()
ttk.Label(customer, text="Name").grid(row=0, column=0, sticky="w")
ttk.Entry(customer, textvariable=name_var, width=30).grid(
row=0, column=1, padx=8, pady=4)
ttk.Label(customer, text="Phone").grid(row=1, column=0, sticky="w")
ttk.Entry(customer, width=30).grid(row=1, column=1, padx=8, pady=4)
ttk.Label(order, text="2 x Cold Brew @ $4.75").pack(anchor="w")
ttk.Label(order, text="1 x Blueberry Muffin @ $3.25").pack(anchor="w")
ttk.Label(payment, text="Total due: $12.75").pack(anchor="w")
root.mainloop()
You get three tabs, and notebook.index("end") returns 3.

How does this code work?
ttk.Notebook(root) creates the tab bar and page area. Each add(frame, text=...) appends a tab, and the first one is selected automatically. A frame is just a container, as in my Python Tkinter Frame tutorial.
Important: give each tab’s widgets the frame as parent, not root, or they’ll land outside the notebook.
Method 2: Insert, hide and forget Tkinter notebook tabs
Same order desk, now with a Gift Card tab and a Payment tab that stays hidden until the order is ready. Add this before root.mainloop():
gift = ttk.Frame(notebook, padding=16)
ttk.Label(gift, text="Gift card balance: $25.00").pack(anchor="w")
notebook.insert(payment, gift, text="Gift Card") # before Payment
notebook.hide(payment) # until order is ready
def remove_gift():
notebook.forget(gift) # gone for good
def ready_to_pay():
notebook.add(payment) # a hidden tab comes back in place
notebook.select(payment)
ttk.Button(order, text="Remove gift card",
command=remove_gift).pack(anchor="w", pady=(12, 4))
ttk.Button(order, text="Ready to pay",
command=ready_to_pay).pack(anchor="w")
print("Tabs:", [notebook.tab(t, "text") for t in notebook.tabs()])
print("Payment state:", notebook.tab(payment, "state"))
On start it prints Tabs: ['Customer', 'Order', 'Gift Card', 'Payment'] and Payment state: hidden.

How does this code work?
insert(payment, gift, ...) puts the new tab in front of Payment; a number like 1 works too.
hide() only hides the tab: it stays in tabs() with the state hidden, and add(payment) restores it in the same spot with its text. forget() removes the tab completely, though the frame itself still exists.
If you’d rather swap whole screens without a tab bar, my guide to going to the next page in Python Tkinter uses stacked frames instead.
How do I get the current tab in a Tkinter notebook?
Call notebook.select() with no argument for the open tab’s widget name, index("current") for its position and tab(tab_id, "text") for its label.
To react when the user switches, bind the <<NotebookTabChanged>> virtual event. Here the order desk gets a status line:
status = ttk.Label(root, anchor="w", padding=(12, 0, 0, 8))
status.pack(fill="x")
def on_tab_changed(event):
nb = event.widget
tab_id = nb.select() # widget name of the open tab
index = nb.index(tab_id) # same as nb.index("current")
text = nb.tab(tab_id, "text")
status.config(text=f"Viewing: {text} (tab {index + 1} "
f"of {nb.index('end')})")
notebook.bind("<<NotebookTabChanged>>", on_tab_changed)
After opening the Order tab, the status line reads Viewing: Order (tab 2 of 3).

To see when it fires, I switched tabs from code in a test script, run in VS Code:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
notebook = ttk.Notebook(root)
notebook.pack()
for name in ("Customer", "Order", "Payment"):
notebook.add(ttk.Frame(notebook, width=320, height=120), text=name)
def on_tab_changed(event):
nb = event.widget
print(f"Changed -> {nb.index('current')}: {nb.tab('current', 'text')}")
notebook.bind("<<NotebookTabChanged>>", on_tab_changed)
print("Tab count:", notebook.index("end"))
root.after(400, lambda: notebook.select(1))
root.after(800, lambda: notebook.select(2))
root.after(1200, root.destroy)
root.mainloop()
Output:
Tab count: 3
Changed -> 0: Customer
Changed -> 1: Order
Changed -> 2: Payment

How does this code work?
select(1) opens a tab by position and fires the same event as a click. Note the first change line: the event also runs when the window first shows tab 0.
So your handler shouldn’t assume a user clicked. More event patterns are in Python Tkinter events.
Method 3: Disable a notebook tab until a form is filled in
A disabled tab stays visible but grayed out. On the order desk, Payment stays locked until the customer has a name:
notebook.tab(payment, state="disabled") # grayed out, can't be opened
def check_customer(*args):
ready = bool(name_var.get().strip())
notebook.tab(payment, state="normal" if ready else "disabled")
name_var.trace_add("write", check_customer)
Before the name is typed, the tab state is disabled and select(payment) leaves the Customer tab open; after it, the state is normal.

How does this code work?
notebook.tab(tab, state=...) changes one tab’s options; the states are normal, disabled and hidden. trace_add("write", ...) calls check_customer after every keystroke in the entry, like the validation in my Python Tkinter Entry guide.
Method 4: Style Tkinter notebook tabs with ttk.Style
Notebook tabs are ttk widgets, so you style them with ttk.Style, not bg=. Same app, new look:
style = ttk.Style()
style.theme_use("clam")
style.configure("TNotebook", background="#e8eef5")
style.configure("TNotebook.Tab", padding=(18, 6),
font=("Segoe UI", 10, "bold"))
style.map("TNotebook.Tab",
background=[("selected", "#1d4ed8"), ("active", "#bfdbfe")],
foreground=[("selected", "white")])

How does this code work?
TNotebook styles the whole widget and TNotebook.Tab styles each tab. style.map() sets values per state, so selected and active (hover) get their own colors.
I use clam because the default Windows theme, vista, ignores tab colors.
Pro Tip: a named style like Side.TNotebook changes one notebook only. Here it moves the tabs to the left:
style = ttk.Style()
style.configure("Side.TNotebook", tabposition="wn") # west, top
notebook.configure(style="Side.TNotebook")
