Creating a Cell Plugin

A step-by-step tutorial for building a custom cell plugin in the Notion database.

Introduction

This guide walks you through the process of creating a custom CellPlugin to extend the database system with new property types. A cell plugin controls how values are stored, rendered, and interacted with.

Step-by-Step: Create a "Tag" Plugin

Define the Types

// tag-plugin.tsx

export type TagData = string[];
export interface TagConfig {
  options: string[];
}
export type TagPlugin = CellPlugin<"tag", TagData, TagConfig>;

Implement the Plugin

// tag-plugin.tsx
import { createCompareFn } from "@notion-kit/table-view";

export const tagPlugin: TagPlugin = {
  id: "tag",
  default: {
    name: "Tags",
    icon: <TagIcon />, // Your own icon component
    config: { options: [] },
    data: [],
  },
  meta: {
    name: "Tag",
    desc: "A multi-select tag property",
    icon: <TagIcon />, // Your own icon component
  },
  fromValue: (value, config) => value.split(",").map((tag) => tag.trim()),
  toValue: (data) => data.join(", "),
  toTextValue: (data) => data.join(", "),
  compare: createCompareFn<TagPlugin>((a, b) =>
    (a[0] ?? "").localeCompare(b[0] ?? ""),
  ),
  reducer: (v) => v, // This is deprecated
  renderCell: (props) => <TagCell {...props} />,
  renderConfigMenu: (props) => <TagConfigMenu {...props} />,
};

Render the Cell Component

// tag-plugin.tsx

function TagCell({
  data: tags,
  config,
  onChange,
}: CellProps<TagData, TagConfig>) {
  const options = config?.options ?? [];
  return (
    <div>
      {options.map((tag) => (
        <Badge
          key={tag}
          onClick={() => {
            if (!onChange) return;
            const next = tags.includes(tag)
              ? tags.filter((t) => t !== tag)
              : [...tags, tag];
            onChange(next);
          }}
        >
          {tag} {tags.includes(tag) ? "✅" : ""}
        </Badge>
      ))}
    </div>
  );
}

Optional Configuration Menu

// tag-plugin.tsx

function TagConfigMenu({
  config,
  propId,
  onChange,
}: ConfigMenuProps<TagConfig>) {
  return (
    <div>
      <label htmlFor={`${propId}-options`}>
        Tag Options (comma separated):
      </label>
      <input
        id={`${propId}-options`}
        defaultValue={config?.options?.join(", ") ?? ""}
        onBlur={(e) => {
          const options = e.target.value.split(",").map((o) => o.trim());
          // persist config change using your system
          onChange({ options });
        }}
      />
    </div>
  );
}

Register the Plugin

Finally, register your plugin in the table view:

import { tagPlugin } from "./tag-plugin";

function DatabaseView() {
  return (
    <TableView
      plugins={[...DEFAULT_PLUGINS, tagPlugin]} // Register your custom plugin here
      // other props...
    />
  );
}

This plugin system makes your table highly extensible while maintaining strong typing. You can now build more plugins like number pickers, multi-selects, color swatches, or even embedded widgets.