Table
A table here is a real one — TableContent renders a <table> with a header, body and rows — and everything beyond that is a part you add. Selection, sorting, resizing, expanding, dragging and loading more are each opt-in, so a plain table costs nothing.
import { Table, TableBody, TableCell, TableColumn, TableRow } from "ropav";The frame
Table is the frame, TableScrollContainer is what scrolls, and TableContent is the table itself. Keeping them apart is what lets the header stay put while the body scrolls, and what gives a wide table somewhere to overflow that is not the page.
| Name | Role | Joined |
|---|---|---|
| Ada Lovelace | Engineering | 2024 |
| Grace Hopper | Engineering | 2025 |
| Katherine Johnson | Research | 2026 |
table-basic.vue
<script setup lang="ts">
import {
Table,
TableBody,
TableCell,
TableColumn,
TableContent,
TableHeader,
TableRow,
TableScrollContainer,
} from "ropav";
const columns = [
{ id: "name", isRowHeader: true, name: "Name" },
{ id: "role", name: "Role" },
{ id: "joined", name: "Joined" },
];
const people = [
{ id: "1", joined: "2024", name: "Ada Lovelace", role: "Engineering" },
{ id: "2", joined: "2025", name: "Grace Hopper", role: "Engineering" },
{ id: "3", joined: "2026", name: "Katherine Johnson", role: "Research" },
];
</script>
<template>
<div class="frame">
<Table>
<TableScrollContainer>
<TableContent aria-label="Team" class="table">
<TableHeader>
<TableColumn
v-for="column in columns"
:id="column.id"
:key="column.id"
:is-row-header="column.isRowHeader"
>
{{ column.name }}
</TableColumn>
</TableHeader>
<TableBody>
<TableRow v-for="person in people" :id="person.id" :key="person.id">
<TableCell v-for="column in columns" :key="column.id">
{{ person[column.id as keyof typeof person] }}
</TableCell>
</TableRow>
</TableBody>
</TableContent>
</TableScrollContainer>
</Table>
</div>
</template>
<style scoped>
.frame {
width: 100%;
max-width: var(--rp-container-2xl);
}
.table {
min-width: 480px;
}
</style>One column should carry is-row-header: it is the cell that names the row, and what a screen reader reads back when the reader moves down a column. Give that cell text-value when its content is a control rather than text, so the row keeps its name while it is being edited.
Selection
selection-mode on TableContent turns it on, and TableSelectionCheckbox is the box to put in the first cell of the header and of each row. The selected keys are row ids, controlled or uncontrolled like any other value here.
Sorting
allows-sorting on a TableColumn makes its header a button, and sort-descriptor on TableContent says which column is sorted and which way. The table does not sort your data — it reports what was asked for and renders what you give back, which is the only version that works once the data is on a server.
Resizing
TableResizableContainer and TableColumnResizer add column resizing; width, min-width and max-width on a column set its bounds.
Long tables
TableLoadMore renders a sentinel at the end of the body that reports when it comes into view. For a table long enough that the rows themselves are the cost, reach for the virtualizer instead.
API
Table
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "primary" | "secondary" | "primary" | Visual variant. |
Slots: default.
TableBody
| Prop | Type | Default | Description |
|---|---|---|---|
itemKey | ((item: T, index: number) => CollectionKey) | — | A row's key. Defaults to the item's own id, then key, then its index. |
items | readonly T[] | — | The rows' data, for a table inside a Virtualizer. Only then: without one every row would be rendered anyway, which is what the plain slot already does. Each row is rendered from the default slot, and has to carry the id its item is keyed by — that key is what pairs the row's cells with the geometry the layout worked out for them. |
itemTextValue | ((item: T) => string | undefined) | — | Text a row is matched on by typeahead. Rendering is what creates cells here, so typeahead cannot read a row that is outside the window — it has no cells yet. Such a row has to be named from the data instead. |
Slots: default, empty, loader.
TableCell
| Prop | Type | Default | Description |
|---|---|---|---|
textValue | string | — | Text this cell contributes to its row's text value. Derived from its content when absent. |
The default slot receives:
| Slot prop | Type |
|---|---|
hasChildRows | boolean |
isDisabled | boolean |
isExpanded | boolean |
isSelected | boolean |
isTreeColumn | boolean |
TableColumn
| Prop | Type | Default | Description |
|---|---|---|---|
allowsSorting | boolean | — | Whether pressing this column header sorts the table. Declared as boolean so Vue casts a bare allows-sorting attribute instead of reading "". |
defaultWidth | FlexSize | "1fr" | Width the column starts at, when the caller is not controlling it. |
id | CollectionKey | — | Identity of the column. Falls back to a generated key. |
isRowHeader | boolean | — | Whether cells under this column name their row. Declared as boolean rather than through the variants type, so Vue casts a bare is-row-header attribute instead of reading "". |
maxWidth | FlexSize | — | Largest width a resize may reach. Cannot be given in fr. |
minWidth | FlexSize | 75 | Smallest width a resize may reach. Cannot be given in fr. |
width | FlexSize | — | Width the caller controls, in pixels, as a percentage of the table, or in fr units. A resize never changes it. Only read inside a TableResizableContainer. |
The default slot receives:
| Slot prop | Type |
|---|---|
allowsSorting | boolean |
isFocusVisible | boolean |
isHovered | boolean |
isPressed | boolean |
isResizing | boolean |
sort | (direction?: TableSortDirection | undefined) => void |
sortDirection | TableSortDirection | undefined |
startResize | () => void |
TableColumnResizer
| Prop | Type | Default | Description |
|---|---|---|---|
ariaLabel | string | "Resize column" | Accessible name of the resizer. |
TableContent
| Prop | Type | Default | Description |
|---|---|---|---|
defaultExpandedKeys | Iterable<CollectionKey> | — | Expanded row keys the table starts with. |
defaultSelectedKeys | "all" | Iterable<CollectionKey> | — | Selected row keys the table starts with. |
disabledBehavior | "all" | "selection" | "all" | Whether a disabled row is also unfocusable. |
disabledKeys | Iterable<CollectionKey> | — | Row keys that cannot be selected, and by default cannot be focused either. |
disallowEmptySelection | boolean | — | Whether the table must always keep a row selected. Declared as boolean so Vue casts a bare attribute instead of reading "". |
dragAndDropHooks | DragAndDropHooks | — | Drag and drop behaviour, from useDragAndDrop. |
expandedKeys | Iterable<CollectionKey> | — | Expanded row keys, for controlled use. |
selectedKeys | "all" | Iterable<CollectionKey> | — | Selected row keys, for controlled use. |
selectionBehavior | "replace" | "toggle" | "toggle" | What a press on a row means when several rows can be selected. |
selectionMode | "none" | "single" | "multiple" | "none" | Whether one, many or no rows can be selected. |
sortDescriptor | TableSortDescriptor | null | — | The column being sorted and the direction it is sorted in. Controlled with no internal fallback, exactly as React Aria has it: pass back what sortChange hands you. |
treeColumn | CollectionKey | — | The column that carries the expand control and the indentation, which is what turns the grid into a tree grid. |
Slots: default.
| Event | Payload |
|---|---|
expandedChange | Set<CollectionKey> |
selectionChange | CollectionSelection |
sortChange | TableSortDescriptor |
update:expandedKeys | Set<CollectionKey> |
update:selectedKeys | CollectionSelection |
update:sortDescriptor | TableSortDescriptor |
TableDragHandle
| Prop | Type | Default | Description |
|---|---|---|---|
ariaLabel | string | — | Accessible name. Defaults to naming the row, or the whole selection when several travel. |
Slots: default.
TableDropIndicator
| Prop | Type | Default | Description |
|---|---|---|---|
target* | DropTarget | — | The position this indicator stands for. |
TableExpandTrigger
Slots: default.
TableFooter
Slots: default.
TableHeader
The default slot receives:
| Slot prop | Type |
|---|---|
allowsDragging | boolean |
TableLoadMore
| Prop | Type | Default | Description |
|---|---|---|---|
isLoading | boolean | — | Whether the next page is on its way, which is when the indicator row is rendered. |
scrollOffset | number | 1 | How far from the end of the scroll box loading starts, as a multiple of the box's own height. 1 is one screen ahead; 0 waits until the very end comes into view. |
Slots: default.
| Event | Payload |
|---|---|
loadMore | — |
TableLoadMoreContent
Slots: default.
TableResizableContainer
Slots: default.
| Event | Payload |
|---|---|
resize | Map<CollectionKey, FlexSize> |
resizeEnd | Map<CollectionKey, FlexSize> |
resizeStart | Map<CollectionKey, FlexSize> |
TableRow
| Prop | Type | Default | Description |
|---|---|---|---|
hasChildRows | boolean | — | Whether the row has children, which is what decides whether it reports expansion at all. Declared as boolean so Vue casts a bare attribute instead of reading "". |
id | CollectionKey | — | Identity of the row. Falls back to a generated key. |
level | number | 0 | Depth in a tree grid, counted from zero. |
parentKey | CollectionKey | — | The row this one hangs off, in a tree grid. |
textValue | string | — | Text the row is matched on by typeahead. Derived from its row header cells when absent. |
The default slot receives:
| Slot prop | Type |
|---|---|
allowsDragging | boolean |
hasChildRows | boolean |
isDisabled | boolean |
isDragging | boolean |
isDropTarget | boolean |
isExpanded | boolean |
isFocused | boolean |
isFocusVisible | boolean |
isHovered | boolean |
isPressed | boolean |
isSelected | boolean |
selectionMode | SelectionMode |
TableScrollContainer
Slots: default.
TableSelectionCheckbox
| Prop | Type | Default | Description |
|---|---|---|---|
ariaLabel | string | — | Accessible name. Defaults to what React Aria names it: Select All in the header of a multiple-selection table, Select for a row or for a single-selection table. |
variant | "primary" | "secondary" | — | Visual variant of the checkbox. |
TableSortableColumnHeader
| Prop | Type | Default | Description |
|---|---|---|---|
showIndicator | boolean | true | Whether to render the sort indicator when a direction is set. |
sortDirection | "ascending" | "descending" | — | Current sort direction for the column. Pass the sortDirection handed to the default slot of TableColumn. |
Slots: default, indicator.