# Tab bar

> Editor tabs with icons, dirty dots and close buttons. A native Pyzed component for Python desktop GUIs, with a live preview, code examples and its API.

Source: https://albakore.github.io/pyzed/library/tab-bar/

:::tip[In plain words]
The tabs at the top of an editor, one per open document.
:::

A row of editor tabs with optional controls at the start and end. `ui.Tab.for_file(path)` creates a tab with the file's name, icon and path as id. `dirty=True` shows a dot that turns into the close button on hover, `preview=True` draws the name in italics, and `status` tints it with a git color. `on_select(id)` and `on_close(id)` receive the tab id.

`ui.tab_bar` · Category: [Navigation](https://albakore.github.io/pyzed/library/#navigation) · [Full reference →](https://albakore.github.io/pyzed/reference/ui/#tab_bar)

**Code:**

```python
ui.tab_bar(
    [ui.Tab.for_file("src/main.rs"),
     ui.Tab.for_file("src/app.rs", dirty=True, status="modified"),
     ui.Tab.for_file("README.md", preview=True)],
    "src/main.rs", on_select=on_select, on_close=on_close,
    start=[ui.icon_button("arrow-left", on_back, size="sm"),
           ui.icon_button("arrow-right", on_forward, size="sm")],
    end=[ui.icon_button("plus", on_new, size="sm"), ui.icon_button("columns-2", on_split, size="sm")],
)
```

## When to use

- For open documents in an editor, or any set of views the user opens and closes.

## Usage

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

class Editor(pz.App):
    def __init__(self):
        self.open, self.active = ["main.py", "app.py"], "main.py"

    def close(self, path):
        i = self.open.index(path)
        self.open.remove(path)
        if self.active == path:
            self.active = self.open[min(i, len(self.open) - 1)] if self.open else None

    def view(self):
        return ui.tab_bar([ui.Tab.for_file(p) for p in self.open], self.active,
                          on_select=lambda p: setattr(self, "active", p), on_close=self.close)
```

## 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.
:::

### Tabs

```python
pz.container(ui.tab_bar([ui.Tab.for_file("main.rs"), ui.Tab.for_file("app.rs")], "main.rs",
                        on_close=on_close), width=400)
```

```python
pz.container(ui.tab_bar([ui.Tab.for_file("main.rs"), ui.Tab.for_file("notes.md", dirty=True)], "main.rs",
                        on_close=on_close), width=400)
```

```python
pz.container(ui.tab_bar([ui.Tab.for_file("README.md", preview=True)], "README.md", on_close=on_close), width=400)
```

```python
pz.container(ui.tab_bar([ui.Tab.for_file("app.py", status="modified"),
                         ui.Tab.for_file("new.py", status="created")], "app.py", on_close=on_close), width=400)
```

```python
pz.container(ui.tab_bar([ui.Tab("home", "Home", icon="house"), ui.Tab("logs", "Logs", icon="scroll-text")],
                        "home", on_select=on_select), width=400)
```

### Controls

```python
pz.container(ui.tab_bar([ui.Tab.for_file("main.rs")], "main.rs", on_close=on_close,
                        start=[ui.icon_button("arrow-left", on_back, size="sm")],
                        end=[ui.icon_button("plus", on_new, size="sm")]), width=400)
```

```python
pz.row([ui.tab(ui.Tab.for_file("main.rs"), active=True),
        ui.tab(ui.Tab.for_file("app.rs"), active=False, dimmed=True)])
```

### States and events

A middle click closes a tab, `on_context(id)` runs on a right click and `on_new` on a double click of the empty space.

```python
ui.tab_bar([ui.Tab("app", "app.py", icon="file", pinned=True), ui.Tab("ui", "ui.py", icon="file")],
           "ui", on_close=on_close, on_context=on_context, on_new=on_new)
```

## Events

Tab bar has **7 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 | Of | Handler gets | When |
| --- | --- | --- | --- |
| `on_select` | `ui.tab_bar`, `ui.tab` | `(tab_id)` | Callable that receives the chosen item. |
| `on_close` | `ui.tab_bar`, `ui.tab` | `(tab_id)` | Message or callable sent when the X is pressed. |
| `on_drag` | `ui.tab_bar` | `(tab_id)` | Called with the tab id when a tab starts being dragged. |
| `on_drop` | `ui.tab_bar` | `(tab_id, event)` | Called with the tab id and the `Event` when the dragged tab is released: `event.window_x`/`window_y` (outside the window if it was dropped outside) and `event.screen`. |
| `on_drag_over` | `ui.tab_bar` | `(index)` | While `dragging` is set: called with the insertion index under the pointer, or `None` when it leaves the bar. |
| `on_context` | `ui.tab_bar` | `(tab_id)` | Called with the id of a tab that is right-clicked. |
| `on_new` | `ui.tab_bar` | `()` | Sent on a double click of the empty space after the tabs. |

**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 Editor(pz.App):
    def __init__(self):
        self.open, self.active = ["main.py", "app.py"], "main.py"
        self.untitled = 0

    def close(self, path):               # on_close: the id of the tab
        i = self.open.index(path)
        self.open.remove(path)
        if self.active == path:
            self.active = self.open[min(i, len(self.open) - 1)] if self.open else None

    def new(self):                       # on_new: double click after the tabs
        self.untitled += 1
        self.open.append(f"untitled-{self.untitled}")
        self.active = self.open[-1]

    def view(self):
        return ui.tab_bar([ui.Tab.for_file(p) for p in self.open], self.active,
                          on_select=lambda p: setattr(self, "active", p), on_close=self.close,
                          on_new=self.new, on_context=lambda p: print("menu for", p))
```

**Declarative:**

```python
from dataclasses import dataclass

@dataclass
class Activate:
    path: str

@dataclass
class Close:
    path: str

class Editor(pz.App):
    open, active = ["main.py", "app.py"], "main.py"

    def update(self, message):
        match message:
            case Activate(path):
                self.active = path
            case Close(path):
                self.open = [p for p in self.open if p != path]
                self.active = self.open[0] if self.open else None

    def view(self):
        return ui.tab_bar([ui.Tab.for_file(p) for p in self.open], self.active,
                          on_select=Activate, on_close=Close)
```

**Functional:**

```python
from dataclasses import dataclass

@dataclass(frozen=True)
class State:
    open: tuple = ("main.py", "app.py")
    active: str = "main.py"

def view(state, set_state):
    def close(path):
        rest = tuple(p for p in state.open if p != path)
        set_state(open=rest, active=state.active if state.active != path else (rest[0] if rest else ""))
    return ui.tab_bar([ui.Tab.for_file(p) for p in state.open], state.active,
                      on_select=lambda p: set_state(active=p), on_close=close)
```

## Properties and API

### `ui.tab_bar`

```python
tab_bar(
    tabs,
    active,
    *,
    on_select=None,
    on_close=None,
    start=(),
    end=(),
    height=32,
    on_drag=None,
    on_drop=None,
    on_drag_over=None,
    dragging=None,
    drop_index=None,
    on_context=None,
    on_new=None,
    theme=None,
)
```

A row of tabs with optional controls at the start and end (navigation, split, +). The close button appears on the active tab and on hover. Tabs can be dragged to reorder them or out of the window: see [Draggable tabs](https://albakore.github.io/pyzed/guides/windows/#draggable-tabs-and-detachable-windows).

| Property | Default | Description |
| --- | --- | --- |
| `tabs` | *required* | List of `Tab`. |
| `active` | *required* | Id of the active tab. |
| `start` | `()` | Widgets at the start (left). |
| `end` | `()` | Widgets at the end (right). |
| `height` | `32` | Height. |
| `dragging` | `None` | Id of the tab being dragged (dims it and turns on `on_drag_over`). |
| `drop_index` | `None` | Draws the insertion mark before the tab at this index (`len(tabs)`: after the last one). |
| `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). |

Events: `on_select`, `on_close`, `on_drag`, `on_drop`, `on_drag_over`, `on_context`, `on_new` ([events](#events)).

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

### `ui.Tab`

```python
@dataclass
class Tab(id, label, icon=None, icon_color=None, dirty=False, preview=False, status=None, pinned=False, tooltip=None)
```

A tab. `Tab.for_file(path, **kwargs)` creates it with the file's name and icon.

| Property | Default | Description |
| --- | --- | --- |
| `id` | *required* | Stable identifier used to focus or scroll it with a `Command`. |
| `label` | *required* | Label text. |
| `icon` | `None` | Name of a Lucide icon (`pyzed icons` lists them). |
| `icon_color` | `None` | Icon color: a theme token or a hex color. |
| `dirty` | `False` | Unsaved changes (shows a dot). |
| `preview` | `False` | Preview tab (in italics). |
| `status` | `None` | Git status (tints the name). |
| `pinned` | `False` | Shows only the icon, has no close button and goes first in `tab_bar`. |
| `tooltip` | `None` | Text shown on hover (the full path…). |

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

### `ui.tab`

```python
tab(item, *, active, on_select=None, on_close=None, height=32, dimmed=False, theme=None)
```

A single tab. You normally use `tab_bar`.

| Property | Default | Description |
| --- | --- | --- |
| `item` | *required* | The `Tab`. |
| `active` | *required* | Whether it is the active one. |
| `height` | `32` | Height. |
| `dimmed` | `False` | Mutes the label (the tab being dragged). |
| `theme` | `None` | Theme to use instead of the current one (`pyzed.theme.current()`). |

Events: `on_select`, `on_close` ([events](#events)).

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

## Related

- [Toolbar](https://albakore.github.io/pyzed/library/toolbar/): The bar under the tabs: breadcrumbs and buffer actions.
- [Breadcrumbs](https://albakore.github.io/pyzed/library/breadcrumbs/): `src › main.rs › fn main`, optionally clickable.
- [Segmented](https://albakore.github.io/pyzed/library/segmented/): A group of mutually exclusive text buttons.
