Live table
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.
First paint
The rows as the server draws them, before the hub joins.
| Ticket | Subject | Owner | Status | Age (h) |
|---|---|---|---|---|
| T-4127 | Refund for a double charge | Triage agent | Needs a person | 2 |
| T-4126 | Cannot sign in on mobile | Maya Chen | Working | 5 |
| T-4125 | Invoice address change | Billing agent | Done | 9 |
| T-4124 | Export stuck at 90 percent | Triage agent | Working | 14 |
| T-4123 | API key rotation | Sam Ortiz | Waiting | 26 |
| T-4122 | Webhook retries | Triage agent | Failed | 31 |
On a page with Ocean Realtime, LiveTable draws these rows first and the live island updates them from the hub as rows change; here they are static.
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.
| Ticket | Subject | Owner | Status | Age (h) |
|---|---|---|---|---|
| T-4127 | Refund for a double charge | Triage agent | Needs a person | 2 |
| T-4126 | Cannot sign in on mobile | Maya Chen | Working | 5 |
| T-4125 | Invoice address change | Billing agent | Done | 9 |
| T-4124 | Export stuck at 90 percent | Triage agent | Working | 14 |
| T-4123 | API key rotation | Sam Ortiz | Waiting | 26 |
| T-4122 | Webhook retries | Triage agent | Failed | 31 |
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.
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);On the Mac
The same vocabulary drawn by Ocean UI's native face (egui on Metal), as the gallery's snapshot.


Use it
Web
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
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 of ocean-ui-core, ocean-ui-web and ocean-ui-native; available 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. |
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
Markdown
The same page as Markdown, for an agent or a pull request.
# 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:  
## 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)