# Table

> A data table with a fixed header, sorting and selection. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API.

Source: https://albakore.github.io/pyzed/library/table/

:::tip[In plain words]
Rows and columns of data, like a spreadsheet you can sort by clicking a column.
:::

A data table with a fixed header, optional sorting and row selection. Columns are `ui.Column` objects whose `key` is a dict key, an attribute name or a function of the row; `render` turns a value into any widget. The table sorts the rows itself by the column whose **title** is `sort_by`: you only store that title and the direction.

`ui.table` · Category: [Data](https://albakore.github.io/pyzed/library/#data) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#table)

**Code:**

```python
rows = [
    {"name": "render.rs", "lang": "Rust", "lines": 1840},
    {"name": "app.py", "lang": "Python", "lines": 512},
    {"name": "theme.py", "lang": "Python", "lines": 730},
    {"name": "docs.yml", "lang": "YAML", "lines": 64},
]
pz.container(ui.table(
    [ui.Column("File", "name"), ui.Column("Language", "lang", width=120),
     ui.Column("Lines", "lines", width=80, align="end")],
    rows, sort_by="Lines", descending=True, on_sort=on_sort, selected=rows[1], striped=True,
), width=520)
```

## When to use

- For records with several comparable fields: files, users, processes, results.
- For a single column of items, a list of [List items](https://albakore.github.io/pyzed/library/list-item/) is lighter.

## Usage

```python
import pyzed as pz
from pyzed import ui

class Files(pz.App):
    def __init__(self):
        self.rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}]
        self.sort_by, self.descending, self.selected = "Lines", True, None

    def sort(self, title):
        self.descending = not self.descending if self.sort_by == title else False
        self.sort_by = title

    def view(self):
        return ui.table(
            [ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")],
            self.rows, sort_by=self.sort_by, descending=self.descending, on_sort=self.sort,
            on_row_press=lambda row: setattr(self, "selected", row), selected=self.selected,
        )
```

## Variants

:::note[About these captures]
Each capture is recorded by pyzed itself, off-screen at 2x, in One Dark or One Light to match this site's theme, and cropped to the component so it shows at its real size. The pointer hovers, presses or types into it; a component that does not react to the pointer cycles through a few themes. With reduced motion you see the first frame. Open **Code** under a capture to see the snippet that draws it; free names such as `on_save` stand for your own callbacks.
:::

### Options

```python
pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")],
                      [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}]), width=320)
```

```python
pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")],
                      [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}], striped=True), width=320)
```

```python
rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}]
pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], rows, selected=rows[1]), width=320)
```

```python
pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")],
                      [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}], sort_by="Lines", descending=True,
                      on_sort=on_sort), width=320)
```

```python
pz.container(ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")],
                      [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}, {"name": "ui.py", "lines": 1290}], row_height=24), width=320)
```

### Cells

```python
rows = [{"job": "build", "state": "passed"}, {"job": "lint", "state": "failed"}]
tint = {"passed": "success", "failed": "error"}
pz.container(ui.table([ui.Column("Job", "job"),
                       ui.Column("State", "state", width=110, render=lambda s: ui.badge(s, tint=tint[s]))],
                      rows), width=320)
```

```python
people = [("Ana", 31), ("Luis", 27)]
pz.container(ui.table([ui.Column("Name", lambda p: p[0]),
                       ui.Column("Age", lambda p: p[1], width=70, align="end")], people), width=320)
```

### States and events

`on_row_double_press(row)` opens a row and `on_row_context(row)` shows a context menu.

```python
rows = [{"name": "app.py", "size": 4}, {"name": "ui.py", "size": 12}, {"name": "README.md", "size": 2}]
ui.table(["name", "size"], rows, selected=[rows[0], rows[2]], on_row_press=on_press,
         on_row_double_press=on_open, on_row_context=on_context, height=130)
```

```python
ui.table(["name", "size"], [], empty="No files yet", height=100)
```

## Examples

### Custom cells

`render` receives the value and returns a widget, here a badge.

**Code:**

```python
rows = [{"job": "build", "state": "passed"}, {"job": "lint", "state": "failed"},
        {"job": "docs", "state": "running"}]
tint = {"passed": "success", "failed": "error", "running": "warning"}
pz.container(ui.table(
    [ui.Column("Job", "job"),
     ui.Column("State", "state", width=120, render=lambda s: ui.badge(s, tint=tint[s]))],
    rows,
), width=400)
```

## Events

Table has **4 events of its own**, listed below. Like every widget it also has the **18 common events** (`click`, `double_click`, `right_click`, `mouse_enter`, `drag`, `key_down`, `resize`…), attached with `.on(...)`: see [Events](https://albakore.github.io/pyzed/guides/events/) and [Events of every component](https://albakore.github.io/pyzed/library/events/).

| Event | Handler gets | When |
| --- | --- | --- |
| `on_sort` | `(title)` | Callable that receives the clicked title. |
| `on_row_press` | `(row)` | Callable that receives the clicked row. |
| `on_row_double_press` | `(row)` | Called with the row on a double click (open, edit…). |
| `on_row_context` | `(row)` | Called with the row on a right click (a context menu). |

**Handler gets** is what Pyzed passes to your callable: `()` means nothing. Any handler can also be a message (a value that is not callable), which goes to `update()`.

### Handling the events

The same events written three ways. **Imperative**: methods of the app change `self`. **Declarative**: the handlers produce messages and one `update()` decides. **Functional**: a pure function draws an immutable state and asks for changes with `set_state(...)`. See [Imperative, declarative and functional](https://albakore.github.io/pyzed/guides/code-styles/).

**Imperative:**

```python
class Files(pz.App):
    def __init__(self):
        self.rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}]
        self.sort_by, self.descending = "Lines", True
        self.selected, self.opened = None, None

    def sort(self, title):               # on_sort: the clicked column title
        self.descending = not self.descending if self.sort_by == title else False
        self.sort_by = title

    def view(self):
        key = "name" if self.sort_by == "File" else "lines"
        rows = sorted(self.rows, key=lambda r: r[key], reverse=self.descending)
        return ui.table(
            [ui.Column("File", "name"), ui.Column("Lines", "lines", width=80, align="end")], rows,
            sort_by=self.sort_by, descending=self.descending, on_sort=self.sort, selected=self.selected,
            on_row_press=lambda row: setattr(self, "selected", row),         # click
            on_row_double_press=lambda row: setattr(self, "opened", row),    # double click
            on_row_context=lambda row: print("menu for", row["name"]),       # right click
        )
```

**Declarative:**

```python
from dataclasses import dataclass

@dataclass
class Sort:
    title: str

@dataclass
class Select:
    row: dict

class Files(pz.App):
    rows = [{"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730}]
    sort_by, descending, selected = "Lines", True, None

    def update(self, message):
        match message:
            case Sort(title):
                self.descending = not self.descending if title == self.sort_by else False
                self.sort_by = title
            case Select(row):
                self.selected = row

    def view(self):
        return ui.table([ui.Column("File", "name"), ui.Column("Lines", "lines", width=80)], self.rows,
                        sort_by=self.sort_by, descending=self.descending, selected=self.selected,
                        on_sort=Sort, on_row_press=Select)
```

**Functional:**

```python
from dataclasses import dataclass

ROWS = ({"name": "app.py", "lines": 512}, {"name": "theme.py", "lines": 730})

@dataclass(frozen=True)
class State:
    sort_by: str = "Lines"
    descending: bool = True
    selected: str = ""

def view(state, set_state):
    key = "name" if state.sort_by == "File" else "lines"
    rows = sorted(ROWS, key=lambda r: r[key], reverse=state.descending)
    return ui.table(
        [ui.Column("File", "name"), ui.Column("Lines", "lines", width=80)], rows,
        sort_by=state.sort_by, descending=state.descending,
        selected=next((r for r in rows if r["name"] == state.selected), None),
        on_sort=lambda t: set_state(sort_by=t, descending=not state.descending if t == state.sort_by else False),
        on_row_press=lambda row: set_state(selected=row["name"]))
```

## Properties and API

### `ui.table`

```python
table(
    columns,
    rows,
    *,
    sort_by=None,
    descending=False,
    on_sort=None,
    on_row_press=None,
    selected=None,
    striped=False,
    row_height=30,
    height=None,
    on_row_double_press=None,
    on_row_context=None,
    empty=None,
    theme=None,
)
```

A data table with a fixed header, optional sorting and row selection. Sorting happens here when `sort_by` names a column: clicking a header calls `on_sort(title)` and you update `sort_by`/`descending` in your state. Rows are virtualized: only the ones in view are built, so hundreds of thousands of rows scroll smoothly.

| Property | Default | Description |
| --- | --- | --- |
| `columns` | *required* | `ui.Column` or plain keys. |
| `rows` | *required* | Dicts or objects. |
| `sort_by` | `None` | Title of the column to sort by. |
| `descending` | `False` | Descending order. |
| `selected` | `None` | The row to highlight, or a list or set of rows (multiple selection). |
| `striped` | `False` | Shaded alternating rows. |
| `row_height` | `30` | Height of each row. |
| `height` | `None` | Height: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). |
| `empty` | `None` | Text or widget shown in place of the rows when there are none. |
| `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). |

Events: `on_sort`, `on_row_press`, `on_row_double_press`, `on_row_context` ([events](#events)).

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#table)

### `ui.Column`

```python
@dataclass
class Column(title, key, width='fill', align='start', render=None, sortable=True)
```

A column of `ui.table`. `key` is a dict key, an attribute name or a function of the row; `render` (optional) turns the value into a widget. `width` is `"fill"`, a number or a portion such as `"fill:2"`.

| Property | Default | Description |
| --- | --- | --- |
| `title` | *required* | Header (and name used for sorting). |
| `key` | *required* | How to get the value from each row. |
| `width` | `"fill"` | Width: pixels, `"fill"`, `"shrink"` or `"fill:N"` (proportion). |
| `align` | `"start"` | `"start"`, `"center"` or `"end"`. |
| `render` | `None` | Function value → widget. |
| `sortable` | `True` | Whether the header can be clicked to sort. |

[Full reference →](https://albakore.github.io/pyzed/reference/ui/#column)

## Related

- [List item](https://albakore.github.io/pyzed/library/list-item/): A list row with icon, text and an end slot.
- [Virtual list](https://albakore.github.io/pyzed/library/virtual-list/): A list that only builds the rows in view, for millions of items.
- [Stat](https://albakore.github.io/pyzed/library/stat/): A dashboard number with change and sparkline.
- [Settings group](https://albakore.github.io/pyzed/library/settings-group/): A titled block of field rows, for settings screens.
