CTkDataTable User Guide and API Reference

CTkDataTable is a virtualized data table for CustomTkinter. Use it when you want to show records from Python lists, database queries, reports, admin screens, dashboards, or audit logs without creating one Tkinter widget for every cell.

The table renders only the visible body rows, so large datasets stay responsive. Your full row list is still kept in memory.

What You Need First

CTkDataTable requires Python 3.11 or newer. Install it from PyPI with the same Python interpreter that runs your application:

python -m pip install CTkDataTable

This installs the supported dependencies automatically (customtkinter>=5.2.2,<6.1 and CTkScrollableDropdownPP>=2.2,<2.5 for this release). On Linux, Python's Tk support may also need to be installed through the operating-system package manager if import tkinter is unavailable.

Import the widget:

import customtkinter as ctk

from CTkDataTable import CTkDataTable

Import only the helpers your application uses. The complete public import surface is:

from CTkDataTable import (
    BadgeStyle,
    CellChangeCallback,
    CellChangeEvent,
    CellEditRequest,
    CellValidationError,
    CellValidator,
    ColorValue,
    Column,
    ColumnAlign,
    ColumnDefinition,
    ColumnType,
    ComboOption,
    ComboboxChangeCallback,
    CTkDataTable,
    EventOrigin,
    RowSnapshot,
    SelectionChangeCallback,
    SelectionChangeEvent,
    TableAction,
    TableColumn,
    TableDensity,
    TableRowEvent,
    TableStyle,
    rows_from_cursor,
)

ComboboxChangeCallback is a compatibility alias for CellChangeCallback; new annotations should normally use CellChangeCallback. See Public Imports and Typing Helpers for the exact aliases and literal values.

Project links: PyPI package, source repository, and issue tracker.

Use the table in three steps:

  1. Define columns.
  2. Provide rows.
  3. Place the table with grid(), pack(), or place().

The most important rule:

column["key"] == row_dictionary_key

For example, this column:

{"key": "name", "title": "Name", "width": 180}

reads this value from each row:

{"name": "Alice"}

Use the exact same spelling and capitalization. customer_name and customerName are different keys.

Entry Point 1: Static Data Display

Use this pattern when you already have a list of records and want to show it.

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Customers")
app.geometry("760x420")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {"key": "id", "title": "ID", "width": 80, "type": "number"},
    {"key": "name", "title": "Customer", "width": 220},
    {"key": "status", "title": "Status", "width": 140, "type": "badge"},
]

rows = [
    {"id": 1, "name": "Northwind Components", "status": "Open"},
    {"id": 2, "name": "Meridian Foods", "status": "Closed"},
    {"id": 3, "name": "Blue Ridge Logistics", "status": "In Review"},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

Entry Point 2: Real-Time Updates

Use add_row(), add_rows(), update_row_where(), and delete_row_by_key() when rows change after the table is already visible.

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Live Orders")
app.geometry("820x420")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(1, weight=1)

columns = [
    {"key": "id", "title": "Order", "width": 100},
    {"key": "customer", "title": "Customer", "width": 220},
    {"key": "status", "title": "Status", "width": 140, "type": "badge"},
    {"key": "amount", "title": "Amount", "width": 120, "type": "currency"},
]

rows = [
    {"id": "SO-1001", "customer": "Northwind Components", "status": "Open", "amount": 1250},
    {"id": "SO-1002", "customer": "Meridian Foods", "status": "Open", "amount": 890},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 16))


def add_order() -> None:
    table.add_row(
        {"id": "SO-1003", "customer": "Blue Ridge Logistics", "status": "Open", "amount": 1425}
    )


def mark_shipped() -> None:
    table.update_row_where(
        "id",
        "SO-1001",
        {"id": "SO-1001", "customer": "Northwind Components", "status": "Shipped", "amount": 1250},
    )


def remove_order() -> None:
    table.delete_row_by_key("id", "SO-1002")


toolbar = ctk.CTkFrame(app, corner_radius=0)
toolbar.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 8))
ctk.CTkButton(toolbar, text="Add", command=add_order).grid(row=0, column=0, padx=6, pady=8)
ctk.CTkButton(toolbar, text="Ship SO-1001", command=mark_shipped).grid(row=0, column=1, padx=6, pady=8)
ctk.CTkButton(toolbar, text="Remove SO-1002", command=remove_order).grid(row=0, column=2, padx=6, pady=8)

app.mainloop()

Entry Point 3: Interactive Sorting, Searching, and Filtering

Users can click sortable headers. Your code can also call sort_by(), search(), and set_column_filter().

import customtkinter as ctk

from CTkDataTable import CTkDataTable, TableRowEvent


app = ctk.CTk()
app.title("Interactive Table")
app.geometry("900x500")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(2, weight=1)

status = ctk.CTkLabel(app, text="No selection", anchor="w")
status.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 4))

search = ctk.CTkEntry(app, placeholder_text="Search visible columns")
search.grid(row=1, column=0, sticky="ew", padx=16, pady=(4, 8))

columns = [
    {"key": "id", "title": "ID", "width": 80, "type": "number"},
    {"key": "name", "title": "Customer", "width": 220},
    {"key": "status", "title": "Status", "width": 140, "type": "badge"},
    {"key": "amount", "title": "Amount", "width": 120, "type": "currency"},
]

rows = [
    {"id": 1, "name": "Northwind Components", "status": "Open", "amount": 1250},
    {"id": 2, "name": "Meridian Foods", "status": "Closed", "amount": 890},
    {"id": 3, "name": "Blue Ridge Logistics", "status": "Open", "amount": 1425},
]


def show_selected(event: TableRowEvent) -> None:
    status.configure(
        text=f"Selected source row {event.source_index}, visible row {event.view_index}: {event.row['name']}"
    )


def report_sort(column_key: str, ascending: bool) -> None:
    direction = "ascending" if ascending else "descending"
    status.configure(text=f"Sorted by {column_key} {direction}")


table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    multi_select=True,
    on_row_click=show_selected,
    on_sort=report_sort,
)
table.grid(row=2, column=0, sticky="nsew", padx=16, pady=(0, 16))

search.bind("<KeyRelease>", lambda _event: table.search(search.get()))

table.set_column_filter("status", {"type": "equals", "value": "Open"})
table.sort_by("amount", ascending=False)

app.mainloop()

Row Data

Rows are converted to dictionaries internally. The table accepts:

Source row value How to use it
dict Pass directly in data, set_data(), add_row(), or add_rows().
Any mapping object Pass directly if it behaves like a dictionary.
sqlite3.Row Set connection.row_factory = sqlite3.Row, then pass fetched rows directly.
SQLAlchemy rows with _mapping Pass fetched result rows directly.
PostgreSQL dictionary rows Use psycopg 3 dict_row or psycopg2 RealDictCursor, then pass fetched rows directly.
Plain DB-API tuple rows Convert them with rows_from_cursor(cursor).

Plain tuple rows do not contain column names, so the table cannot use them directly.

Stable Row IDs

For database-backed data, configure row_key with the field holding the primary key:

table = CTkDataTable(app, columns=columns, data=rows, row_key="customer_id")

Every row must contain that field and its value must be hashable and unique. The table validates a replacement dataset before applying it, so a missing or duplicate ID does not partly replace the current rows. Stable IDs survive sorting, filtering, and source-index shifts and are exposed by events and all ..._by_id() methods. Cell editing cannot change the configured identity field.

SQLite Rows

import sqlite3


connection = sqlite3.connect("customers.db")
connection.row_factory = sqlite3.Row

rows = connection.execute(
    """
    SELECT id, name, status
    FROM customers
    ORDER BY name
    """
).fetchall()

table.set_data(rows)

DB-API Cursor Rows

from CTkDataTable import rows_from_cursor


cursor.execute(
    """
    SELECT id, name, status
    FROM customers
    ORDER BY name
    """
)

table.set_data(rows_from_cursor(cursor))

SQLAlchemy Rows

from sqlalchemy import text


with engine.connect() as connection:
    result = connection.execute(
        text("SELECT id, name, status FROM customers ORDER BY name")
    )
    table.set_data(result.fetchall())

If database column names do not match your table keys, use SQL aliases:

SELECT customer_id AS id, customer_name AS name, order_status AS status
FROM customers;

Column Definitions

You can define columns with dictionaries, TableColumn, or the fluent Column builder. Dictionary columns are the shortest option.

columns = [
    {"key": "id", "title": "ID", "width": 80, "type": "number"},
    {"key": "name", "title": "Customer", "width": 220},
]

Use TableColumn when you prefer typed Python objects:

from CTkDataTable import TableColumn


columns = [
    TableColumn(key="id", title="ID", width=80, type="number"),
    TableColumn(key="name", title="Customer", width=220),
]

Use Column when you want fluent column setup:

from CTkDataTable import Column


columns = [
    Column("id").title("ID").width(80).number(),
    Column("name").title("Customer").width(220).text(),
    Column("status").title("Status").width(140).badge(
        colors={"Open": "#22c55e", "Closed": "#64748b"},
        fallback_color="#94a3b8",
    ),
]

All Column Types in One App

This complete app demonstrates all 13 column types: text, number, percentage, currency, date, datetime, badge, checkbox, combobox, progress, link, pill_list, and action.

from datetime import date, datetime

import customtkinter as ctk

from CTkDataTable import CTkDataTable, TableRowEvent


app = ctk.CTk()
app.title("Column Types")
app.geometry("1180x520")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(1, weight=1)

message = ctk.CTkLabel(app, text="Click a link or action", anchor="w")
message.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 8))

columns = [
    {"key": "name", "title": "Text", "width": 180, "type": "text"},
    {"key": "quantity", "title": "Number", "width": 95, "type": "number", "number_format": "{:,.0f}"},
    {"key": "margin", "title": "Percentage", "width": 120, "type": "percentage", "percentage_format": "{value:.1f}%"},
    {"key": "amount", "title": "Currency", "width": 120, "type": "currency", "currency_symbol": "$"},
    {"key": "due_date", "title": "Date", "width": 120, "type": "date", "date_format": "%d %b %Y"},
    {"key": "updated_at", "title": "Datetime", "width": 150, "type": "datetime", "datetime_format": "%d %b %H:%M"},
    {
        "key": "status",
        "title": "Badge",
        "width": 120,
        "type": "badge",
        "badge_colors": {"Open": "#22c55e", "Blocked": "#ef4444"},
        "badge_fallback_color": "#64748b",
    },
    {"key": "approved", "title": "Checkbox", "width": 105, "type": "checkbox"},
    {
        "key": "stage",
        "title": "Combobox",
        "width": 145,
        "type": "combobox",
        "values": ["New", "Review", "Done"],
    },
    {"key": "progress", "title": "Progress", "width": 145, "type": "progress", "progress_text_format": "{percent:.0f}%"},
    {"key": "profile", "title": "Link", "width": 120, "type": "link"},
    {
        "key": "tags",
        "title": "Pills",
        "width": 170,
        "type": "pill_list",
        "pill_colors": {"Urgent": "#ef4444", "Finance": "#0ea5e9"},
        "pill_fallback_color": "#64748b",
        "pill_text_color": "#ffffff",
    },
    {
        "key": "actions",
        "title": "Action",
        "width": 170,
        "type": "action",
        "sortable": False,
        "actions": [
            {"key": "view", "label": "View"},
            {"key": "delete", "label": "Delete"},
        ],
    },
]

