# Live table

A table of Ocean Base rows that updates in place as they change, through Ocean Realtime's hub.

A Worker reads the rows with the request's own cookie, and the live table writes them in the grid's classes, marked for the live island. The island joins the hub, swaps rows as they are inserted, updated or deleted, shows who else is here, and reads the rows again from read_url after every rejoin. The hub's address is written without its query, so no key or token reaches the page.

## Use it

Web:

```rust
use ocean_ui_web::{LiveTable, to_html};
let changes = ocean_ui_data::Changes::table("desk", "tickets");
to_html(&LiveTable { hub_url: "wss://realtime.plungeai.com/realtime/v1/websocket", changes: &changes, cols: &[("title", "Title"), ("state", "State")], rows: &rows, label: "Tickets", read_url: Some("/tickets") })
```

Native:

```rust
use ocean_ui_native::{data::LiveGrid, grid::GridOptions};
let mut grid = LiveGrid::new(cols);
grid.show(ui, &rows, &mut view, GridOptions::new("gallery-live"))
```

Cargo feature `data`; since 2026-10-29.

## Props

| Name | Type | Default | What |
|---|---|---|---|
| `hub_url` | `&str` | `—` | The hub's socket, wss://realtime.plungeai.com/realtime/v1/websocket; any query on it is dropped. |
| `changes` | `&Changes` | `—` | The database (schema), the collection (table), the filter and the event the island follows. |
| `cols` | `&[(&str, &str)]` | `—` | The columns as (key, label) pairs, in the order shown. |
| `rows` | `&[Value]` | `—` | The read's rows as JSON objects, each keyed by its id column. |
| `label` | `&str` | `—` | The table's accessible name. |
| `read_url` | `Option<&str>` | `None` | A same-origin path the island reads again after every rejoin; anything else is dropped. |

On the Mac: ![Live table, dark](/ui/native/data-dark.png) ![Live table, light](/ui/native/data-light.png)

## Examples

### First paint

The rows as the server draws them, before the hub joins.

```rust
let (cols, rows) = (columns(), rows());
let state = state_from_query(&cols, "");
let view = apply(&cols, &rows, &state);
WebGrid { cols: &cols, rows: &rows, view: &view, state: &state, base_href: at.href }.html_plain(out);
out.push_str(hub);
```

### Newest first

A live table usually reads newest first.

```rust
let (cols, rows) = (columns(), rows());
let state = state_from_query(&cols, "sort=age&dir=asc");
let view = apply(&cols, &rows, &state);
WebGrid { cols: &cols, rows: &rows, view: &view, state: &state, base_href: at.href }.html_plain(out);
```

### Few columns

Show only the columns that matter; the rest give way on narrow screens.

```rust
let (cols, rows) = (columns(), rows());
let state = state_from_query(&cols, "hide=owner&hide=age");
let view = apply(&cols, &rows, &state);
WebGrid { cols: &cols, rows: &rows, view: &view, state: &state, base_href: at.href }.html_plain(out);
```

## Accessibility

- The table is named by label through aria-label, and its headers are column headers.
- Who else is here is announced in a polite live region above the table.
- Rows change in place without moving focus, and before the island connects the table reads as a plain table.

## Related

- [Grid](/ui/components/grid.md)
- [Status](/ui/components/status.md)
- [Empty](/ui/components/empty.md)
