Table View
A composable, editable data table with multiple layouts.
Installation
pnpm add @notion-kit/table-viewFeatures
- Controlled or uncontrolled resources for rows, properties, and view state
- Create, edit, duplicate, reorder, hide, and resize properties and rows
- Search rows, build nested AND/OR filters, sort, group, calculate, and bulk-edit selected rows
- Table, List, Board, Timeline, and Calendar layouts
- Open rows in a center dialog, side pane, or full page
- Built-in plugins for title, text, number, checkbox, select, multi-select, links, and dates
- Optional table and row edit logs with historical values and manual pagination
Requirements
TableView uses DEFAULT_PLUGINS when no plugins prop is supplied. When you
provide plugins, include the plugin factories your properties use. Every table
needs exactly one title property so rows have a label in every layout.
Examples
Uncontrolled Table
Use defaultData, defaultProperties, and defaultView when the table owns
its state. These defaults are read once when the component mounts; remount with
a different key to reset the table.
Controlled Table
Pass a resource and apply each callback's next value to keep it authoritative.
Rows, properties, and view state can be controlled independently.
Do not switch a resource between controlled and uncontrolled during one mount.
Edit logs
Pass fetchTableEditLogs to show Edit log in View Settings. Pass
fetchRowEditLogs to show it in a row's action menu. Each callback enables its
own entry, including in accessible menus of locked tables. Both props also
work with TableViewWrapper when you compose your own content.
The examples above show static sample history. To use the same mock API:
import { useState } from "react";
import {
TableView,
type ColumnDefs,
type DefaultPlugins,
type Row,
} from "@notion-kit/table-view";
import { createMockEditLogApi } from "@notion-kit/table-view/mock";
function Example({
data,
properties,
}: {
data: Row<DefaultPlugins>[];
properties: ColumnDefs<DefaultPlugins>;
}) {
const [editLogs] = useState(() => createMockEditLogApi({ data, properties }));
return (
<TableView
defaultData={data}
defaultProperties={properties}
{...editLogs}
/>
);
}The factory copies the initial fixtures and defaults to 10 records per page.
It returns stable callbacks, supports AbortSignal, and returns no history for
new or unknown rows. Editing a demo never adds log entries.
For a backend, provide callbacks with these exported types:
import type {
EditLogPage,
FetchRowEditLogs,
FetchTableEditLogs,
RowEditLog,
TableEditLog,
} from "@notion-kit/table-view";
const tablePage: EditLogPage<TableEditLog> = {
items: [
{
id: "log-1",
editedAt: Date.UTC(2026, 8, 17, 9),
action: "update-config",
target: { id: "score", name: "Score" },
property: { id: "score", name: "Score", type: "number" },
},
],
nextCursor: null,
};
const rowPage: EditLogPage<RowEditLog> = {
items: [
{
id: "log-2",
editedAt: Date.UTC(2026, 8, 17, 10),
rowId: "row-1",
property: { id: "notes", name: "Notes", type: "text" },
value: "Ready for review",
textValue: "Ready for review",
},
],
nextCursor: null,
};
const fetchTableEditLogs: FetchTableEditLogs = async ({ cursor, signal }) => {
signal.throwIfAborted();
return cursor ? { items: [], nextCursor: null } : tablePage;
};
const fetchRowEditLogs: FetchRowEditLogs = async ({
rowId,
cursor,
signal,
}) => {
signal.throwIfAborted();
return rowId === "row-1" && !cursor
? rowPage
: { items: [], nextCursor: null };
};Replace these sample responses with your endpoint adapter and pass signal to
its fetch request. Your application owns log storage, authorization, and
historical snapshots. The viewer does not record edits, restore values, or
provide undo.
Requests run only when the user opens history, clicks Load more, or retries
a failed request. cursor is omitted for the first page. Return records newest
first, with stable IDs and ordering, and set nextCursor to null at the end.
Keep pagination boundaries stable when new records arrive. Scrolling does not
fetch another page.
Closing history, switching rows, removing a callback, or unmounting cancels the request and discards its session. Reopening fetches the first page. Changing a callback reference alone does not refetch; the next action uses the latest callback. A failed additional page keeps the visible records and retries the same cursor. Repeated IDs are ignored, while a repeated cursor or invalid page shows an error.
Table records can include a historical property snapshot for field edits.
The viewer uses its icon, or the default icon for its type. Omit property
for table-level actions such as creating a property or changing the layout
to display the action icon. Both table and row history display entries on
one line, with horizontal scrolling for long content.
Table-view generates messages from the record's action and snapshots.
TableEditLogAction defines the supported actions. Unknown actions and records
missing required snapshots fail page validation. Callers store structured
records without composing display messages.
For cell edits in table history, use action: "update" and include
cell: { property, value, textValue }. Other actions omit cell.
The viewer uses the same read-only renderer as row history. The cell's property
snapshot supplies the leading icon. Set target.name to the historical row name.
The viewer combines the property and row names, such as Released · TODO 1.
The lock, unlock, filter, sort, and group actions use their corresponding
icons. For change-layout, include layout to display the destination view's
icon. The generated messages are Locked database, Unlocked database,
Changed to Board view, Updated filters, and Updated sorting rules.
For group, include groupBy with the historical property snapshot. The viewer
displays Grouped by followed by its icon and name. Omit groupBy when grouping
is removed. change-type requires a property snapshot and displays
Changed to <type> type. update-config displays Updated property settings,
while rename uses the new historical name in target.name.
Row records carry the historical property name, optional IconData, type,
config, value, and textValue. Include historical option metadata for select
values and formatting settings for numbers and dates. Created-time and
last-edited-time values contain resolved Unix timestamps in milliseconds.
Current columns and row metadata never replace these snapshots. Unknown types,
invalid values, missing renderers, and renderer errors display textValue.
See read-only history renderers
for custom plugins.
Timeline layout
Set the layout to "timeline" and select a Date property for the timeline.
The range can be daily, monthly, or quarterly.
const view: PartialTableViewState = {
layout: "timeline",
dateView: {
range: "monthly",
datePropertyId: "due-date",
},
};
<TableView
defaultProperties={properties}
defaultData={data}
defaultView={view}
/>;Calendar layout
Calendar displays rows with valid dates in Month, Week, or Day. Click an event to open its row. Click an empty date or time to create a row and its initial date in one data update. Drag events to move them, resize their true edges, or move between the all-day area and time grid.
Set dateView.datePropertyId to the ID of a property whose type is "date".
The calendar uses that property's config.tz as the display timezone and
the table's weekStartsOn setting for week boundaries. The title property
supplies event labels.
Calendar respects row search, filters, and sorting. Rows without a valid start date are omitted from the calendar but remain in the data resource. The example includes an unscheduled task that appears in Table layout.
Date values
Store dates in the selected property's value using DateData from
@notion-kit/table-hook/plugins:
import type { DateData } from "@notion-kit/table-hook/plugins";
const allDay: DateData = {
start: Date.UTC(2026, 8, 16),
end: Date.UTC(2026, 8, 18),
endDate: true,
includeTime: false,
};
const timed: DateData = {
start: Date.UTC(2026, 8, 16, 10),
end: Date.UTC(2026, 8, 16, 11),
endDate: true,
includeTime: true,
};With config.tz: "UTC", the all-day value covers September 16 through
September 18, including the end date. The timed value covers 10:00–11:00 UTC.
Set endDate: false to store a date without an explicit end. Calendar displays
one day for an all-day value or one hour for a timed value in that case.
The table adapter converts the inclusive all-day end to the exclusive
endAt used by the standalone Calendar block.
Event gestures write the result back as DateData.
Shared date settings
Calendar and Timeline share dateView.datePropertyId and dateView.range.
Switching to Calendar replaces quarterly with monthly; switching to
Timeline replaces weekly with monthly. Supported ranges stay selected.
The viewed date survives layout changes within a mounted table, including a
visit to Table or Board. Scrolling does not call onViewChange.
When the selected property is missing, Calendar selects an existing Date
property or creates one with empty values. If you control properties,
data, or view, accept the corresponding resource callbacks so
initialization and event edits can complete. Timeline initializes a newly
created Date property from each row's creation and last-edited timestamps.
Set view.locked to prevent creation, date edits, and range changes while
keeping Previous, Today, Next, and row opening available. Locked tables do
not create a missing Date property.
For a calendar whose events are independent of table rows, use the Calendar block.
Migrate Timeline settings
| Previous API | Current API |
|---|---|
view.timeline | view.dateView |
TimelineViewState | DateViewState |
setTimelineRange | setDateViewRange |
setTimelineDateProperty | setDateViewDateProperty |
view.timeline_range.change | view.date_view_range.change |
view.timeline_property.change | view.date_view_property.change |
Migrate persisted settings and controlled resources to the current names.
The standalone @notion-kit/ui/timeline exports remain available.
addRow({ initialValues: { [propertyId]: value } }) returns the proposed row
ID. A controlled owner must accept the data before the row can be opened.
Custom cell plugins
Create a custom data plugin when a property needs its own storage or table
semantics, then register a matching UI adapter for display, editing, or
configuration. An adapter that provides renderBulkEditor participates in
bulk editing.
API Reference
TableView
| Prop | Type | Description |
|---|---|---|
plugins | TablePluginPair | Paired data/UI plugin registration. Defaults to DEFAULT_PLUGINS. |
data | Row[] | Controlled rows resource. |
defaultData | Row[] | Initial rows for an uncontrolled table. |
properties | ColumnDefs | Controlled properties resource. |
defaultProperties | ColumnDefs | Initial properties for an uncontrolled table. |
view | PartialTableViewState | Controlled layout and presentation state. |
defaultView | PartialTableViewState | Initial view state for an uncontrolled table. |
onDataChange | (change) => void | Receives the next rows resource and its action. |
onPropertiesChange | (change) => void | Receives the next properties resource and its action. |
onViewChange | (change) => void | Receives the next view resource and its action. |
fetchTableEditLogs | FetchTableEditLogs | Optional paginated table history callback. Enables the table Edit log entry. |
fetchRowEditLogs | FetchRowEditLogs | Optional paginated row history callback. Enables row Edit log entries. |
getRowUrl | (rowId: string) => string | Returns the URL used when opening a row in a new tab. |
weekStartsOn | Weekday | Sets the first day of the week for date-based features. |
defaultColumn | Partial<ColumnDef> | Overrides the default TanStack Table column options. |
children | ReactNode | Content rendered inside the Table View provider. |
TableViewState
| Field | Description |
|---|---|
layout | The active layout. Table, List, Board, Timeline, and Calendar have dedicated renderers; other layout values currently use the table renderer. |
locked | Prevents table edits while leaving existing rows navigable. |
rowView | Where an opened row appears: center, side, or full. |
openedRowId | The currently opened row, or null. |
dateView | Shared Calendar and Timeline configuration containing range and datePropertyId. |
pluginMethods | Persisted sorting, grouping, and group-sort choices for plugins. |
filters | Persisted advanced filter tree (TableFilterState); use null to clear all filters. |
DateViewState
Shared by Calendar and Timeline through view.dateView or
defaultView.dateView. Both fields can be omitted in PartialTableViewState.
| Field | Type | Default | Description |
|---|---|---|---|
range | "daily" | "weekly" | "monthly" | "quarterly" | "monthly" | Calendar supports daily, weekly, and monthly. Timeline supports daily, monthly, and quarterly. An unsupported range becomes monthly. |
datePropertyId | string | null | null | ID of the Date property used for event values. Resolves to an existing or newly created Date property when editable. |
The viewed date is navigation state within the mounted table. It is not a
field in DateViewState and is not persisted by onViewChange.