Build your first app#

The Quickstart put a themed window on the screen. This tutorial builds a small but complete application on top of it — a contact book with a form, live input validation, and a searchable data table — so you touch every part of a real ttkbootstrap app in one sitting: layout, widgets, events, state, and styling.

We build it one piece at a time. Each step is runnable on its own, and each links onward to the Foundations page that covers the idea in depth. The complete program is at the end.

The finished contact book — a New contact form above a searchable table of contacts with a status line — light theme The finished contact book — a New contact form above a searchable table of contacts with a status line — dark theme

What you need#

ttkbootstrap installed (see Installation) and Python 3.10+. Everything here uses only ttkbootstrap — no other dependencies.

Step 1 — the application shell#

Every ttkbootstrap app starts with a root window and ends with mainloop(), the call that hands control to tkinter’s event loop and keeps the window alive. In between you build your interface.

We put the interface in a Frame subclass rather than piling widgets directly onto the root. A frame is a container; subclassing it keeps the app’s state and its widgets together in one object, which is how real apps stay organized as they grow.

import ttkbootstrap as ttk
from ttkbootstrap.constants import *


class ContactBook(ttk.Frame):
    def __init__(self, master):
        super().__init__(master, padding=16)
        self.pack(fill=BOTH, expand=YES)

        ttk.Label(self, text="Contact Book", font="-size 16 -weight bold").pack()


app = ttk.App(title="Contact Book", theme="bootstrap-light", size=(560, 520))
ContactBook(app)
app.mainloop()
  • App is the enhanced root — it creates the window and installs the theme in one step. (It is also exported as ttk.Window; both name the same class.)

  • padding=16 insets the frame’s contents so nothing hugs the window edge.

  • pack(fill=BOTH, expand=YES) makes the frame grow to fill the window. We import * from ttkbootstrap.constants so anchors and fills read as BOTH/YES/W instead of string literals.

See also

Step 2 — a form with grid#

Forms are rows of label + input, so we lay them out with grid — the geometry manager built for aligned rows and columns. Each widget names its row and column; sticky says which edges it clings to inside its cell.

Put this in a new method and call it from __init__:

def _build_form(self):
    form = ttk.Labelframe(self, text="New contact", padding=12)
    form.pack(fill=X)
    form.columnconfigure(1, weight=1)          # let the input column stretch

    ttk.Label(form, text="Name").grid(row=0, column=0, sticky=W, padx=(0, 8), pady=4)
    ttk.Entry(form).grid(row=0, column=1, sticky=EW, pady=4)

    ttk.Label(form, text="Email").grid(row=1, column=0, sticky=W, padx=(0, 8), pady=4)
    ttk.Entry(form).grid(row=1, column=1, sticky=EW, pady=4)

    ttk.Label(form, text="Category").grid(row=2, column=0, sticky=W, padx=(0, 8), pady=4)
    category = ttk.Combobox(form, values=["Friend", "Family", "Work"], state="readonly")
    category.grid(row=2, column=1, sticky=EW, pady=4)

The two pieces that make a grid form behave:

  • columnconfigure(1, weight=1) gives column 1 (the inputs) all the spare horizontal space, so the entries stretch with the window while the labels in column 0 stay their natural width.

  • sticky=EW on the inputs makes them fill that space left-to-right; sticky=W left-aligns the labels.

ttk.Labelframe is just a frame with a titled border — a tidy way to group the form. The state="readonly" combobox lets the user pick a category but not type a new one.

The New contact labelframe with aligned Name, Email, and Category rows; the entry and combobox stretch to the frame width — light theme The New contact labelframe with aligned Name, Email, and Category rows; the entry and combobox stretch to the frame width — dark theme

See also

Layout with grid builds a responsive form step by step — cells, sticky, padding, weight, and spanning.

Step 3 — hold the data with variables#

Right now the entries hold text, but our code has no handle on it. We bind each input to a variable — a StringVar — with textvariable=. The variable and the widget stay in sync automatically: read the variable to get what the user typed, set it to change what the widget shows.

Create the variables in __init__ (before building the form) and attach them:

# in __init__, before self._build_form()
self.name = ttk.StringVar()
self.email = ttk.StringVar()
self.category = ttk.StringVar(value="Friend")
# in _build_form, add textvariable= to each input
ttk.Entry(form, textvariable=self.name).grid(row=0, column=1, sticky=EW, pady=4)
...
ttk.Entry(form, textvariable=self.email).grid(row=1, column=1, sticky=EW, pady=4)
...
category = ttk.Combobox(form, textvariable=self.category,
                        values=["Friend", "Family", "Work"], state="readonly")
category.grid(row=2, column=1, sticky=EW, pady=4)

Giving category an initial value preselects “Friend” in the combobox.

See also

Variables & reactivity and the Variables feature guide for tracing changes and the other variable types.

Step 4 — validate the email#

A contact book with malformed emails is not much use. ttkbootstrap ships a small validation framework: attach a rule to an input and a failing value flags the widget with a danger-red border until it becomes valid again.

Keep a reference to the email entry so we can attach a rule to it:

