From zbeyens. The source code is available on GitHub.

Plate
PlatePliteEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Discussion
    • Comments
    • Suggestion
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • List Classic
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Toggle
  • Marks
    • Bold
    • Italic
    • Underline
    • Code
    • Highlight
    • Keyboard Input
    • Strikethrough
    • Subscript
    • Superscript
      • Font
      • Line Height
      • Text Align
    • Indent
    • List
      • Exit Break
      • Single Block
      • Trailing Block
    • Autoformat
    • Block Menu
    • Block Placeholder
    • Combobox
      • Emoji
      • MentionElement
      • Slash Command
    • Cursor Overlay
    • Drag & Drop
    • Navigation Feedback
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Date

PreviousNext

Inline void date elements with one authored string value.

Date ElementPlus

Date adds inline void elements that display a date label inside text. One required value preserves either a canonical YYYY-MM-DD date or authored text such as sometime next week.

Loading…
ColumnEquation

On This Page

FeaturesFast PathAdd The KitRender The ElementAdd An Insert ActionOwnershipManual SetupInstall PackageAdd The PluginAdd Static RenderingInsert A DateValue ShapeDate NormalizationPicker BehaviorMarkdownAPI Reference
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • Inline void date element.
  • date.insert transaction command.
  • One required value for canonical dates or authored date text.
  • Calendar editing in the registry UI.
  • Static renderer for read-only output.
  • Markdown round-trip through <date value="YYYY-MM-DD" /> and child-text date tags.
Report an issue

Fast Path

Add The Kit

DateKit installs DatePlugin with the registry DateElement.

'use client';
 