rows = [
    {
        "name": "Northwind Components",
        "quantity": 1200,
        "margin": 18.4,
        "amount": 12640.5,
        "due_date": date(2026, 6, 12),
        "updated_at": datetime(2026, 6, 4, 9, 30),
        "status": "Open",
        "approved": True,
        "stage": "Review",
        "progress": 72,
        "profile": "Open",
        "tags": ["Urgent", "Finance"],
        "actions": None,
    },
    {
        "name": "Meridian Foods",
        "quantity": 860,
        "margin": 7.5,
        "amount": -240.75,
        "due_date": "2026-06-18",
        "updated_at": "2026-06-04T15:45:00",
        "status": "Blocked",
        "approved": False,
        "stage": "New",
        "progress": 35,
        "profile": "Open",
        "tags": "Sales, Follow-up",
        "actions": None,
    },
]


def handle_link(event: TableRowEvent) -> None:
    message.configure(text=f"Link clicked for {event.row['name']}")


def handle_action(event: TableRowEvent) -> None:
    message.configure(text=f"{event.action_key} clicked for {event.row['name']}")


table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    horizontal_scroll=True,
    on_link_click=handle_link,
    on_action_click=handle_action,
)
table.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 16))

app.mainloop()

Column Type Reference

Each column type below has a complete runnable example. Copy the whole code block into a Python file and run it from an environment where customtkinter and CTkDataTable are installed.

Every column type supports these common adjustable options:

Option Default Accepted values What it changes
key Required String row field name. Must match row dictionary keys exactly. Which row value the column displays.
title Title-cased key for dictionary columns String Header label.
width 140 for dictionary columns Integer greater than 0 Preferred column width in logical pixels.
align Depends on type "left", "center", or "right" Cell and header alignment.
visible True True or False Whether the column is rendered and searched.
sortable True True or False Whether header clicks sort this column.
formatter None Callable (value, row) -> str Replaces built-in display formatting for that column.
metadata {} Mapping App-specific metadata stored with the column.

Common Column builder methods also work with every type: .title(...), .width(...), .align(...), .hide(), .no_sort(), .fmt(...), and .metadata(...).

Text

Text is the default column type. It displays the row value as text.

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Text Column")
app.geometry("520x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {"key": "customer", "title": "Customer", "width": 260, "type": "text"},
]

rows = [
    {"customer": "Northwind Components"},
    {"customer": "Meridian Foods"},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

Builder methods for text columns:

Method Parameters What it sets
Column("customer").text() None Sets type="text".
Column("customer").fmt(func) Callable (value, row) -> str Sets a custom formatter for displayed text.

Adjustable options for text columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "text" Uses plain text display.

Number

Number columns right-align by default and sort numerically.

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Number Column")
app.geometry("520x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {
        "key": "quantity",
        "title": "Quantity",
        "width": 140,
        "type": "number",
        "number_format": "{:,.0f}",
    },
]

rows = [
    {"quantity": 12400},
    {"quantity": 820},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

Builder methods for number columns:

Method Parameters What it sets
Column("quantity").number(format=None) Optional format string or callable (value) -> str Sets type="number" and optional number_format.

Adjustable options for number columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "number" Uses numeric display and numeric sorting.
align "right" "left", "center", or "right" Number columns render right for "left" or "right"; use "center" to center.
number_format None Format string using .format(number) or callable (value) -> str Display format for numeric values.

Percentage

Percentage columns right-align by default, sort numerically, and format numeric row values with a percent sign.

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Percentage Column")
app.geometry("540x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {
        "key": "margin",
        "title": "Margin",
        "width": 150,
        "type": "percentage",
        "percentage_format": "{value:.1f}%",
    },
]

rows = [
    {"margin": 18.4},
    {"margin": 7.5},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

For ratio values such as 0.184, set percentage_multiplier to 100 before display.

Builder methods for percentage columns:

Method Parameters What it sets
Column("margin").percentage(format="{value:.0f}%", multiplier=1.0) Keyword-only format string and display multiplier Sets type="percentage", percentage_format, and percentage_multiplier.

Adjustable options for percentage columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "percentage" Uses percentage display and numeric sorting.
align "right" "left", "center", or "right" Percentage columns render right for "left" or "right"; use "center" to center.
percentage_format "{value:.0f}%" Format string using value, raw_value, and multiplier; positional {} also receives the display value. Display format for percentage values.
percentage_multiplier 1.0 Number greater than 0 Multiplies the raw numeric value before formatting.

Currency

Currency columns sort numerically and format money values.

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Currency Column")
app.geometry("560x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {
        "key": "amount",
        "title": "Amount",
        "width": 150,
        "type": "currency",
        "currency_symbol": "GBP ",
        "currency_format": "{symbol}{value:,.2f}",
        "currency_negative_format": "({symbol}{value:,.2f})",
    },
]

rows = [
    {"amount": 12640.5},
    {"amount": -2450.75},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

Builder methods for currency columns:

Method Parameters What it sets
Column("amount").currency(symbol="$", format="{symbol}{value:,.2f}", negative_format="-{symbol}{value:,.2f}") Keyword-only formatting options Sets type="currency" and currency formatting.

Adjustable options for currency columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "currency" Uses currency display and numeric sorting.
align "right" "left", "center", or "right" Currency columns render right for "left" or "right"; use "center" to center.
currency_symbol "$" String Symbol or prefix used by currency format strings.
currency_format "{symbol}{value:,.2f}" Format string using symbol, value, and signed_value Format for zero and positive values.
currency_negative_format "-{symbol}{value:,.2f}" Format string using symbol, absolute value, and signed_value Format for negative values.

Date

Date columns accept datetime.date, datetime.datetime, and ISO date strings.

from datetime import date

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Date Column")
app.geometry("560x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {"key": "due", "title": "Due", "width": 150, "type": "date", "date_format": "%d %b %Y"},
]

rows = [
    {"due": date(2026, 6, 12)},
    {"due": "2026-06-18"},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

Builder methods for date columns:

Method Parameters What it sets
Column("due").date(fmt="%Y-%m-%d") strftime format string Sets type="date" and date_format.

Adjustable options for date columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "date" Uses date display and date-aware sorting.
date_format "%Y-%m-%d" strftime format string Display format for parsed date values.

Datetime

Datetime columns accept datetime.datetime, datetime.date, and ISO datetime strings.

from datetime import datetime

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Datetime Column")
app.geometry("600x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {
        "key": "updated",
        "title": "Updated",
        "width": 180,
        "type": "datetime",
        "datetime_format": "%d %b %H:%M",
    },
]

rows = [
    {"updated": datetime(2026, 6, 4, 15, 45)},
    {"updated": "2026-06-05T09:15:00"},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

Builder methods for datetime columns:

Method Parameters What it sets
Column("updated").datetime(fmt="%Y-%m-%d %H:%M") strftime format string Sets type="datetime" and datetime_format.

Adjustable options for datetime columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "datetime" Uses datetime display and datetime-aware sorting.
datetime_format "%Y-%m-%d %H:%M" strftime format string Display format for parsed datetime values.

Badge

Badge columns draw a rounded label for status-like values.

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Badge Column")
app.geometry("560x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {
        "key": "status",
        "title": "Status",
        "width": 150,
        "type": "badge",
        "badge_colors": {"Open": "#22c55e", "Closed": "#64748b", "Blocked": "#ef4444"},
        "badge_fallback_color": "#94a3b8",
    },
]

rows = [
    {"status": "Open"},
    {"status": "Blocked"},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

Builder methods for badge columns:

Method Parameters What it sets
Column("status").badge(colors=None, fallback_color=None, fallback_handler=None) Optional color mapping, fallback color, and fallback handler Sets type="badge" and badge styling options.

Adjustable options for badge columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "badge" Draws the value as a rounded badge.
badge_colors {} Mapping of displayed text to color string or light/dark tuple Fill color for known badge values.
badge_fallback_color None Color string or light/dark tuple Fill color when the value is not in badge_colors.
badge_fallback_handler None Callable (value, row, column) -> BadgeStyle, color, or None Dynamic fallback text and colors.

Checkbox

Checkbox columns display boolean row values and toggle them when clicked. New code can use the column's on_change or the table-wide on_cell_change, both of which receive CellChangeEvent. The older on_checkbox_toggle callback remains supported; it receives TableRowEvent with the updated row, column_key set to the checkbox column, and action_key set to "checkbox".

import customtkinter as ctk

from CTkDataTable import CTkDataTable, TableRowEvent


app = ctk.CTk()
app.title("Checkbox Column")
app.geometry("560x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {"key": "approved", "title": "Approved", "width": 130, "type": "checkbox"},
]

rows = [
    {"approved": True},
    {"approved": False},
]


def handle_checkbox(event: TableRowEvent) -> None:
    assert event.column_key is not None
    approved = event.row[event.column_key]


table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    on_checkbox_toggle=handle_checkbox,
)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

Builder methods for checkbox columns:

Method Parameters What it sets
Column("approved").checkbox(editable=True, validator=None, on_change=None) Keyword-only editing settings Sets type="checkbox" and its editing hooks.

Adjustable options for checkbox columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "checkbox" Draws a clickable checkbox.
align "center" "left", "center", or "right" Alignment. Checkbox columns default to center alignment.
editable True True or False Whether a user can toggle the value.
validator None CellEditRequest -> str or None Rejects a proposed toggle by returning an error message.
on_change None CellChangeEvent -> None Runs after the committed row and view are updated.

Combobox

Combobox columns keep a table-style dropdown arrow visible in every cell and open a themed, scrollable popup powered by CTkScrollableDropdownPP. Search turns on automatically when a column has more than ten displayed choices; set searchable=True or False to override that behaviour. Selecting an option updates the in-memory row; your application decides when and how to save that row to a database.

Plain strings use the same text for display and storage. ComboOption(label, value) separates a friendly label from a hashable stored value, so the table can display "In progress" while retaining "in_progress", an integer foreign key, or another database-ready value. Use either values or options in a dictionary definition, not both.

Click the arrow to choose an option. Use dropdown_height and dropdown_width to size the popup and items_per_page to paginate very long searchable lists. With allow_empty=True, Delete, Backspace, or the labelled empty option stores empty_value; customize its text with empty_label. The stored value defaults to None, which most Python database drivers map naturally to SQL NULL. With allow_custom=True, click the text area to type: Enter or clicking elsewhere commits the text, while Escape cancels it. Existing values outside the configured choices remain visible and unchanged until the user replaces them. The dropdown dependency is installed automatically with CTkDataTable.

import customtkinter as ctk

from CTkDataTable import CellChangeEvent, ComboOption, CTkDataTable


app = ctk.CTk()
app.geometry("680x300")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)


def status_changed(event: CellChangeEvent) -> None:
    print(event.row_id, event.old_value, "->", event.new_value)


columns = [
    {"key": "id", "title": "ID", "width": 80, "type": "number"},
    {
        "key": "status",
        "title": "Status",
        "width": 190,
        "type": "combobox",
        "options": [
            ComboOption("Pending", "pending"),
            ComboOption("Active", "active"),
            ComboOption("Complete", "complete"),
        ],
        "allow_custom": False,
        "allow_empty": True,
        "empty_value": None,
        "empty_label": "No status",
        "dropdown_height": 300,
        "dropdown_width": 240,
        "searchable": True,
        "items_per_page": 25,
        "on_change": status_changed,
    },
]

rows = [
    {"id": 101, "status": "pending"},
    {"id": 102, "status": "Legacy"},
]

table = CTkDataTable(app, columns=columns, data=rows, row_key="id")
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)


def save_to_database() -> None:
    if not table.commit_edit():
        print("Cannot save:", table.edit_validation_error)
        return

    current_rows = table.get_data()
    # Run parameterized UPDATE statements using row["id"], then commit.


save_button = ctk.CTkButton(app, text="Save", command=save_to_database)
save_button.grid(row=1, column=0, pady=(0, 16))

app.mainloop()

get_data() does not commit an active text editor. The explicit commit_edit() above gives Save a predictable boundary and lets validation stop the database operation. row_key="id" puts the stable database identity in event.row_id; do not use a source or view position as a database key because positions can change.

The widget does not open database connections, run SQL, or own transactions. Use parameterized statements in application code. If a save fails, roll back there and either keep the edited rows for retry or reload authoritative rows with set_data().

Builder method and adjustable options:

Method or option Default What it changes
Column("status").combobox(values, *, allow_custom=False, allow_empty=False, empty_value=None, empty_label="— None —", dropdown_height=280, dropdown_width=None, searchable=None, items_per_page=50, editable=True, validator=None, on_change=None) Required values Sets type="combobox", popup behaviour, and editing options.
values or options Required Ordered strings or ComboOption objects. Dictionary definitions accept either name; direct TableColumn uses options.
allow_custom False Allows text values outside values.
allow_empty False Adds a labelled empty option and permits blank custom text.
empty_value None Value stored for a blank selection; use None for SQL NULL.
empty_label "— None —" Text displayed for the empty choice; it must not duplicate an option label.
dropdown_height 280 Maximum popup height in logical pixels; short lists shrink to fit. Minimum 120.
dropdown_width None Popup width in logical pixels; None uses at least the cell width or 220.
searchable None None enables search for more than ten choices; True or False overrides it.
items_per_page 50 Maximum choices per searchable page.
editable True Set False to display the dropdown styling without allowing user edits.
validator None Receives CellEditRequest; return None to accept or an error string to reject.
on_change None Receives CellChangeEvent after a successful change.

A combobox requires at least one non-empty string or ComboOption, even when allow_empty=True. Option labels act as dropdown keys: the same label cannot represent different stored values. Repeated stored values keep the first option. empty_label must be non-blank and distinct from every option label.

Progress

Progress columns draw a numeric value as a progress bar.

Values outside progress_min/progress_max are clamped to the track visually; the stored row value is not changed.

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Progress Column")
app.geometry("620x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {
        "key": "completion",
        "title": "Complete",
        "width": 180,
        "type": "progress",
        "progress_min": 0,
        "progress_max": 100,
        "progress_color": "#2563eb",
        "progress_background_color": "#dbeafe",
        "progress_show_text": True,
        "progress_text_format": "{percent:.0f}%",
    },
]

rows = [
    {"completion": 72},
    {"completion": 35},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

Builder methods for progress columns:

Method Parameters What it sets
Column("completion").progress(minimum=0.0, maximum=100.0, color=None, background_color=None, show_text=True, text_format="{percent:.0f}%") Keyword-only progress settings Sets type="progress" and progress bar options.

Adjustable options for progress columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "progress" Draws a progress bar and sorts numerically.
align "center" "left", "center", or "right" Alignment. Progress columns default to center alignment.
progress_min 0.0 Number less than progress_max Minimum value for the bar.
progress_max 100.0 Number greater than progress_min Maximum value for the bar.
progress_color None Color string or light/dark tuple Progress fill color.
progress_background_color None Color string or light/dark tuple Progress track color.
progress_show_text True True or False Whether formatted progress text is drawn beside the bar.
progress_text_format "{percent:.0f}%" Format string using value, minimum, maximum, min, max, percent, and ratio Progress text.

Link columns draw clickable text. The underline appears on hover, press, or keyboard focus, and a click calls on_link_click with a TableRowEvent when that callback is configured.

import customtkinter as ctk

from CTkDataTable import CTkDataTable, TableRowEvent


app = ctk.CTk()
app.title("Link Column")
app.geometry("620x300")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(1, weight=1)

message = ctk.CTkLabel(app, text="Click a profile link", anchor="w")
message.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 8))

columns = [
    {"key": "name", "title": "Customer", "width": 220},
    {"key": "profile", "title": "Profile", "width": 140, "type": "link", "link_color": "#2563eb"},
]

rows = [
    {"name": "Northwind Components", "profile": "Open profile"},
    {"name": "Meridian Foods", "profile": "Open profile"},
]


def handle_link(event: TableRowEvent) -> None:
    message.configure(text=f"Link clicked for {event.row['name']}")


table = CTkDataTable(app, columns=columns, data=rows, on_link_click=handle_link)
table.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 16))

app.mainloop()

Builder methods for link columns:

Method Parameters What it sets
Column("profile").link(color=None) Optional color string or light/dark tuple Sets type="link" and optional link_color.

Adjustable options for link columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "link" Draws clickable text, underlined on hover, press, or keyboard focus.
link_color None Color string or light/dark tuple Link text and underline color.

Pill List

Pill list columns display tags from a list, tuple, set, frozenset, comma-separated string, or single value.

import customtkinter as ctk

from CTkDataTable import CTkDataTable


app = ctk.CTk()
app.title("Pill List Column")
app.geometry("640x260")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(0, weight=1)

columns = [
    {
        "key": "tags",
        "title": "Tags",
        "width": 240,
        "type": "pill_list",
        "pill_colors": {"Urgent": "#ef4444", "Finance": "#0ea5e9"},
        "pill_fallback_color": "#64748b",
        "pill_text_color": "#ffffff",
    },
]

rows = [
    {"tags": ["Urgent", "Finance"]},
    {"tags": "Sales, Follow-up"},
]

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=0, column=0, sticky="nsew", padx=16, pady=16)

app.mainloop()

Builder methods for pill list columns:

Method Parameters What it sets
Column("tags").pill_list(colors=None, fallback_color=None, text_color=None) Optional color mapping, fallback color, and text color Sets type="pill_list" and pill styling options.

Adjustable options for pill list columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "pill_list" Draws values as compact tag pills.
pill_colors {} Mapping of pill text to color string or light/dark tuple Fill color for known pill values.
pill_fallback_color None Color string or light/dark tuple Fill color for pill values not found in pill_colors.
pill_text_color None Color string or light/dark tuple Text color for every pill in the column.

Action

Action columns draw row-level buttons. A click calls on_action_click with event.action_key.

import customtkinter as ctk

from CTkDataTable import CTkDataTable, TableRowEvent


app = ctk.CTk()
app.title("Action Column")
app.geometry("720x300")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(1, weight=1)

message = ctk.CTkLabel(app, text="Click an action", anchor="w")
message.grid(row=0, column=0, sticky="ew", padx=16, pady=(16, 8))

columns = [
    {"key": "id", "title": "ID", "width": 80, "type": "number"},
    {"key": "name", "title": "Customer", "width": 220},
    {
        "key": "actions",
        "title": "Actions",
        "width": 180,
        "type": "action",
        "sortable": False,
        "actions": [
            {"key": "view", "label": "View"},
            {"key": "delete", "label": "Delete", "fg_color": "#fee2e2", "text_color": "#991b1b"},
        ],
    },
]

rows = [
    {"id": 1, "name": "Northwind Components", "actions": None},
    {"id": 2, "name": "Meridian Foods", "actions": None},
]


def handle_action(event: TableRowEvent) -> None:
    message.configure(text=f"{event.action_key} clicked for {event.row['name']}")


table = CTkDataTable(app, columns=columns, data=rows, on_action_click=handle_action)
table.grid(row=1, column=0, sticky="nsew", padx=16, pady=(0, 16))

app.mainloop()

Builder methods for action columns:

Method Parameters What it sets
Column("actions").action(buttons, sortable=False) Sequence of action strings, dictionaries, or TableAction objects Sets type="action", actions, and sortable.

Adjustable options for action columns:

Option Default Accepted values What it changes
Common options See common options table See common options table Key, title, width, alignment, visibility, sorting, formatting, and metadata.
type "text" "action" Draws row-level action buttons.
align "center" "left", "center", or "right" Alignment. Action columns default to center alignment.
sortable True for dictionary columns; False from Column(...).action() unless changed True or False Whether header clicks sort this action column. Usually set to False.
actions () Sequence of TableAction, mapping, or string actions Buttons drawn in each row.

Major Features

Built-In Search Entry

Set searchable=True to add a search entry above the table. Use search_delay_ms to debounce typing.

table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    searchable=True,
    search_delay_ms=150,
    on_search=lambda query: None,
)
table.grid(row=0, column=0, sticky="nsew")

Searching is case-insensitive. It checks visible, non-action columns and combines with every active column filter. Comboboxes are searched by their displayed labels; other columns use their stored values. A display-only formatter does not change search matching.

External Search Entry

Use an external CTkEntry when you want the search box in your own toolbar.

search = ctk.CTkEntry(app, placeholder_text="Search customers")
search.grid(row=0, column=0, sticky="ew")

table = CTkDataTable(app, columns=columns, data=rows)
table.grid(row=1, column=0, sticky="nsew")

search.bind("<KeyRelease>", lambda _event: table.search(search.get()))

Sorting

Header clicks sort sortable columns. Clicking the same header again toggles direction. Use sort_by() from code when your app decides the sort order.

def report_sort(column_key: str, ascending: bool) -> None:
    sort_state = (column_key, ascending)


table = CTkDataTable(app, columns=columns, data=rows, on_sort=report_sort)
table.sort_by("amount", ascending=False)

Missing values sort last. number, percentage, currency, and progress columns sort numerically. date and datetime columns sort by parsed date values. checkbox columns sort by boolean value. Other columns sort case-insensitively as text.

Column Filters

Column filters combine with global search. A filtered column shows a header indicator.

table.set_column_filter("status", {"type": "equals", "value": "Open"})
table.set_column_filter("amount", {"type": "range", "min": 100, "max": 500})
table.set_column_filter("due", {"type": "date_range", "min": "2026-06-01", "max": "2026-06-30"})

active_filters = table.get_column_filters()

table.clear_column_filter("status")
table.clear_column_filters()

Supported mapping filter definitions:

Type Definition Match behavior
contains {"type": "contains", "value": "north"} Case-insensitive substring match.
equals {"type": "equals", "value": "Open"} Exact Python equality.
not_equals {"type": "not_equals", "value": "Closed"} Exact Python inequality.
in {"type": "in", "values": ["Open", "Paused"]} Value is in a non-string iterable.
bool {"type": "bool", "value": True} bool(value) matches the expected boolean.
range {"type": "range", "min": 100, "max": 500} Numeric value is within the optional inclusive min and max bounds. Commas in numeric strings are ignored.
date_range {"type": "date_range", "min": "2026-06-01", "max": "2026-06-30"} Parsed date or datetime is within the optional inclusive bounds.

You can also pass a callable:

table.set_column_filter(
    "amount",
    lambda value, row: value is not None and float(value) >= 1000 and row["status"] == "Open",
)

Filter callables receive the stored cell value and the current internal row mapping. Treat that row as read-only. An unsupported filter type or invalid range bound raises ValueError; a non-mapping, non-callable definition raises TypeError. Applying search or filters removes selections for rows that are no longer visible and can therefore emit on_selection_change.

Selection and Multi-Select

Single selection is enabled by default. A normal click replaces the selection. With multi_select=True, Ctrl-click toggles a row on Windows/X11, Command-click toggles on macOS/Aqua, and Shift-click selects a range on every platform. Set on_selection_change when application state must follow the completed selection rather than individual mouse clicks.

from CTkDataTable import SelectionChangeEvent


def selection_changed(event: SelectionChangeEvent) -> None:
    print("Selected IDs:", event.row_ids)
    print("Added:", event.added_row_ids, "Removed:", event.removed_row_ids)


table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    row_key="id",
    multi_select=True,
    on_selection_change=selection_changed,
)

selected_row = table.get_selected_row()
selected_rows = table.get_selected_rows()
source_indices = table.get_selected_indices()
view_indices = table.get_selected_view_indices()

The event contains read-only row snapshots plus current row_ids, source_indices, and view_indices. It also reports the added and removed source indices and row IDs. Stable IDs are the safest values to carry into application or database logic; view indices describe only the current filtered and sorted display.

The same operations are available from code. add, toggle, and range_select are mutually exclusive and require multi_select=True; notify=False suppresses only the selection callback.

table.select_row(0)                       # replace selection by source index
table.select_row_by_id(42, add=True)      # add an unselected row
table.select_row_by_id(42, toggle=True)   # add or remove it
table.select_row_by_id(51, range_select=True)
table.clear_selection(notify=False)

Keyboard, Focus, Tooltips, and Accessibility

The table canvas participates in Tab focus and shows a focus ring for the focused row. Click the table or Tab to it before using these row-navigation keys:

Key Behavior
Up, Down Move one row.
Page Up, Page Down Move by one page.
Home, End Move to first or last visible row.
Enter Calls on_row_double_click for the focused row.
Shift with movement Extends the selection range in multi-select mode.

For a focused combobox cell that permits empty values, Delete or Backspace stores its configured empty_value. A custom-text combobox editor uses Enter to commit and Escape to cancel. Pointer users receive hover/pressed feedback and pointer cursors on sortable headers, links, actions, checkboxes, and combobox controls.

Tooltips appear only when display content is actually truncated. Checkbox and action cells do not produce text tooltips; pill-list tooltips show the complete comma-separated list. Tooltips are supplemental and should not contain information absent from the underlying row value.

Because rows and controls are drawn on one Tk Canvas rather than created as native widgets, operating-system accessibility tools may expose the table as a canvas instead of a semantic grid with one accessible object per cell. Applications that require full screen-reader grid semantics should provide an alternate accessible view or use a native widget designed for that requirement.

Bulk Actions with Multi-Select

def delete_selected() -> None:
    removed = table.delete_selected_rows()
    deleted_count = removed


button = ctk.CTkButton(app, text="Delete selected", command=delete_selected)
button.grid(row=0, column=0, sticky="w")

table = CTkDataTable(app, columns=columns, data=rows, multi_select=True)
table.grid(row=1, column=0, sticky="nsew")

Row, cell, link, action, context-menu, and the compatibility checkbox callback receive TableRowEvent. Action clicks do not also fire row or cell callbacks. Link clicks fire on_link_click when that callback is set.

Successful checkbox and combobox edits use one consistent pipeline. First the model and filtered/sorted view update. Then the column's on_change receives CellChangeEvent, followed by the table-wide on_cell_change. For checkboxes, the backward-compatible on_checkbox_toggle(TableRowEvent) runs last. Initial data and redraws never emit edit events. update_cell() and update_cell_by_id() emit them only when notify=True, with origin="api"; user edits use origin="user".

For pointer interactions, a changed selection emits on_selection_change before the target callback. A normal cell click then calls on_cell_click followed by on_row_click. Action, link, checkbox, and combobox control clicks are handled as controls and do not also call the normal cell/row callbacks. Double-clicking an ordinary row calls on_row_double_click; Enter invokes the same callback for the focused row. Context-menu items call only on_context_action.

from CTkDataTable import CellChangeEvent, TableRowEvent


def row_clicked(event: TableRowEvent) -> None:
    row_identity = (event.source_index, event.view_index)


def cell_clicked(event: TableRowEvent) -> None:
    selected_column = event.column_key


def action_clicked(event: TableRowEvent) -> None:
    selected_action = event.action_key


def checkbox_toggled(event: TableRowEvent) -> None:
    assert event.column_key is not None
    checked = event.row[event.column_key]


def any_cell_changed(event: CellChangeEvent) -> None:
    print(event.row_id, event.column_key, event.old_value, event.new_value, event.origin)


table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    on_row_click=row_clicked,
    on_cell_click=cell_clicked,
    on_action_click=action_clicked,
    on_cell_change=any_cell_changed,
    on_checkbox_toggle=checkbox_toggled,
)

CellChangeEvent.row is a read-only top-level snapshot taken after the update. If a callback mutates the table again, use the event snapshot for the completed change rather than assuming the widget's current state is still identical.

Validation and Edit Lifecycle

Checkbox and combobox columns accept validator; the table accepts cell_validator for rules shared by every editable column. Both receive CellEditRequest and return None to accept the proposed value or a message string to reject it. Column validation runs before table-wide validation and before the data changes.

from CTkDataTable import CellEditRequest, Column


def validate_status(request: CellEditRequest) -> str | None:
    if request.proposed_value == "closed" and not request.row.get("approved"):
        return "Approve the record before closing it."
    return None


columns = [
    Column("status").combobox(
        ["open", "closed"],
        validator=validate_status,
    )
]

A rejected typed combobox edit stays open and exposes its message through edit_validation_error, allowing the user to correct it. commit_edit() returns False on rejection; cancel_edit() discards the active typed edit. With no active editor, commit_edit() returns True and cancel_edit() returns False. Programmatic update_cell...() calls raise CellValidationError for invalid values. Use is_editing to check whether a custom editor is active.

read_only=True disables all user checkbox and combobox changes for a table, while editable=False disables one editable column. Programmatic cell updates are still available, so application code can refresh values. Use table.set_read_only(True/False) to change the table setting later.

Use update_cell() or update_cell_by_id() when you want cell validators to run. Whole-row methods such as update_row() replace the row directly and do not run cell validators or emit CellChangeEvent.

Context Menus

Pass context_menu to show right-click row actions. On macOS/Aqua or single-button setups, Control-click opens the context menu; Command-click remains the multi-select toggle. Opening a context menu selects its target row normally rather than treating the context-menu modifier as an additive selection modifier.

from CTkDataTable import TableRowEvent


def handle_context(event: TableRowEvent) -> None:
    if event.action_key == "copy_id":
        app.clipboard_clear()
        app.clipboard_append(str(event.row["id"]))


table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    context_menu=[
        {"key": "copy_id", "label": "Copy ID"},
        {"key": "view", "label": "View"},
        "archive",
    ],
    on_context_action=handle_context,
)

Resizable Columns, Fill Width, and Horizontal Scroll

Set resizable_columns=True to let users drag header dividers. Set column_width_mode="fill" when the visible columns should expand or shrink with the table width. Set horizontal_scroll=True when the total minimum column width may be wider than the table.

table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    density="comfortable",
    column_width_mode="fill",
    resizable_columns=True,
    min_column_width=64,
    horizontal_scroll=True,
)

table.set_column_width("name", 260)
name_width = table.get_column_width("name")

When horizontal scrolling is enabled, Shift plus mouse wheel scrolls horizontally.

In "fixed" mode, visible columns keep their configured logical widths; enable horizontal scrolling if their total can exceed the viewport. In "fill" mode, configured widths act as proportions while the table distributes the available width, without taking a visible column below its effective minimum. set_column_width() and drag resizing clamp ordinary columns to min_column_width; action columns may use a larger minimum so their configured buttons remain usable. Hidden columns do not participate in layout.

Use density="compact", "comfortable" (the default), or "spacious" to select coordinated row, header, and footer heights. An explicit row_height, header_height, or footer_height overrides that part of the preset. Scrollbars hide automatically when their content fits; pass autohide_scrollbars=False to keep applicable scrollbars visible.

Truncated display content shows its full value in a tooltip after tooltip_delay_ms (600 milliseconds by default). Set show_tooltips=False to disable these tooltips. Sortable headers, links, action buttons, and editable controls provide automatic hover and pressed feedback, while keyboard focus is shown with a focus ring.

Table Styling

Use style for table-wide colors, spacing, and corner radii. Pass a dictionary or a TableStyle object when creating the table. Later, call configure_style() to merge changes into the current style, or set_style() to replace it.

from CTkDataTable import CTkDataTable, TableStyle


table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    style={
        "surface_bg": "#ffffff",
        "header_bg": "#111827",
        "header_text_color": "#ffffff",
        "row_alt_bg": "#f8fafc",
        "hover_bg": "#e0f2fe",
        "selected_bg": "#2563eb",
        "selected_text_color": "#ffffff",
        "divider_color": "#dbe3ef",
        "border_color": "#cbd5e1",
        "corner_radius": 12,
        "cell_padding_x": 14,
        "badge_radius": 7,
        "checkbox_radius": 4,
        "action_radius": 6,
    },
)

table.configure_style(
    link_text_color="#0f766e",
    checkbox_fill_checked="#16a34a",
    action_bg="#eff6ff",
)

table.set_style(
    TableStyle(
        surface_bg=("#ffffff", "#111827"),
        text_color=("#111827", "#e5e7eb"),
        header_bg=("#f1f5f9", "#020617"),
    )
)

Color options accept normal color strings or CustomTkinter light/dark tuples. Dimension options must be non-negative numbers. style also accepts aliases such as fg_color, text, divider, and action_text; the canonical option names are listed in the TableStyle reference.

Style Hooks

Style hooks are opt-in. Set enable_style_hooks=True, then provide row_style and/or cell_style.

Style callbacks return None or a mapping with:

Key Effect
fg_color Row or cell background color.
text_color Row or cell text color.

Colors can be strings or CustomTkinter light/dark tuples.

def row_style(row):
    if row["status"] == "Overdue":
        return {"fg_color": "#fff7ed", "text_color": "#9a3412"}
    return None


def cell_style(_row, column_key, value):
    if column_key == "amount" and value < 0:
        return {"text_color": "#dc2626"}
    return None


table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    enable_style_hooks=True,
    row_style=row_style,
    cell_style=cell_style,
)

BadgeStyle for Dynamic Badges