# in _build_form, replace the plain email Entry line:
email_entry = ttk.Entry(form, textvariable=self.email)
email_entry.grid(row=1, column=1, sticky=EW, pady=4)
ttk.Validation.regex(email_entry, r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

regex() passes when the contents match the pattern (here: something @ something . something). By default the rule fires on focus-out, so the field flags only after the user leaves it — not on every keystroke.

See also

Input validation for the full rule set, custom rules, and controlling when rules run.

Step 5 — show the contacts in a table#

Contacts need somewhere to live. Tableview is ttkbootstrap’s data table — a themed grid with sorting, an optional search bar, and column controls built in. We give it column headings and start it empty:

def _build_table(self):
    self.table = ttk.Tableview(
        self,
        coldata=["Name", "Email", "Category"],
        rowdata=[],
        searchable=True,
        bootstyle="primary",
        height=8,
    )
    self.table.pack(fill=BOTH, expand=YES, pady=(16, 8))
  • coldata is the list of column headings; rowdata=[] starts with no rows.

  • searchable=True adds the search bar above the table — press Enter in it to filter.

  • fill=BOTH, expand=YES lets the table soak up the space below the form as the window resizes.

See also

The Tableview widget page for its full API — pagination, column configuration, exporting, and programmatic selection.

Step 6 — wire the button to a callback#

The last piece connects the form to the table. A button runs a callback — the function you pass as command= — when clicked. Ours reads the variables, adds a row, and resets the form.

Add the button to the form, and the method it calls:

# at the end of _build_form:
add_button = ttk.Button(form, text="Add contact", bootstyle="success",
                        command=self.add_contact)
add_button.grid(row=3, column=1, sticky=E, pady=(8, 0))
def add_contact(self):
    name = self.name.get().strip()
    email = self.email.get().strip()
    if not name or not email:
        self.status.configure(text="Name and email are required.", bootstyle="danger")
        return
    self.table.insert_row(values=[name, email, self.category.get()])
    count = len(self.table.get_rows())
    self.status.configure(text=f"{count} contact(s).", bootstyle="secondary")
    self.name.set("")
    self.email.set("")

Notice how the pieces come together: we get() the variables we bound in step 3, guard against empty input, insert_row() into the table from step 5 (it refreshes the view for us), then set("") to clear the entries for the next contact. command=self.add_contact passes the method itself — no parentheses; tkinter calls it for you on each click.

The status line is a label at the bottom of the app; add a method to build it and call it from __init__:

def _build_status(self):
    self.status = ttk.Label(self, text="No contacts yet.", bootstyle="secondary")
    self.status.pack(fill=X)

See also

Events & callbacks for command vs bind, the event object, and after.

A note on styling#

We styled as we went, with bootstyle="success" for the Add button, "primary" for the table’s accents, "secondary"/"danger" on the status label to signal calm vs. error. bootstyle describes intent (a color and, optionally, a variant), never a literal color, so the whole app re-themes when you change theme on the App. Try theme="bootstrap-dark" — every widget follows.

See also

Styling with bootstyle for the full grammar and the reference table of every keyword.

The complete app#

import ttkbootstrap as ttk
from ttkbootstrap.constants import *


class ContactBook(ttk.Frame):
    def __init__(self, master):
        super().__init__(master, padding=16)
        self.pack(fill=BOTH, expand=YES)

        self.name = ttk.StringVar()
        self.email = ttk.StringVar()
        self.category = ttk.StringVar(value="Friend")

        self._build_form()
        self._build_table()
        self._build_status()

    def _build_form(self):
        form = ttk.Labelframe(self, text="New contact", padding=12)
        form.pack(fill=X)
        form.columnconfigure(1, weight=1)

        ttk.Label(form, text="Name").grid(row=0, column=0, sticky=W, padx=(0, 8), pady=4)
        ttk.Entry(form, textvariable=self.name).grid(row=0, column=1, sticky=EW, pady=4)

        ttk.Label(form, text="Email").grid(row=1, column=0, sticky=W, padx=(0, 8), pady=4)
        email_entry = ttk.Entry(form, textvariable=self.email)
        email_entry.grid(row=1, column=1, sticky=EW, pady=4)
        ttk.Validation.regex(email_entry, r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

        ttk.Label(form, text="Category").grid(row=2, column=0, sticky=W, padx=(0, 8), pady=4)
        category = ttk.Combobox(form, textvariable=self.category,
                                values=["Friend", "Family", "Work"], state="readonly")
        category.grid(row=2, column=1, sticky=EW, pady=4)

        add_button = ttk.Button(form, text="Add contact", bootstyle="success",
                                command=self.add_contact)
        add_button.grid(row=3, column=1, sticky=E, pady=(8, 0))

    def _build_table(self):
        self.table = ttk.Tableview(
            self,
            coldata=["Name", "Email", "Category"],
            rowdata=[],
            searchable=True,
            bootstyle="primary",
            height=8,
        )
        self.table.pack(fill=BOTH, expand=YES, pady=(16, 8))

    def _build_status(self):
        self.status = ttk.Label(self, text="No contacts yet.", bootstyle="secondary")
        self.status.pack(fill=X)

    def add_contact(self):
        name = self.name.get().strip()
        email = self.email.get().strip()
        if not name or not email:
            self.status.configure(text="Name and email are required.", bootstyle="danger")
            return
        self.table.insert_row(values=[name, email, self.category.get()])
        count = len(self.table.get_rows())
        self.status.configure(text=f"{count} contact(s).", bootstyle="secondary")
        self.name.set("")
        self.email.set("")


app = ttk.App(title="Contact Book", theme="bootstrap-light", size=(560, 520))
ContactBook(app)
app.mainloop()

Where to go next#

You have now used every layer of a ttkbootstrap app. To go deeper: