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.