import {
  formatDateValue,
  getDateDisplayLabel,
  parseCanonicalDateValue,
} from '@platejs/date';
import { DatePlugin } from '@platejs/date/react';
import {
  type PlateElementProps,
  PlateElement,
  useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
 
import { Calendar } from '@/components/ui/calendar';
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { inlineSuggestionVariants } from '@/lib/inline-suggestion';
 
export function DateElement(props: PlateElementProps<typeof DatePlugin>) {
  const { editor, element } = props;
  const readOnly = useEditorReadOnly();
 
  const trigger = (
    <button
      className={cn(
        'w-fit cursor-pointer rounded-sm bg-muted px-1 text-muted-foreground',
        inlineSuggestionVariants()
      )}
      contentEditable={false}
      draggable
      type="button"
    >
      {getDateDisplayLabel(element.value)}
    </button>
  );
 
  return (
    <PlateElement
      {...props}
      className="inline-block"
      attributes={{
        ...props.attributes,
        contentEditable: false,
      }}
    >
      {readOnly ? (
        trigger
      ) : (
        <Popover>
          <PopoverTrigger asChild>{trigger}</PopoverTrigger>
          <PopoverContent className="w-auto p-0">
            <Calendar
              selected={parseCanonicalDateValue(element.value)}
              onSelect={(date) => {
                if (!date) return;
 
                editor.update.nodes.set(
                  { value: formatDateValue(date) },
                  { at: element }
                );
              }}
              mode="single"
              initialFocus
            />
          </PopoverContent>
        </Popover>
      )}
      {props.children}
    </PlateElement>
  );
}
 
export const DateKit = [DatePlugin.configure({ component: DateElement })];
'use client';
 
import {
  formatDateValue,
  getDateDisplayLabel,
  parseCanonicalDateValue,
} from '@platejs/date';
import { DatePlugin } from '@platejs/date/react';
import {
  type PlateElementProps,
  PlateElement,
  useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
 
import { Calendar } from '@/components/ui/calendar';
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover';

























































import { createPlateEditor } from 'platejs/react';
 
import { DateKit } from '@/components/editor/date';
 
export const editor = createPlateEditor({
  plugins: DateKit,
});
import { createPlateEditor } from 'platejs/react';
 
import { DateKit } from '@/components/editor/date';
 
export const editor = createPlateEditor({
  plugins: DateKit,
});

Render The Element

date owns the inline wrapper, display label, popover, calendar picker, and static element.

'use client';
 
import {
  formatDateValue,
  getDateDisplayLabel,
  parseCanonicalDateValue,
} from '@platejs/date';
import { DatePlugin } from '@platejs/date/react';
import {
  type PlateElementProps,
  PlateElement,
  useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
 
import { Calendar } from '@/components/ui/calendar';
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from

























































Add An Insert Action

The registry insert toolbar maps PLUGINS.date to the plugin-owned update.

components/editor/transforms.ts
import { BaseDatePlugin } from '@platejs/date';
import { PLUGINS } from 'platejs';
 
export const insertInlineMap = {
  [PLUGINS.date]: (editor) =>
    editor.plugin(BaseDatePlugin).update.insert({}, { select: true }),
};
components/editor/transforms.ts
import { BaseDatePlugin } from '@platejs/date';
import




Ownership

LayerOwnerWhat It Does
@platejs/datePackageExports BaseDatePlugin and date value helpers.
@platejs/date/reactPackageExports DatePlugin.
dateRegistryAdds DatePlugin.configure({ component: DateElement }).
date-staticRegistryAdds BaseDatePlugin.configure({ component: DateElementStatic }).
dateRegistry UIRenders the editable popover/calendar element and static element.
@platejs/markdownPackageConverts date MDX tags to the package-owned value.

BaseDatePlugin is inline and void. The text child exists only to satisfy Plite's element shape.

Manual Setup

Install Package

pnpm add @platejs/date
pnpm add @platejs/date

Add The Plugin

Use the React plugin when the editor renders the calendar popover.

import { DatePlugin } from '@platejs/date/react';
import { createPlateEditor } from 'platejs/react';
 
import { DateElement } from '@/components/editor/date';
 
export const editor = createPlateEditor({
  plugins: [DatePlugin.configure({ component: DateElement })],
});
import { DatePlugin } from '@platejs/date/react';
import { createPlateEditor } from 'platejs/react';
 
import { DateElement } from '@/components/editor/date';
 
export const editor = createPlateEditor({
  plugins: [DatePlugin.configure({ component: DateElement })],
});

Add Static Rendering

Use the base kit when rendering read-only output with platejs/static.

import { BaseDatePlugin, getDateDisplayLabel } from '@platejs/date';
import { type PliteElementProps, PliteElement } from 'platejs/static';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
import { inlineSuggestionVariants } from '@/lib/inline-suggestion';
 
export function DateElementStatic(
  props: PliteElementProps<typeof BaseDatePlugin>
) {
  const { element } = props;
 
  return (
    <PliteElement as="span"















Insert A Date

DatePlugin exposes date insertion through the date transaction group.

editor.update((tx) =>
  tx.date.insert({ value: '2026-03-23' }, { select: true })
);
editor.update((tx) =>
  tx.date.insert({ value: '2026-03-23' }, { select: true })
);

Value Shape

Date elements store one non-empty authored string. Parsing is derived from that value rather than persisted as a second field.

const value = [
  {
    children: [
      { text: 'Due ' },
      {
        children: [{ text: '' }],
        type: 'date',
        value: '2026-03-23',
      },
      { text: '.' },
    ],
    type: 'paragraph',
  },
];
const value = [
  {
    children: [
      { text: 'Due ' },
      {
        children: [{ text: '' }],
        type: 'date',
        value: '2026-03-23',
      },
      { text: '.' },
    ],
    type: 'paragraph',
  },
];
FieldTypeNotes
type'date'Persisted element type owned by BaseDatePlugin.
children[{ text: '' }]Required Plite child for the inline void element.
valuestringRequired canonical date or authored date text.

Date Normalization

normalizeDateValue returns the one persisted string.

InputStored Value
Date objectformatDateValue(value) when the object is valid.
YYYY-MM-DDThe trimmed string.
Invalid canonical stringThe trimmed authored string.
Mon Mar 23 2026'2026-03-23' when JavaScript can parse it.
Blank stringundefined; no Date node is constructed.
Other textThe trimmed authored string.

getDateDisplayLabel(value) returns Today, Yesterday, Tomorrow, a localized long date, or the authored string.

Picker Behavior

The registry element is display-only while read-only. In editable mode, clicking the inline label opens a calendar popover.

StateBehavior
canonical valueThe trigger shows a relative or localized date label.
other valueThe trigger shows the authored string.
calendar selectionThe node is set to { value: formatDateValue(date) }.

The registry element uses contentEditable={false} on the inline wrapper, so users edit the date through the calendar instead of typing inside the void node.

Markdown

Canonical values serialize as a self-closing date tag with a value attribute.

Date: <date value="2026-03-23" />
Date: <date value="2026-03-23" />

Other authored values serialize as child text.

Date: <date>sometime next week</date>
Date: <date>sometime next week</date>

The deserializer also accepts child text such as <date>Mon Mar 23 2026</date> and normalizes it to value: '2026-03-23' when the date is safe to parse.

API Reference

APIPackageUse
BaseDatePlugin@platejs/dateHeadless inline void date plugin.
DatePlugin@platejs/date/reactReact date plugin.
editor.plugin(BaseDatePlugin).update.insert(input?, options?)BaseDatePlugin updateInserts a date with separate date input and node placement options.
normalizeDateValue(value)@platejs/dateReturns the canonical or authored string, or undefined for empty/invalid input.
formatDateValue(date)@platejs/dateFormats a Date object as YYYY-MM-DD.
parseCanonicalDateValue(value)@platejs/dateParses only valid canonical date strings.
getDateDisplayLabel(value, options?)@platejs/dateBuilds the visible date label.
DateElement@platejs/dateElement shape with one required value.
import { cn } from '@/lib/utils';
import { inlineSuggestionVariants } from '@/lib/inline-suggestion';
export function DateElement(props: PlateElementProps<typeof DatePlugin>) {
const { editor, element } = props;
const readOnly = useEditorReadOnly();
const trigger = (
<button
className={cn(
'w-fit cursor-pointer rounded-sm bg-muted px-1 text-muted-foreground',
inlineSuggestionVariants()
)}
contentEditable={false}
draggable
type="button"
>
{getDateDisplayLabel(element.value)}
</button>
);
return (
<PlateElement
{...props}
className="inline-block"
attributes={{
...props.attributes,
contentEditable: false,
}}
>
{readOnly ? (
trigger
) : (
<Popover>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
selected={parseCanonicalDateValue(element.value)}
onSelect={(date) => {
if (!date) return;
editor.update.nodes.set(
{ value: formatDateValue(date) },
{ at: element }
);
}}
mode="single"
initialFocus
/>
</PopoverContent>
</Popover>
)}
{props.children}
</PlateElement>
);
}
export const DateKit = [DatePlugin.configure({ component: DateElement })];
'@/components/ui/popover'
;
import { cn } from '@/lib/utils';
import { inlineSuggestionVariants } from '@/lib/inline-suggestion';
export function DateElement(props: PlateElementProps<typeof DatePlugin>) {
const { editor, element } = props;
const readOnly = useEditorReadOnly();
const trigger = (
<button
className={cn(
'w-fit cursor-pointer rounded-sm bg-muted px-1 text-muted-foreground',
inlineSuggestionVariants()
)}
contentEditable={false}
draggable
type="button"
>
{getDateDisplayLabel(element.value)}
</button>
);
return (
<PlateElement
{...props}
className="inline-block"
attributes={{
...props.attributes,
contentEditable: false,
}}
>
{readOnly ? (
trigger
) : (
<Popover>
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
selected={parseCanonicalDateValue(element.value)}
onSelect={(date) => {
if (!date) return;
editor.update.nodes.set(
{ value: formatDateValue(date) },
{ at: element }
);
}}
mode="single"
initialFocus
/>
</PopoverContent>
</Popover>
)}
{props.children}
</PlateElement>
);
}
export const DateKit = [DatePlugin.configure({ component: DateElement })];
'use client';
 
import {
  formatDateValue,
  getDateDisplayLabel,
  parseCanonicalDateValue,
} from '@platejs/date';
import { DatePlugin } from '@platejs/date/react';
import {
  type PlateElementProps,
  PlateElement,
  useEditorReadOnly,
} from 'platejs/react';
import * as React from 'react';
 
import { Calendar } from '@/components/ui/calendar';
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { inlineSuggestionVariants } from '@/lib/inline-suggestion';
 
export function DateElement(props: PlateElementProps<typeof DatePlugin>) {
  const { editor, element } = props;
  const readOnly = useEditorReadOnly();
 
  const trigger = (
    <button
      className={cn(
        'w-fit cursor-pointer rounded-sm bg-muted px-1 text-muted-foreground',
        inlineSuggestionVariants()
      )}
      contentEditable={false}
      draggable
      type="button"
    >
      {getDateDisplayLabel(element.value)}
    </button>
  );
 
  return (
    <PlateElement
      {...props}
      className="inline-block"
      attributes={{
        ...props.attributes,
        contentEditable: false,
      }}
    >
      {readOnly ? (
        trigger
      ) : (
        <Popover>
          <PopoverTrigger asChild>{trigger}</PopoverTrigger>
          <PopoverContent className="w-auto p-0">
            <Calendar
              selected={parseCanonicalDateValue(element.value)}
              onSelect={(date) => {
                if (!date) return;
 
                editor.update.nodes.set(
                  { value: formatDateValue(date) },
                  { at: element }
                );
              }}
              mode="single"
              initialFocus
            />
          </PopoverContent>
        </Popover>
      )}
      {props.children}
    </PlateElement>
  );
}
 
export const DateKit = [DatePlugin.configure({ component: DateElement })];
{ PLUGINS }
from
'platejs'
;
export const insertInlineMap = {
[PLUGINS.date]: (editor) =>
editor.plugin(BaseDatePlugin).update.insert({}, { select: true }),
};
className
=
"inline-block"
{
...
props}>
<span
className={cn(
'w-fit rounded-sm bg-muted px-1 text-muted-foreground',
inlineSuggestionVariants()
)}
>
{getDateDisplayLabel(element.value)}
</span>
{props.children}
</PliteElement>
);
}
export const BaseDateKit = [
BaseDatePlugin.configure({ component: DateElementStatic }),
];
import { BaseDatePlugin, getDateDisplayLabel } from '@platejs/date';
import { type PliteElementProps, PliteElement } from 'platejs/static';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
import { inlineSuggestionVariants } from '@/lib/inline-suggestion';
 
export function DateElementStatic(
  props: PliteElementProps<typeof BaseDatePlugin>
) {
  const { element } = props;
 
  return (
    <PliteElement as="span" className="inline-block" {...props}>
      <span
        className={cn(
          'w-fit rounded-sm bg-muted px-1 text-muted-foreground',
          inlineSuggestionVariants()
        )}
      >
        {getDateDisplayLabel(element.value)}
      </span>
      {props.children}
    </PliteElement>
  );
}
 
export const BaseDateKit = [
  BaseDatePlugin.configure({ component: DateElementStatic }),
];