Use BadgeStyle when a badge fallback handler needs to set custom text and colors.

from typing import Any, Mapping

from CTkDataTable import BadgeStyle, TableColumn


def status_fallback(value: Any, _row: Mapping[str, Any], _column: TableColumn) -> BadgeStyle:
    text = str(value or "Unknown")
    return BadgeStyle(text=text, fill_color="#64748b", text_color="#ffffff")


columns = [
    {
        "key": "status",
        "title": "Status",
        "width": 140,
        "type": "badge",
        "badge_colors": {"Open": "#22c55e", "Closed": "#64748b"},
        "badge_fallback_handler": status_fallback,
    }
]

Set footer=True and provide summaries. Summaries run against the current visible rows after search and column filters.

table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    footer=True,
    footer_height=38,
    summaries={
        "id": "count",
        "amount": "sum",
        "status": lambda visible_rows: f"{len(visible_rows)} visible",
    },
)

Built-in summary names are count, sum, avg, average, min, and max. count counts visible rows; numeric summaries ignore None, empty, and non-numeric values and accept commas in numeric strings. Summary callbacks receive shallow copies of the current visible rows. Unknown strings are displayed as literal footer text, and callback failures are raised as RuntimeError with the column key.

Loading State

Use set_loading(True) before a reload that you schedule back into the Tkinter event loop.

def reload_rows() -> None:
    table.set_loading(True)
    app.after(50, finish_reload)


def finish_reload() -> None:
    table.set_data(fetch_rows())
    table.set_loading(False)

Async Loading

Use load_async() to run a row loader on a background thread. The widget sets its loading state, runs your function, then safely calls set_data() on the Tkinter thread.

import time


def fetch_rows():
    time.sleep(1)
    return [
        {"id": 1, "name": "Northwind Components", "status": "Open"},
        {"id": 2, "name": "Meridian Foods", "status": "Closed"},
    ]


def loaded(rows):
    loaded_count = len(rows)


def failed(error):
    last_error = error


thread = table.load_async(
    fetch_rows,
    on_success=loaded,
    on_error=failed,
    clear_on_error=False,
)

fetch_rows must be callable; invalid callbacks (including None) raise TypeError before a worker starts. The function then runs on the returned daemon threading.Thread and must not read or update Tkinter widgets. set_data(), on_success, and on_error run later on the Tkinter event-loop thread. on_success receives shallow copies of the committed rows. clear_on_error=True clears existing rows before displaying the error; the default preserves them. If another async load starts before an older one finishes, only the newest result is applied.

Error State

Use set_error() to show an error message without changing rows. Use clear_error() to return to the current data view.

table.set_error("Could not refresh customer data")
table.clear_error()

Constructor Reference

table = CTkDataTable(
    master,
    columns,
    data=None,
    # All remaining CTkDataTable options are keyword-only.
    density="comfortable",
    row_height=None,
    header_height=None,
    footer_height=None,
    font=None,
    header_font=None,
    horizontal_scroll=False,
    autohide_scrollbars=True,
    column_width_mode="fixed",
    multi_select=False,
    row_key=None,
    read_only=False,
    searchable=False,
    search_delay_ms=0,
    resizable_columns=False,
    min_column_width=48,
    style=None,
    enable_style_hooks=False,
    row_style=None,
    cell_style=None,
    context_menu=None,
    on_context_action=None,
    footer=False,
    summaries=None,
    empty_message="No records to display",
    loading_message="Loading records...",
    error_message="Could not load records",
    show_tooltips=True,
    tooltip_delay_ms=600,
    on_row_click=None,
    on_row_double_click=None,
    on_cell_click=None,
    on_action_click=None,
    on_link_click=None,
    on_checkbox_toggle=None,
    on_cell_change=None,
    on_selection_change=None,
    cell_validator=None,
    on_sort=None,
    on_search=None,
    **kwargs,
)

CTkDataTable inherits from customtkinter.CTkFrame. Visual keyword arguments such as corner_radius, border_width, fg_color, and border_color style the rounded table viewport. If omitted, the table sets corner_radius=12 and border_width=1.

Argument Default Accepted values How to apply it
master Required Parent widget such as CTk, CTkFrame, or a tab/container. First positional argument.
columns Required Sequence of dictionaries, TableColumn, or Column objects. CTkDataTable(app, columns=columns).
data None Iterable of row-like mapping objects. None becomes an empty table. Pass at creation or call set_data().
density "comfortable" "compact", "comfortable", or "spacious". Sets row/header/footer heights to 34/36/32, 42/44/38, or 50/52/44. density="spacious".
row_height None None or integer pixels, at least 28. None uses the density preset. row_height=48.
header_height None None or integer pixels, at least 32. None uses the density preset. header_height=46.
footer_height None None or integer pixels, at least 28. None uses the density preset and matters when footer=True. footer_height=40.
font None Tkinter/CustomTkinter font object or tuple accepted by Canvas text items. font=("Segoe UI", 13).
header_font None Font object or tuple. Defaults to font when supplied, otherwise a bold table default. header_font=("Segoe UI", 13, "bold").
horizontal_scroll False True or False. Creates horizontal scrolling support; use it when fixed/minimum widths can exceed the viewport. horizontal_scroll=True.
autohide_scrollbars True True or False. Hides the vertical and optional horizontal scrollbar whenever its content fits. autohide_scrollbars=False.
column_width_mode "fixed" "fixed" or "fill". Fixed uses configured widths; fill distributes the visible table width across columns. column_width_mode="fill".
multi_select False True or False. Enables Ctrl-click toggle on Windows/X11, Command-click toggle on macOS/Aqua, additive API selection, and Shift/API range selection. multi_select=True.
row_key None Non-empty row field name or None. When set, every row needs a unique hashable value at that key. row_key="customer_id".
read_only False True or False. Disables user editing for all checkbox and combobox columns. read_only=True.
searchable False True or False. searchable=True.
search_delay_ms 0 Integer milliseconds, 0 or greater. search_delay_ms=150.
resizable_columns False True or False. resizable_columns=True.
min_column_width 48 Integer pixels, at least 24. min_column_width=64.
style None TableStyle, mapping, or None. Controls table-wide colors, spacing, and radii. style={"header_bg": "#111827"}.
enable_style_hooks False True or False. Required for row_style and cell_style. enable_style_hooks=True.
row_style None Callable (row) -> mapping or None. Supports fg_color and text_color. row_style=my_row_style.
cell_style None Callable (row, column_key, value) -> mapping or None. Supports fg_color and text_color. cell_style=my_cell_style.
context_menu None Sequence of TableAction, mapping, or string actions. context_menu=[{"key": "copy", "label": "Copy"}].
on_context_action None Callable (event: TableRowEvent) -> None. on_context_action=handle_context.
footer False True or False. footer=True.
summaries None Mapping of column key to summary name or callback. summaries={"amount": "sum"}.
empty_message "No records to display" String. empty_message="No customers yet".
loading_message "Loading records..." String. loading_message="Loading customers...".
error_message "Could not load records" String. Used by set_error(None) and load_async() failures. error_message="Load failed".
show_tooltips True True or False. Shows full display content when supported cells are visually truncated; excludes checkbox and action cells. show_tooltips=False.
tooltip_delay_ms 600 Integer milliseconds, 0 or greater. tooltip_delay_ms=350.
on_row_click None Callable (event: TableRowEvent) -> None. on_row_click=select_row.
on_row_double_click None Callable (event: TableRowEvent) -> None. Also called by Enter. on_row_double_click=open_row.
on_cell_click None Callable (event: TableRowEvent) -> None. on_cell_click=inspect_cell.
on_action_click None Callable (event: TableRowEvent) -> None. on_action_click=handle_action.
on_link_click None Callable (event: TableRowEvent) -> None. on_link_click=open_link.
on_checkbox_toggle None Callable (event: TableRowEvent) -> None. Fires after a checkbox cell toggles. on_checkbox_toggle=handle_checkbox.
on_cell_change None Callable (event: CellChangeEvent) -> None. Runs after the column callback for every successful notified edit. on_cell_change=track_change.
on_selection_change None Callable (event: SelectionChangeEvent) -> None. Runs after selection actually changes. on_selection_change=sync_selection.
cell_validator None Callable (request: CellEditRequest) -> str or None. Shared validation after column validation. cell_validator=validate_edit.
on_sort None Callable (column_key: str, ascending: bool) -> None. on_sort=report_sort.
on_search None Callable (query: str) -> None. Runs after built-in or programmatic global search is applied. on_search=report_search.
**kwargs Frame defaults plus table defaults CustomTkinter CTkFrame options such as fg_color, border_color, corner_radius, border_width. fg_color=("white", "#1f2937").

Public Imports and Typing Helpers

The package root deliberately exports the widget, its configuration/event classes, rows_from_cursor, and the following typing helpers. Import from CTkDataTable rather than private module paths.

