Table View

A composable, editable data table with multiple layouts.

Installation

pnpm add @notion-kit/table-view

Features

  • 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 APICurrent API
view.timelineview.dateView
TimelineViewStateDateViewState
setTimelineRangesetDateViewRange
setTimelineDatePropertysetDateViewDateProperty
view.timeline_range.changeview.date_view_range.change
view.timeline_property.changeview.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

PropTypeDescription
pluginsTablePluginPairPaired data/UI plugin registration. Defaults to DEFAULT_PLUGINS.
dataRow[]Controlled rows resource.
defaultDataRow[]Initial rows for an uncontrolled table.
propertiesColumnDefsControlled properties resource.
defaultPropertiesColumnDefsInitial properties for an uncontrolled table.
viewPartialTableViewStateControlled layout and presentation state.
defaultViewPartialTableViewStateInitial view state for an uncontrolled table.
onDataChange(change) => voidReceives the next rows resource and its action.
onPropertiesChange(change) => voidReceives the next properties resource and its action.
onViewChange(change) => voidReceives the next view resource and its action.
fetchTableEditLogsFetchTableEditLogsOptional paginated table history callback. Enables the table Edit log entry.
fetchRowEditLogsFetchRowEditLogsOptional paginated row history callback. Enables row Edit log entries.
getRowUrl(rowId: string) => stringReturns the URL used when opening a row in a new tab.
weekStartsOnWeekdaySets the first day of the week for date-based features.
defaultColumnPartial<ColumnDef>Overrides the default TanStack Table column options.
childrenReactNodeContent rendered inside the Table View provider.

TableViewState

FieldDescription
layoutThe active layout. Table, List, Board, Timeline, and Calendar have dedicated renderers; other layout values currently use the table renderer.
lockedPrevents table edits while leaving existing rows navigable.
rowViewWhere an opened row appears: center, side, or full.
openedRowIdThe currently opened row, or null.
dateViewShared Calendar and Timeline configuration containing range and datePropertyId.
pluginMethodsPersisted sorting, grouping, and group-sort choices for plugins.
filtersPersisted 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.

FieldTypeDefaultDescription
range"daily" | "weekly" | "monthly" | "quarterly""monthly"Calendar supports daily, weekly, and monthly. Timeline supports daily, monthly, and quarterly. An unsupported range becomes monthly.
datePropertyIdstring | nullnullID 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.