Skip to content

Tables overview

A table is how Orbit lists records in the admin UI: one row per record, columns for fields, and toolbar chrome for search, filters, and actions. You configure the table in Python with a fluent API (Table.make(...).columns([...]).filters([...]) and so on). Orbit renders HTML; the Conduit list host hydrates interactivity (typing in search, clicking sort headers, changing page, bulk-selecting rows).

You usually attach a table to a Resource so list / create / edit pages stay wired together — the list page is often the first screen operators live in. You can also embed a standalone table on a custom page or inside a table widget. Column types, filters, actions, grouping, summaries, and cell layouts each have dedicated guides (see Guides below).

Table overview (light)

Table overview (dark)

The basis of any table is rows and columns. Feed rows with .records([...]) (dicts or objects) or store a .query(...) for your app layer. You define the columns that appear in each row.

Orbit ships many column types — see the columns catalog. Pass them to .columns([...]):

from almasix.orbit.tables import Table, TextColumn, IconColumn
table = (
Table.make("posts")
.columns([
TextColumn.make("title"),
TextColumn.make("slug"),
IconColumn.make("featured").boolean(),
])
)

In this example there are three columns: title and slug as text, and a boolean icon for “featured”.

Columns (light)

Columns (dark)

Chain configurators onto a column. .searchable() adds a search field to the table toolbar and matches that column’s values (you can mark several columns searchable — one query searches them all):

from almasix.orbit.tables import TextColumn
TextColumn.make("title").searchable()

Searchable column (light)

Searchable column (dark)

.sortable() adds a sort control on the column header; clicking it sorts the table by that column:

TextColumn.make("title").sortable()

Sortable column (light)

Sortable column (dark)

Use .default_sort("title", "desc") on the table for the initial sort until the user picks another column. For full-text or otherwise custom search, call .search_using(callback) or mark the whole table .searchable() when no column is searchable yet.

Display nested data with dot notation. For a post that has an author dict (or object) with a name, use:

TextColumn.make("author.name")

Orbit resolves each segment (author, then name) on dicts and objects. The same paths work for search and sort when the column is searchable/sortable.

Relationship column (light)

Relationship column (dark)

.columns([...]) replaces the full column list. To append without wiping prior configuration (handy with global defaults), use .push_columns([...]):

from almasix.orbit.tables import Table, TextColumn
Table.configure_using(lambda t: t.push_columns([
TextColumn.make("created_at")
.label("Created")
.sortable()
.toggleable(is_toggled_hidden_by_default=True),
]))

Beyond column search, filters let users narrow rows in other ways. Attach them with .filters([...]):

from almasix.orbit.tables import SelectFilter, Filter
table.filters([
Filter.make("featured").query(lambda records, value: [
r for r in records if r.get("featured")
] if value else records),
SelectFilter.make("status").options({
"draft": "Draft",
"review": "Review",
"published": "Published",
}),
])

A filter icon appears in the toolbar. Opening it shows each filter’s control (checkbox, select, and so on). See Filters for TernaryFilter, TrashedFilter, groups, and .defer_filters().

Filters (light)

Filters (dark)

Actions are buttons that run a callback (or open a URL / modal). On a table they live in three places:

Placement Method When to use
Per row .record_actions([...]) (alias .actions([...])) Edit, view, delete, or custom verbs for one record
Header .header_actions([...]) Create and other table-wide shortcuts
Bulk toolbar .toolbar_actions([...]) (alias .bulk_actions([...])) Operate on every selected row at once
from almasix.orbit.actions import Action, CreateAction, DeleteBulkAction, EditAction
table = (
Table.make("posts")
.columns([TextColumn.make("title")])
.record_actions([
Action.make("feature")
.action(lambda record, **_: record.__setitem__("featured", True))
.hidden(lambda record, **_: bool(record.get("featured"))),
EditAction.make(),
])
.toolbar_actions([DeleteBulkAction.make()])
.header_actions([CreateAction.make()])
)

When bulk actions are present, each row gets a checkbox and a selection bar appears once rows are selected. Actions can confirm, open modals, and collect forms — see table actions and panel actions.

Row / bulk actions (light)

Row / bulk actions (dark)

Tables are paginated by default (per-page options 5 / 10 / 25 / 50). Users change page size and move between pages from the footer chrome.

