Skip to content

Data table

A team members table: filter by name or email, toggle statuses with chips, pick a role, sort by any column, select rows with checkboxes, page through results, copy them as CSV and invite people from a dialog.

Data tableData table

The screenshot follows the site’s theme: switch between light and dark at the top of the page to see the block in One Light and One Dark.

From the repository root:

Terminal window
python examples/blocks/data_table.py
  • Filtering, sorting and paging done in plain Python, so it is easy to swap in a database query.
  • Two-line cells (avatar, name and email) through ui.Column(render=...).
  • pz.Command.copy puts the CSV on the clipboard.
  • ui.prompt for the invite, with Enter to send.
Component Used for
Table The table
Chip Status filters
Search input Text filter
Select Role filter
Checkbox Row selection
Badge Statuses
Avatar Members
Facepile Team
Prompt Invite dialog
Icon button Pagination and row menu

The whole block, ready to copy. It only needs pip install pyzed-ui.

examples/blocks/data_table.py
"""Data table block: a team members table with search, status filters, sorting, selection and paging."""
import pyzed as pz
from pyzed import ui
PEOPLE = [
("Olivia Martin", "olivia@acme.dev", "Owner", "Active", "Just now"),
("Jackson Lee", "jackson@acme.dev", "Admin", "Active", "5 min ago"),
("Isabella Nguyen", "isabella@acme.dev", "Editor", "Active", "1 hour ago"),
("William Kim", "will@acme.dev", "Viewer", "Invited", "—"),
("Sofia Davis", "sofia@acme.dev", "Editor", "Active", "Yesterday"),
("Liam Johnson", "liam@acme.dev", "Editor", "Suspended", "3 weeks ago"),
("Emma Wilson", "emma@acme.dev", "Admin", "Active", "2 hours ago"),
("Noah Brown", "noah@acme.dev", "Viewer", "Active", "4 days ago"),
("Ava Garcia", "ava@acme.dev", "Editor", "Invited", "—"),
("Lucas Rossi", "lucas@acme.dev", "Viewer", "Active", "Today"),
("Mia Fischer", "mia@acme.dev", "Editor", "Active", "Last week"),
("Ethan Clark", "ethan@acme.dev", "Viewer", "Suspended", "2 months ago"),
("Chloe Dubois", "chloe@acme.dev", "Admin", "Active", "Today"),
("Mateo Silva", "mateo@acme.dev", "Editor", "Active", "Yesterday"),
]
ROWS = [{"id": i, "name": n, "email": e, "role": r, "status": s, "seen": seen}
for i, (n, e, r, s, seen) in enumerate(PEOPLE)]
BY_NAME = {r["name"]: r for r in ROWS}
STATUS_TINT = {"Active": "success", "Invited": "info", "Suspended": "error"}
ROLES = ["All roles", "Owner", "Admin", "Editor", "Viewer"]
PAGE_SIZE = 9
SORT_KEYS = {"Member": "name", "Role": "role", "Status": "status", "Last active": "seen"}
class DataTable(pz.App):
title = "Team members"
size = (1200, 760)
def __init__(self):
self.query = ""
self.statuses = {"Active", "Invited"}
self.role = "All roles"
self.sort_by, self.descending = "Member", False # a column title
self.selected = {1, 4}
self.page = 0
self.notice = ""
self.inviting, self.invite_email = False, ""
# -- data ------------------------------------------------------------------------------
def visible(self):
q = self.query.lower()
rows = [r for r in ROWS
if r["status"] in self.statuses
and (self.role == "All roles" or r["role"] == self.role)
and (q in r["name"].lower() or q in r["email"].lower())]
return rows
# -- actions ---------------------------------------------------------------------------
def toggle_status(self, status):
self.statuses ^= {status}
self.page = 0
def toggle_row(self, row_id):
self.selected ^= {row_id}
def sort(self, key):
self.descending = not self.descending if key == self.sort_by else False
self.sort_by = key
def export(self):
rows = self.visible()
self.notice = f"Copied {len(rows)} rows as CSV"
lines = ["name,email,role,status"] + [f"{r['name']},{r['email']},{r['role']},{r['status']}" for r in rows]
return pz.Command.copy("\n".join(lines))
def invite(self):
if "@" in self.invite_email:
self.notice = f"Invitation sent to {self.invite_email}"
self.inviting, self.invite_email = False, ""
def set_query(self, value):
self.query, self.page = value, 0
# -- view ------------------------------------------------------------------------------
def person(self, row):
return pz.row([
ui.avatar("".join(p[0] for p in row["name"].split()), size=28, index=row["id"]),
pz.column([ui.label(row["name"], weight="medium"), ui.muted(row["email"], size="sm")], spacing=1),
], spacing=10, align="center")
def toolbar(self):
chips = [ui.chip(f"{status} {sum(r['status'] == status for r in ROWS)}",
on_press=lambda status=status: self.toggle_status(status),
selected=status in self.statuses, icon="circle-dot" if status in self.statuses else None)
for status in STATUS_TINT]
return pz.row([
pz.container(ui.search_input(self.query, self.set_query, placeholder="Filter by name or email…"),
width=300),
*chips,
ui.select(ROLES, self.role, lambda v: setattr(self, "role", v), width=140),
pz.hspace(),
ui.button("Export", self.export, icon="download", variant="ghost"),
ui.button("Invite member", lambda: setattr(self, "inviting", True), icon="user-plus", variant="solid"),
], spacing=8, align="center")
def table(self, rows):
columns = [
ui.Column("", "id", width=36, sortable=False,
render=lambda i: ui.checkbox(i in self.selected, lambda _=None, i=i: self.toggle_row(i))),
ui.Column("Member", "name", width=("fill", 3), render=lambda name: self.person(BY_NAME[name])),
ui.Column("Role", "role", width=("fill", 1), render=lambda role: ui.label(role, color="text_muted")),
ui.Column("Status", "status", width=("fill", 1),
render=lambda s: ui.badge(s, tint=STATUS_TINT[s])),
ui.Column("Last active", "seen", width=("fill", 1), render=lambda s: ui.muted(s)),
ui.Column("", "id", width=44, sortable=False,
render=lambda i: ui.icon_button("ellipsis", None, size="sm", tooltip="More")),
]
return ui.table(columns, rows, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort,
row_height=52)
def view(self):
rows = self.visible()
pages = max(1, -(-len(rows) // PAGE_SIZE))
page_rows = sorted(rows, key=lambda r: r[SORT_KEYS[self.sort_by]], reverse=self.descending)
page_rows = page_rows[self.page * PAGE_SIZE:(self.page + 1) * PAGE_SIZE]
start = self.page * PAGE_SIZE + 1 if rows else 0
footer = pz.row([
ui.muted(self.notice or f"{len(self.selected)} selected · showing {start}–{start + len(page_rows) - 1 if rows else 0} "
f"of {len(rows)}", size="sm"),
pz.hspace(),
ui.icon_button("chevron-left", lambda: setattr(self, "page", max(0, self.page - 1)),
disabled=self.page == 0, variant="filled"),
*[ui.button(str(n + 1), lambda n=n: setattr(self, "page", n), size="sm",
variant="solid" if n == self.page else "ghost") for n in range(pages)],
ui.icon_button("chevron-right", lambda: setattr(self, "page", min(pages - 1, self.page + 1)),
disabled=self.page >= pages - 1, variant="filled"),
], spacing=6, align="center")
page = pz.column([
pz.row([
pz.column([ui.heading("Team members", level=2),
ui.muted("Manage who can access the Acme workspace and what they can do.")],
spacing=4),
pz.hspace(),
ui.facepile(["OM", "JL", "IN", "WK", "SD", "LJ"], size=28),
], align="center"),
self.toolbar(),
ui.card(pz.column([self.table(page_rows), ui.divider(), pz.container(footer, padding=(4, 4, 0, 4))],
spacing=8), padding=8),
], spacing=20, padding=28, height="fill").style(background="background")
dialog = None
if self.inviting:
dialog = ui.prompt("Invite a member", self.invite_email, lambda v: setattr(self, "invite_email", v),
self.invite, lambda: setattr(self, "inviting", False),
message="They get an email with a link to join the workspace.",
placeholder="name@company.com", submit_label="Send invite", id="invite")
return ui.dialog_layer(page, dialog, on_dismiss=lambda: setattr(self, "inviting", False))
if __name__ == "__main__":
pz.run(DataTable)

Change theme (or self.theme = pz.themes.get("One Dark").with_accent("#e11d48")), replace the sample data with your own and split pieces into components when they grow. An AI agent with the MCP server can read this block with read_example("blocks/data_table") and adapt it for you.