Export Runtime/type definition Purpose
CTkDataTable customtkinter.CTkFrame subclass The table widget.
TableColumn Frozen dataclass Fully normalized column configuration.
Column Fluent Mapping[str, Any] builder Builds a column definition through chained methods.
ColumnDefinition `TableColumn Column
ColumnType Literal "text", "number", "percentage", "currency", "date", "datetime", "badge", "checkbox", "action", "progress", "link", "pill_list", or "combobox" Valid column type annotation.
ColumnAlign Literal "left", "center", or "right" Valid column alignment annotation.
TableDensity Literal "compact", "comfortable", or "spacious" Constructor density annotation.
ColorValue `str tuple[str, str]`
TableStyle Frozen dataclass Optional table-wide shape, spacing, and color overrides.
TableAction Frozen dataclass Row action button or context-menu action definition.
BadgeStyle Frozen dataclass Resolved dynamic badge text/fill/text-color result.
ComboOption Frozen, slotted dataclass One combobox display label and stored hashable value.
TableRowEvent Frozen dataclass Row/cell/link/action/context callback payload.
CellEditRequest Frozen, slotted dataclass Proposed edit supplied to validators.
CellChangeEvent Frozen, slotted dataclass Successfully committed cell-change payload.
SelectionChangeEvent Frozen, slotted dataclass Completed selection-change payload.
CellValidationError ValueError subclass Invalid programmatic cell edit, with message, column_key, and value.
EventOrigin Literal "user" or "api" Origin field used by edit and selection events.
RowSnapshot Mapping[str, Any] Read-only top-level row copy used by structured events.
CellValidator `Callable[[CellEditRequest], str None]`
CellChangeCallback Callable[[CellChangeEvent], None] Column/table cell-change callback annotation.
ComboboxChangeCallback Alias of CellChangeCallback Backward-compatible name; prefer CellChangeCallback in new code.
SelectionChangeCallback Callable[[SelectionChangeEvent], None] Selection callback annotation.
rows_from_cursor Function Converts a fetched DB-API cursor result to row dictionaries.

TableStyle Reference

Use a TableStyle object or mapping at construction. Every field defaults to None, meaning the table derives a suitable value from the active CustomTkinter theme or the renderer default. A color is a ColorValue: either one Tk-compatible color string or a (light_mode_color, dark_mode_color) tuple.

TableStyle is frozen. get_style() returns the current object, set_style() replaces the entire style, and configure_style() merges supplied fields into it. Keyword fields override the same fields in a mapping/object passed during that call. Passing None for a field through configure_style() clears that override; calling set_style() with no fields clears every TableStyle override. Active theme values are then used, with any explicit top-level fg_color, border_color, corner_radius, or border_width retained as the relevant frame fallback.

table.set_style(TableStyle(header_bg=("#f8fafc", "#111827")))
table.configure_style(header_text_color=("#111827", "#f8fafc"))
table.configure_style(header_bg=None)  # restore the theme-derived header fill

Style mappings reject unknown names with ValueError. Dimension fields must be numeric and non-negative. Color strings are resolved by Tk/CustomTkinter when drawn, so use valid Tk color names or hex colors. Inside a style mapping, fg_color is an alias for canvas_bg; a top-level constructor fg_color is a CTkFrame option. Prefer canonical style names when both APIs are involved.

Style resolution is intentionally layered:

  1. The active CustomTkinter appearance mode and theme provide base colors.
  2. TableStyle overrides those table-wide values, including overlapping top-level table frame colors and shape values.
  3. Column feature options such as link_color, progress_color, badge/pill mappings, and per-TableAction colors override the matching normal-state feature defaults.
  4. Hover, pressed, selected, and keyboard-focus states apply their state colors. Row hooks affect normal rows; selection and hover take priority over row-hook colors.
  5. A cell hook's fg_color or text_color is the most local cell override. It does not replace the internal fill of a checkbox, progress bar, badge, pill, or action button unless that feature exposes its own color option.

Badge and pill labels automatically choose black or white for the stronger contrast against valid Tk fill colors while their table-wide text field remains None. An explicit cell text color, BadgeStyle.text_color, TableColumn.pill_text_color, or table-wide badge/pill text color disables that automatic choice at the applicable level.

Shape and spacing fields:

Option Accepted value None fallback and effect
corner_radius Number >= 0 12; outer table viewport radius.
border_width Number >= 0 1; outer table viewport border width.
cell_padding_x Number >= 0 12; horizontal inset for header, body, and footer content.
badge_padding_x Number >= 0 10; horizontal space around badge text.
button_padding_x Number >= 0 12; horizontal space used when auto-sizing action labels.
badge_radius Number >= 0 Half the badge height; badge corner radius.
checkbox_radius Number >= 0 4; checkbox corner radius.
progress_radius Number >= 0 Half the progress-track height; progress corner radius.
pill_radius Number >= 0 Half the pill height; pill corner radius.
action_radius Number >= 0 5; action-button corner radius.

Table surface and row-state colors:

Option Accepted value Theme-derived fallback and effect
canvas_bg ColorValue Surrounding/container color behind the rounded surface.
surface_bg ColorValue Table viewport surface.
row_bg ColorValue Normal even-row background.
row_alt_bg ColorValue Alternating odd-row background.
header_bg ColorValue Normal header background.
header_hover_bg ColorValue Hovered sortable-header background.
header_pressed_bg ColorValue Pressed sortable-header background.
footer_bg ColorValue Summary-footer background.
hover_bg ColorValue Hovered, unselected row background.
selected_bg ColorValue Selected row background.
selected_hover_bg ColorValue Selected row background while hovered.

Text, line, and indicator colors:

Option Accepted value Theme-derived fallback and effect
text_color ColorValue Normal body text.
hover_text_color ColorValue Body text on a hovered row.
selected_text_color ColorValue Body text on a selected row.
selected_hover_text_color ColorValue Body text on a selected and hovered row.
muted_text_color ColorValue Empty/loading messages and secondary text.
header_text_color ColorValue Header labels.
footer_text_color ColorValue Summary-footer text.
divider_color ColorValue Horizontal row dividers.
header_divider_color ColorValue Header column dividers and control outlines.
border_color ColorValue Outer rounded table border.
sort_indicator_color ColorValue Active sort arrow and hover sort hint.
filter_indicator_color ColorValue Active column-filter marker.
focus_ring_color ColorValue Keyboard-focused row/link outline.
loading_indicator_color ColorValue Loading-state spinner/indicator.

Typed-cell and interaction colors:

Option Accepted value Theme-derived fallback and effect
badge_bg ColorValue Badge fill when no column mapping/fallback supplies one.
badge_text_color ColorValue Badge text; None enables per-fill black/white auto contrast.
pill_bg ColorValue Pill fill when no column mapping/fallback supplies one, including +N overflow pills.
pill_text_color ColorValue Pill text; None enables per-fill black/white auto contrast.
progress_bg ColorValue Default progress-track fill.
progress_fill ColorValue Default completed progress fill.
progress_text_color ColorValue Formatted value drawn beside the progress track.
link_text_color ColorValue Normal link text/underline color when the column has no link_color.
link_hover_text_color ColorValue Hovered or pressed link text/underline color.
checkbox_fill ColorValue Unchecked checkbox fill.
checkbox_fill_checked ColorValue Checked checkbox fill.
checkbox_border ColorValue Normal checkbox border.
checkbox_check ColorValue Check-mark stroke.
action_bg ColorValue Normal action-button fill when an action has no fg_color.
action_hover_bg ColorValue Hovered action fill; a custom per-action fill receives its own derived hover shade.
action_pressed_bg ColorValue Pressed action fill; a custom per-action fill receives its own derived pressed shade.
action_border ColorValue Action-button border when an action has no border_color.
action_text_color ColorValue Action label when an action has no text_color.
control_hover_bg ColorValue Hover feedback behind checkbox and combobox controls.
control_pressed_bg ColorValue Pressed feedback behind checkbox and combobox controls.
control_focus_border_color ColorValue Keyboard-focus border for checkbox and combobox controls.

Message and tooltip colors:

Option Accepted value Theme-derived fallback and effect
error_text_color ColorValue Error-state message.
tooltip_bg ColorValue Truncation-tooltip background.
tooltip_text_color ColorValue Truncation-tooltip text.
tooltip_border_color ColorValue Truncation-tooltip border.

Supported alias names:

Alias Canonical option
fg_color canvas_bg
text, hover_text, selected_text, selected_hover_text, muted_text, header_text, footer_text Matching *_color option.
divider, header_divider, table_border divider_color, header_divider_color, border_color.
sort_indicator, filter_indicator, loading_indicator Matching *_color option.
badge_default_bg, badge_text, pill_text, progress_text, link_text, action_text Matching feature-cell color option.
focus_ring, link_hover_text, control_focus_border, error_text, tooltip_text, tooltip_border Matching focus, interaction, message, or tooltip color option.

TableColumn Reference

Dictionary, Column, and TableColumn definitions normalize to TableColumn.

Option Default Accepted values How to apply it
key Required Unique string row field name. Must match row dictionary keys exactly. {"key": "name"}.
title Key title-cased for dictionaries and Column; required for direct TableColumn. String header label. {"title": "Customer"}.
width 140 for dictionaries and Column; required for direct TableColumn. Integer logical pixels greater than 0. {"width": 220}.
align Depends on type. "left", "center", or "right". {"align": "right"}.
visible True True or False. Hidden columns are not rendered or searched. {"visible": False}.
sortable True True or False. Controls header-click sorting. {"sortable": False}.
type "text" One of the 13 column types. {"type": "currency"}.
formatter None Callable (value, row) -> str. Its returned text replaces built-in type formatting. {"formatter": lambda value, row: str(value).upper()}.
number_format None Format string using .format(number) or callable (value) -> str. {"number_format": "{:,.2f}"}.
percentage_format "{value:.0f}%" Format string using value, raw_value, and multiplier; positional {} also receives the display value. {"percentage_format": "{value:.1f}%"}.
percentage_multiplier 1.0 Number greater than 0. {"percentage_multiplier": 100}.
currency_symbol "$" String prefix or symbol. {"currency_symbol": "GBP "}.
currency_format "{symbol}{value:,.2f}" Format string using symbol, value, and signed_value. {"currency_format": "{symbol}{value:,.0f}"}.
currency_negative_format "-{symbol}{value:,.2f}" Format string using absolute value, plus symbol and signed_value. {"currency_negative_format": "({symbol}{value:,.2f})"}.
date_format "%Y-%m-%d" strftime format string. {"date_format": "%d %b %Y"}.
datetime_format "%Y-%m-%d %H:%M" strftime format string. {"datetime_format": "%d %b %H:%M"}.
badge_colors {} Mapping of displayed badge text to color string or light/dark tuple. {"badge_colors": {"Open": "#22c55e"}}.
badge_fallback_color None Color string or light/dark tuple. {"badge_fallback_color": "#64748b"}.
badge_fallback_handler None Callable (value, row, column) -> BadgeStyle, color, or None. {"badge_fallback_handler": status_fallback}.
pill_colors {} Mapping of pill text to color string or light/dark tuple. {"pill_colors": {"Urgent": "#ef4444"}}.
pill_fallback_color None Color string or light/dark tuple. {"pill_fallback_color": "#64748b"}.
pill_text_color None Color string or light/dark tuple. {"pill_text_color": "#ffffff"}.
actions () Sequence of TableAction, mapping, or string actions. {"actions": [{"key": "view", "label": "View"}]}.
progress_min 0.0 Number. Must be less than progress_max. {"progress_min": 0}.
progress_max 100.0 Number. Must be greater than progress_min. {"progress_max": 100}.
progress_color None Color string or light/dark tuple. {"progress_color": "#2563eb"}.
progress_background_color None Color string or light/dark tuple. {"progress_background_color": "#dbeafe"}.
progress_show_text True True or False. {"progress_show_text": False}.
progress_text_format "{percent:.0f}%" Format string using value, minimum, maximum, min, max, percent, and ratio. {"progress_text_format": "{value:.0f}/{maximum:.0f}"}.
link_color None Color string or light/dark tuple. {"link_color": "#2563eb"}.
options or values () Ordered strings or ComboOption(label, value) objects. Use one name only; direct TableColumn uses options. {"options": [ComboOption("Open", 1)]}.
allow_custom False True or False. Used by combobox. {"allow_custom": True}.
allow_empty False True or False. Used by combobox. {"allow_empty": True}.
empty_value None Hashable stored value used for a blank combobox choice. {"empty_value": None}.
empty_label "— None —" Non-blank display label for the empty combobox choice. {"empty_label": "No status"}.
dropdown_height 280 Integer at least 120; maximum popup height in logical pixels. {"dropdown_height": 320}.
dropdown_width None None or an integer at least 120; popup width in logical pixels. {"dropdown_width": 260}.
searchable None None, True, or False; automatic, forced-on, or forced-off search. {"searchable": True}.
items_per_page 50 Positive integer page size for searchable dropdowns. {"items_per_page": 25}.
editable True for checkbox/combobox; otherwise forced False True or False. {"editable": False}.
validator None Callable (request: CellEditRequest) -> str or None. {"validator": validate_status}.
on_change None Callable (event: CellChangeEvent) -> None. {"on_change": status_changed}.
metadata {} Mapping for your own app-specific data. Not rendered. {"metadata": {"source": "crm"}}.

Default alignment by column type:

Type Default alignment
number, percentage, currency right
checkbox, action, progress center
All other types left

Those type-aware defaults apply to dictionary and Column definitions. Direct TableColumn(...) construction uses its dataclass default align="left", so set align="center" explicitly for a direct checkbox/action/progress column when desired. For numeric, percentage, and currency columns, align="left" is rendered as right alignment; use align="center" for the only non-right numeric layout. The header, body, and summary always share the resulting alignment.

TableColumn.values is a read-only convenience property returning the tuple of stored values from normalized combobox options. TableColumn.from_definition(definition) returns an existing TableColumn unchanged or normalizes one mapping; applications can usually let CTkDataTable perform that normalization automatically.

Column Builder Reference

Column("key") creates a mapping accepted anywhere a column dictionary is accepted.

Method Parameters Effect
Column(key) key: str Starts a text column with the given row key.
.title(title) title: str Sets header text.
.width(logical_pixels) logical_pixels: int Sets preferred column width.
.align(align) "left", "center", "right" Sets alignment.
.hide() None Sets visible=False.
.no_sort() None Sets sortable=False.
.fmt(func) Callable (value, row) -> str Sets formatter.
.metadata(**kwargs) Keyword values Sets metadata.
.text() None Sets type="text".
.number(format=None) Format string or callable Sets type="number" and optional number_format.
.percentage(format="{value:.0f}%", multiplier=1.0) Keyword-only format string and display multiplier Sets percentage options.
.currency(symbol="$", format="{symbol}{value:,.2f}", negative_format="-{symbol}{value:,.2f}") Keyword-only formatting options Sets currency options.
.date(fmt="%Y-%m-%d") strftime format Sets date options.
.datetime(fmt="%Y-%m-%d %H:%M") strftime format Sets datetime options.
.badge(colors=None, fallback_color=None, fallback_handler=None) Badge settings Sets badge options.
.pill_list(colors=None, fallback_color=None, text_color=None) Pill settings Sets pill-list options.
.checkbox(editable=True, validator=None, on_change=None) Keyword-only editing settings Sets checkbox editing options.
.combobox(values, *, allow_custom=False, allow_empty=False, empty_value=None, empty_label="— None —", dropdown_height=280, dropdown_width=None, searchable=None, items_per_page=50, editable=True, validator=None, on_change=None) Strings or ComboOption objects plus popup and editing settings Sets combobox options and its scrollable popup.
.progress(minimum=0.0, maximum=100.0, color=None, background_color=None, show_text=True, text_format="{percent:.0f}%") Progress settings Sets progress options.
.link(color=None) Optional color Sets link options.
.action(buttons, sortable=False) Sequence of actions Sets action button options.

TableAction Reference

TableAction defines a button inside an action column or an item in context_menu.

from CTkDataTable import TableAction


TableAction("view", "View")
TableAction("delete", "Delete", fg_color="#fee2e2", text_color="#991b1b")

Dictionary and string forms are accepted:

{"key": "view", "label": "View", "width": 72}
"archive"

String actions use the string as key and title-case it for the label.

Native context menus use only key and label. width and color fields apply to Canvas-rendered action-column buttons, not operating-system menu items.

TableAction.from_definition(definition) performs the same normalization for one existing TableAction, mapping, or string. It is mainly useful when application code builds reusable action registries; normal column/context-menu inputs are normalized automatically.

Option Default Accepted values How to apply it
key Required String action identifier. {"key": "view"}.
label key.title() for dictionaries and strings; required for direct TableAction. Button/menu text. {"label": "View"}.
width None Positive integer logical pixels. If omitted, the button is measured from the label. {"width": 76}.
fg_color None Color string or light/dark tuple. {"fg_color": "#fee2e2"}.
text_color None Color string or light/dark tuple. {"text_color": "#991b1b"}.
border_color None Color string or light/dark tuple. {"border_color": "#fecaca"}.

BadgeStyle Reference

Use BadgeStyle from a badge_fallback_handler.

BadgeStyle(text="Unknown", fill_color="#64748b", text_color="#ffffff")
Field Default Accepted values Effect
text Required String. Text drawn inside the badge.
fill_color Required Color string or light/dark tuple. Badge fill color.
text_color None Color string, light/dark tuple, or None. Badge text color.

Fallback handler return values:

Return value Result
BadgeStyle(...) Uses custom badge text, fill color, and optional text color.
Color string or tuple Keeps the original cell text and uses the returned fill color.
None Uses badge_fallback_color, then the table default if no fallback color exists.

ComboOption Reference

Use ComboOption when displayed text should differ from the stored value:

ComboOption(label="In progress", value="in_progress")
ComboOption(label="Priority customer", value=7)

Both label and value participate in combobox input matching, but the row stores value. Labels must be unique strings because they are the dropdown's user-facing selection key, and values must be hashable. If stored values repeat, the first option is retained.

CellEditRequest and Validation Reference

Column and table validators receive CellEditRequest before data changes.

Attribute Type Meaning
widget CTkDataTable Table proposing the edit.
row_id Hashable or None Stable ID when row_key is configured.
row Mapping[str, Any] Read-only top-level snapshot before the edit.
source_index int Current source position.
view_index int or None Current visible position, or None when filtered out.
column_key str Field being edited.
old_value Any Current stored value.
proposed_value Any Normalized value to validate.
origin "user" or "api" Where the edit came from.

Return None to accept or a string to reject. Invalid programmatic edits raise CellValidationError, whose public fields are message, column_key, and value.

The row mapping is a read-only top-level copy: keys cannot be assigned through the event, but nested mutable values are not deep-copied. Treat nested values as read-only as well.

CellChangeEvent Reference

Column on_change and table on_cell_change callbacks receive CellChangeEvent after the committed model and view update.

Attribute Type Meaning
widget CTkDataTable Table that completed the change.
row_id Hashable or None Stable ID when row_key is configured.
row Mapping[str, Any] Read-only top-level snapshot after the change.
source_index int Source position after the change.
view_index_before int or None Visible position before the change.
view_index_after int or None Visible position after reapplying sort/filter rules.
column_key str Changed field.
old_value Any Previous stored value.
new_value Any New stored value.
origin "user" or "api" User interaction or a notified API update.

As with CellEditRequest, row is a read-only top-level snapshot rather than a recursive deep copy.

SelectionChangeEvent Reference

on_selection_change receives a completed SelectionChangeEvent only when the selection actually changes.

Attribute Type Meaning
widget CTkDataTable Table whose selection changed.
rows tuple[Mapping, ...] Current read-only row snapshots.
added_rows tuple[Mapping, ...] Read-only snapshots newly added by this operation.
removed_rows tuple[Mapping, ...] Read-only snapshots removed by this operation.
row_ids tuple[Hashable or None, ...] Current stable IDs, or None entries without row_key.
source_indices tuple[int, ...] Current source positions.
view_indices tuple[int, ...] Current visible positions.
added_source_indices, removed_source_indices tuple[int, ...] Source positions changed by this operation.
added_row_ids, removed_row_ids tuple[Hashable or None, ...] Stable identities changed by this operation.
origin "user" or "api" User interaction or a public selection method.

All three row tuples contain read-only top-level copies. Their nested values are shared shallowly and should be treated as read-only. The tuple fields themselves are immutable snapshots; later selection changes do not rewrite an earlier event.

TableRowEvent Reference

Row/cell/link/action/context callbacks and the backward-compatible on_checkbox_toggle receive TableRowEvent.

Attribute Type Meaning
row dict[str, Any] Mutable shallow copy of the row for this callback; changing it does not update the table.
source_index int Index in the original source data list.
view_index int Index in the current filtered and sorted view.
column_key str or None Clicked column key for cell, link, and action events.
action_key str or None Clicked action key. Link events use "link" and checkbox events use "checkbox".

Use source_index when you want the original data position. Use view_index when you intentionally want the current visible order after search, filters, and sorting.

Method Reference

Read-only properties:

Property Type Meaning
row_key str or None Configured stable identity field.
read_only bool Whether user editing is disabled table-wide.
is_editing bool Whether a custom combobox text editor is active.
edit_validation_error str or None Current active-editor validation message.
Method Returns Use
set_data(data) None Replace all rows and clear selection.
get_data() list[dict] Pure read of shallow copies of committed source rows; does not commit an active editor.
commit_edit() bool Commit the active typed editor; False means validation rejected it.
cancel_edit() bool Discard the active typed editor and report whether one was active.
set_read_only(state) None Enable or disable user editing for the whole table.
get_columns() tuple[TableColumn, ...] Get normalized column definitions.
set_columns(columns) None Replace columns while preserving compatible sort/filter/selection state.
set_column_width(column_key, width) None Set one column width, clamped to its effective minimum; action-button content can raise that minimum.
get_column_width(column_key) int Read one column width.
set_column_width_mode(mode) None Switch between "fixed" and "fill" column layout.
get_column_width_mode() "fixed" or "fill" Read the current column layout mode.
refresh() None Redraw without changing rows or state.
get_style() TableStyle Return the current table-wide style options.
set_style(style=None, **kwargs) None Replace table-wide style options and redraw.
configure_style(style=None, **kwargs) None Merge table-wide style options into the current style and redraw.
clear() None Remove all rows.
get_selected_row() dict or None Get the first selected row.
get_selected_rows() list[dict] Get all selected rows.
get_selected_row_ids() list[Hashable] Get selected stable IDs in view order; requires row_key.
get_selected_indices() list[int] Get selected source indices in current view order.
get_selected_view_indices() list[int] Get selected visible row indices.
select_row(source_index, *, add=False, toggle=False, range_select=False, notify=True) bool Select a visible source row. Add/toggle/range modes are mutually exclusive and require multi_select=True; returns whether selection changed.
select_row_by_id(row_id, *, add=False, toggle=False, range_select=False, notify=True) bool Select a visible row by stable ID; requires row_key.
clear_selection(*, notify=True) bool Clear selection and report whether it changed.
get_row(index) dict Get a source row by source index.
get_row_by_id(row_id) dict Get a row by its configured stable ID.
row_id_for_source_index(source_index) Hashable Get a stable ID from a source position.
source_index_for_row_id(row_id) int Resolve a stable ID to its current source position.
get_view_row(view_index) dict Get a row by visible view index.
get_cell(source_index, column_key) Any Pure read of one committed cell by source position.
get_cell_by_id(row_id, column_key) Any Pure read of one committed cell by stable ID.
update_cell(source_index, column_key, value, *, notify=False) bool Validate and update a cell; returns whether its value changed. notify=True emits column/table edit callbacks.
update_cell_by_id(row_id, column_key, value, *, notify=False) bool Validate and update a cell by stable ID; requires row_key.
source_index_for_view_index(view_index) int Convert visible index to source index.
view_index_for_source_index(source_index) int or None Convert source index to visible index, or None if hidden by filters/search.
find_row_index(column_key, value) int or None Find the first source row where the column equals value.
sort_by(column_key, ascending=True) None Sort the view without reordering source data. Unlike header clicks, this API can sort a column whose sortable flag is false.
search(query) None Case-insensitive global search across visible, non-action columns.
filter(query) None Backward-compatible alias for search(query).
set_column_filter(column_key, definition) None Add or replace one column filter.
clear_column_filter(column_key) None Clear one column filter.
clear_column_filters() None Clear all column filters.
get_column_filters() dict Get active column filters.
add_row(row) int Append one row and return its source index.
add_rows(rows) list[int] Append multiple rows and return their source indices.
update_row(index, row) None Replace a source row by source index.
update_row_by_id(row_id, row) None Replace a source row by stable ID.
update_view_row(view_index, row) None Replace a row by current visible index.
update_row_where(column_key, value, new_row) bool Replace the first source row matching column_key == value.
delete_row(index) None Delete a source row by source index.
delete_row_by_id(row_id) None Delete a row by stable ID.
delete_view_row(view_index) None Delete a row by current visible index.
delete_row_where(column_key, value) bool Delete the first source row matching column_key == value.
delete_row_by_key(column_key, value) bool Alias for delete_row_where().
delete_selected_rows() int Delete selected source rows and return the number removed.
set_loading(state) None Show or hide loading state.
set_error(message=None) None Show error state. None or an empty string uses the constructor's error_message.
clear_error() None Hide error state without changing rows.
load_async(fetch_rows, *, on_success=None, on_error=None, clear_on_error=False) threading.Thread Fetch rows on a daemon worker; apply data and callbacks on the Tkinter thread. Only the newest outstanding load is applied.
destroy() None Cancel pending table callbacks/popups and destroy the widget. Call this for normal CustomTkinter teardown; do not use the object afterward.

Data Method Examples

table.set_data([
    {"id": 1, "name": "Alice", "status": "Open"},
    {"id": 2, "name": "Bob", "status": "Closed"},
])

if table.commit_edit():
    rows = table.get_data()
table.clear()
source_index = table.add_row({"id": 3, "name": "Diana", "status": "Open"})
source_indices = table.add_rows([
    {"id": 4, "name": "Evan", "status": "Open"},
    {"id": 5, "name": "Fatima", "status": "Closed"},
])
table.update_row(0, {"id": 1, "name": "Alice Updated", "status": "Open"})
table.update_row_by_id(2, {"id": 2, "name": "Bob Updated", "status": "Closed"})
table.update_view_row(0, {"id": 2, "name": "Bob Updated", "status": "Closed"})

updated = table.update_row_where(
    "id",
    3,
    {"id": 3, "name": "Diana Updated", "status": "Open"},
)
table.delete_row(0)
table.delete_view_row(0)

deleted = table.delete_row_where("id", 5)
deleted_again = table.delete_row_by_key("id", 4)
table.delete_row_by_id(3)
status = table.get_cell(0, "status")
same_status = table.get_cell_by_id(2, "status")

table.update_cell(0, "status", "closed")
table.update_cell_by_id(2, "status", "open", notify=True)
first_source_row = table.get_row(0)
first_visible_row = table.get_view_row(0)

source_index = table.source_index_for_view_index(0)
view_index = table.view_index_for_source_index(source_index)
found_index = table.find_row_index("id", 42)
stable_id = table.row_id_for_source_index(source_index)
same_source_index = table.source_index_for_row_id(stable_id)
one_row = table.get_selected_row()
many_rows = table.get_selected_rows()
source_indices = table.get_selected_indices()
view_indices = table.get_selected_view_indices()
row_ids = table.get_selected_row_ids()

table.select_row_by_id(42)
table.select_row_by_id(43, add=True)
table.select_row_by_id(43, toggle=True)
table.select_row_by_id(51, range_select=True)
table.clear_selection()

Column Method Examples

columns = table.get_columns()

table.set_columns([
    {"key": "id", "title": "ID", "width": 80, "type": "number"},
    {"key": "name", "title": "Customer", "width": 240},
])

table.set_column_width("name", 280)
current_width = table.get_column_width("name")
table.set_column_width_mode("fill")
current_mode = table.get_column_width_mode()
table.refresh()

Style Method Examples

current_style = table.get_style()

table.configure_style(
    header_bg="#111827",
    header_text_color="#ffffff",
    selected_bg="#2563eb",
)

table.set_style(
    {
        "surface_bg": "#ffffff",
        "row_bg": "#ffffff",
        "row_alt_bg": "#f8fafc",
        "text_color": "#111827",
    }
)

Search, Sort, and Filter Method Examples

table.sort_by("name", ascending=True)
table.search("north")
table.filter("north")

table.set_column_filter("status", {"type": "equals", "value": "Open"})
table.clear_column_filter("status")
table.clear_column_filters()

Loading Method Examples

table.set_loading(True)
table.set_data(fetch_rows())
table.set_loading(False)

table.set_error("Could not load rows")
table.clear_error()
thread = table.load_async(
    fetch_rows,
    on_success=lambda rows: None,
    on_error=lambda error: None,
    clear_on_error=True,
)

rows_from_cursor Reference

rows_from_cursor(cursor) converts a DB-API cursor result into dictionaries using cursor.description.

from CTkDataTable import rows_from_cursor


cursor.execute("SELECT id, name, status FROM customers")
rows = rows_from_cursor(cursor)
table.set_data(rows)
Parameter Accepted values Returns
cursor DB-API cursor after a SELECT query has executed and cursor.description is available. list[dict]

The helper calls cursor.fetchall(), so it consumes all remaining result rows. Calling it before a result-producing statement raises ValueError. Use unique SQL column names or aliases because duplicate names would target the same dictionary key.

Complete Mini App

This app combines search, sorting, badges, currency, dates, checkboxes, row actions, context menus, footer summaries, and multi-select.

from datetime import date, timedelta

import customtkinter as ctk

from CTkDataTable import CTkDataTable, TableRowEvent


app = ctk.CTk()
app.title("Work Orders")
app.geometry("1080x620")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(2, weight=1)

toolbar = ctk.CTkFrame(app, corner_radius=0)
toolbar.grid(row=0, column=0, sticky="ew", padx=14, pady=(14, 8))
toolbar.grid_columnconfigure(0, weight=1)

search = ctk.CTkEntry(toolbar, placeholder_text="Search work orders")
search.grid(row=0, column=0, sticky="ew", padx=(10, 8), pady=10)

detail = ctk.CTkLabel(app, text="No row selected", anchor="w")
detail.grid(row=1, column=0, sticky="ew", padx=14, pady=(0, 8))

today = date.today()
columns = [
    {"key": "id", "title": "WO", "width": 90},
    {"key": "title", "title": "Title", "width": 260},
    {
        "key": "priority",
        "title": "Priority",
        "width": 120,
        "type": "badge",
        "badge_colors": {"High": "#ef4444", "Medium": "#f59e0b", "Low": "#22c55e"},
        "badge_fallback_color": "#64748b",
    },
    {"key": "cost", "title": "Cost", "width": 110, "type": "currency"},
    {"key": "due", "title": "Due", "width": 125, "type": "date", "date_format": "%d %b"},
    {"key": "complete", "title": "Done", "width": 90, "type": "checkbox"},
    {
        "key": "actions",
        "title": "Actions",
        "width": 170,
        "type": "action",
        "sortable": False,
        "actions": [
            {"key": "view", "label": "View"},
            {"key": "delete", "label": "Delete"},
        ],
    },
]

rows = [
    {
        "id": "WO-001",
        "title": "Replace intake filter",
        "priority": "High",
        "cost": 450,
        "due": today + timedelta(days=2),
        "complete": False,
        "actions": None,
    },
    {
        "id": "WO-002",
        "title": "Update inspection checklist",
        "priority": "Low",
        "cost": 120,
        "due": today + timedelta(days=8),
        "complete": True,
        "actions": None,
    },
    {
        "id": "WO-003",
        "title": "Audit calibration records",
        "priority": "Medium",
        "cost": 275,
        "due": today + timedelta(days=4),
        "complete": False,
        "actions": None,
    },
]

table: CTkDataTable | None = None


def select_row(event: TableRowEvent) -> None:
    detail.configure(text=f"Selected {event.row['id']}: {event.row['title']}")


def handle_action(event: TableRowEvent) -> None:
    if table is None:
        return
    if event.action_key == "view":
        detail.configure(text=f"Viewing {event.row['id']}")
    elif event.action_key == "delete":
        table.delete_row_by_key("id", event.row["id"])


def handle_context(event: TableRowEvent) -> None:
    if event.action_key == "copy_id":
        app.clipboard_clear()
        app.clipboard_append(event.row["id"])
        detail.configure(text=f"Copied {event.row['id']}")


def handle_checkbox(event: TableRowEvent) -> None:
    assert event.column_key is not None
    detail.configure(
        text=f"{event.row['id']} complete: {event.row[event.column_key]}"
    )


table = CTkDataTable(
    app,
    columns=columns,
    data=rows,
    horizontal_scroll=True,
    multi_select=True,
    footer=True,
    summaries={"id": "count", "cost": "sum"},
    context_menu=[{"key": "copy_id", "label": "Copy ID"}],
    on_row_click=select_row,
    on_action_click=handle_action,
    on_context_action=handle_context,
    on_checkbox_toggle=handle_checkbox,
)
table.grid(row=2, column=0, sticky="nsew", padx=14, pady=(0, 14))

search.bind("<KeyRelease>", lambda _event: table.search(search.get()))

app.mainloop()

Operational Notes, Limitations, and Troubleshooting

Keep these runtime boundaries in mind when designing an application:

Common problems:

Symptom or exception Cause and fix
Cells are blank A column key does not exactly match the row dictionary key. Check spelling, capitalization, and SQL aliases. Missing ordinary values display blank rather than creating a key.
TypeError says rows must be mappings Convert tuple cursor rows with rows_from_cursor(cursor), or configure the database driver to return mapping rows.
Duplicate/missing row-ID error Every row must contain a unique, hashable value for the configured row_key. Validate the whole incoming dataset before calling set_data().
KeyError: Unknown column key A method/filter references a key absent from the current normalized columns, possibly after set_columns().
IndexError for a row Source and view indices were mixed up or became stale. Use the conversion methods or stable row IDs.
ValueError says a selected source row is not visible select_row() addresses source positions but only visible rows can be selected. Clear/adjust search and filters or use the current view conversion first.
A multi-select API call raises ValueError Enable multi_select=True, and pass only one of add, toggle, or range_select.
A typed combobox will not close/save commit_edit() returned False; show edit_validation_error, correct the value, or call cancel_edit().
A checkbox/combobox does not respond The table is read_only, the column has editable=False, or a validator rejected the proposed value.
A table callback did not fire after an API update update_cell...() defaults to notify=False; pass notify=True. Whole-row replacement methods intentionally do not emit cell-change callbacks.
Columns are clipped In fixed mode, enable horizontal_scroll=True, increase the viewport, hide columns, or switch to column_width_mode="fill".
The table does not expand with its window Give the containing grid row/column a positive weight and place the table with sticky="nsew", or configure the equivalent pack(fill="both", expand=True).
Invalid style or column option fails at startup Use the canonical reference names and documented ranges. Unknown style names, unsupported column types, invalid dimensions, duplicate column keys, and invalid combobox choices are rejected early.

When reporting a reproducible problem, include the Python/CTkDataTable versions, operating system, appearance mode, the smallest column/data definition that fails, and the complete traceback in the issue tracker.

Top