Pagination (light)

Pagination (dark)

Pass options to .paginated([...]). Include "all" to offer a full list (use carefully on large datasets):

table.paginated([10, 25, 50, 100, "all"])
table.default_pagination_page_option(25)

Make sure that value appears in the options list.

table.extreme_pagination_links()

Adds « / » controls beside the usual previous / next buttons.

from almasix.orbit.tables import PaginationMode
table.pagination_mode(PaginationMode.SIMPLE) # prev / next only
table.pagination_mode(PaginationMode.CURSOR) # same chrome; cursor semantics for DB hosts

When several tables share a page, give each a unique id so pagination state does not clash:

table.query_string_identifier("users")
table.paginated(False)

The footer chrome (result range, per-page select, and page links) is omitted and every record is shown.

Pagination disabled (light)

Pagination disabled (dark)

Persist the user’s per-page choice with .persist_records_per_page_in_session() (or .persist_in_session() for search, sort, filters, columns, and per-page together).

Make an entire row clickable:

table.record_url(lambda record: f"/posts/{record['id']}")
table.open_record_url_in_new_tab() # optional

On resource tables the list host often supplies a view/edit URL already; .record_url() overrides it. Individual columns can still use .url(...) for cell links.

Allow drag-style reordering by storing order in a column (e.g. sort):

table.reorderable("sort")
table.paginated_while_reordering() # keep pagination while reordering (off by default)
table.before_reordering(lambda order: ...)
table.after_reordering(lambda order: ...)

A toolbar control toggles reorder mode (ListRecordsHost.toggleReordering()). Apply a new order with table.apply_reorder(ids).

Reorder (light)

Reorder (dark)

Customize the trigger with .reorder_records_trigger_action(lambda action, is_reordering: ...).

Add a heading and optional description above the toolbar:

table.heading("Clients").description("Manage your clients here.")

Heading (light)

Heading (dark)

Replace the whole header block with custom HTML:

table.header("<div class='…'>…</div>")
# or table.header(lambda **ctx: "…")

Refresh the table on an interval (hosts honor data-poll):

table.poll("10s")

For heavy lists, mark the table to load asynchronously:

table.defer_loading()

When you control search outside column .searchable() (e.g. a full-text index), pass a callback:

table.search_using(lambda records, search: [
r for r in records if search.lower() in str(r.get("title", "")).lower()
])
# Show the search field even with no searchable columns:
table.searchable()

Remember filters, search, sort, column visibility, and per-page across visits:

table.persist_in_session() # all on
table.persist_in_session(False) # all off
# or: persist_filters_in_session / persist_search_in_session /
# persist_sort_in_session / persist_records_per_page_in_session

Flags are emitted as data-persist-* attributes for the list host / front-end to store.

table.striped() # default is already striped; pass False to disable
table.record_classes(
lambda record: "or-row-draft" if record.get("status") == "draft" else None
)

Striped / row classes (light)

Striped / row classes (dark)

When there are no rows after filters/search:

table.empty_state_heading("No posts yet")
table.empty_state_description("Create your first post to get going.")
table.empty_state_icon("heroicon-o-document-text")
table.empty_state_actions([CreateAction.make()])
# or fully custom markup:
table.empty_state("<div class='…'>…</div>")

Empty state (light)

Empty state (dark)

Register defaults for every table (e.g. in a service provider boot hook):

from almasix.orbit.tables import Table, PaginationMode
Table.configure_using(lambda t: (
t.paginated([10, 25, 50])
.pagination_mode(PaginationMode.DEFAULT)
.striped()
))
Method Role
.records([...]) In-memory list (dicts or objects)
.query(any) Store a query object for your app / ORM layer

get_records() / get_total() filter, search, sort, then paginate the in-memory list. Push filtering to the database when you use .query() with your own loader.

Page What you’ll find
Columns catalog Every column type
Text / money Search, sort, badge, currency, copyable, markdown
Editable columns Inline select / toggle / text / checkbox
Filters Select, ternary, groups, deferred apply
Actions Row, bulk, header, and dropdowns
Layout Split, Stack, Panel, Grid, View
Summaries Sum, Average, Count, Range
Grouping default_group and collapsible groups
Standalone tables Tables outside a Resource

Live sample: Tables overview in examples/orbit-admin (TablesOverviewResource).