Plugins are the fundamental building blocks of Yoopta Editor. Everything in the editor - from simple paragraphs to complex accordions - is implemented as a plugin. This architecture provides maximum flexibility and extensibility.
What is a Plugin?
A plugin defines:
Elements : The structure and visual components
Options : Display information and shortcuts
Events : DOM event handlers (keyboard, mouse, etc.)
Lifecycle : Hooks for creation and destruction
Parsers : HTML/Markdown serialization and deserialization
Commands : Custom operations
Extensions : Slate editor customization
packages/core/editor/src/plugins/types.ts
type Plugin < TElementMap , TPluginOptions > = {
type : string ;
elements : PluginElementsMap ;
options ?: PluginOptions < TPluginOptions >;
events ?: PluginDOMEvents ;
lifecycle ?: PluginLifeCycleEvents ;
parsers ?: Partial < Record < PluginParserTypes , PluginParsers >>;
commands ?: Record < string , ( editor : YooEditor , ... args : any []) => any >;
extensions ?: ( slate : SlateEditor , editor : YooEditor , blockId : string ) => SlateEditor ;
};
Creating a Plugin
Use the YooptaPlugin class to create plugins:
import { YooptaPlugin } from '@yoopta/editor' ;
const MyPlugin = new YooptaPlugin ({
type: 'MyPlugin' ,
elements: {
'my-element' : {
render : ( props ) => < div { ... props . attributes }>{props. children } </ div > ,
props: { nodeType: 'block' },
}
},
options: {
display: {
title: 'My Plugin' ,
description: 'A custom plugin' ,
},
shortcuts: [ 'my' , 'custom' ],
},
});
Plugin type names should use PascalCase (“Paragraph”, “HeadingOne”) while element type names within plugins use kebab-case (“paragraph”, “heading-one”).
Plugin Elements
Elements define the structure and rendering of your plugin’s content:
packages/core/editor/src/plugins/types.ts
type PluginElement < TKeys , T > = {
render ?: ( props : PluginElementRenderProps ) => JSX . Element ;
props ?: PluginElementProps < T >;
asRoot ?: boolean ; // Is this the root element?
children ?: TKeys []; // Allowed child element types
injectElementsFromPlugins ?: string []; // Allow elements from other plugins
rootPlugin ?: string ; // Plugin this element belongs to
placeholder ?: string ; // Placeholder text when empty
};
Simple Element
const Paragraph = new YooptaPlugin ({
type: 'Paragraph' ,
elements: {
paragraph: {
render : ( props ) => < p { ... props . attributes }>{props. children } </ p > ,
props: { nodeType: 'block' },
},
},
});
Element with Custom Props
type CalloutProps = {
nodeType : 'block' ;
theme : 'info' | 'warning' | 'error' | 'success' ;
};
const Callout = new YooptaPlugin <{ callout : CalloutProps }>({
type: 'Callout' ,
elements: {
callout: {
render : ( props ) => {
const { theme } = props . element . props || {};
return (
< div
className = { `callout callout- ${ theme } ` }
{ ... props . attributes }
>
{ props . children }
</ div >
);
},
props: {
nodeType: 'block' ,
theme: 'info' // default value
},
},
},
});
Nested Elements
Plugins can have multiple element types with hierarchical structure:
const Accordion = new YooptaPlugin ({
type: 'Accordion' ,
elements: {
'accordion-list' : {
render : ( props ) => < div { ... props . attributes }>{props. children } </ div > ,
props: { nodeType: 'block' },
asRoot: true ,
children: [ 'accordion-list-item' ],
},
'accordion-list-item' : {
render : ( props ) => (
< details { ... props . attributes } >
{ props . children }
</ details >
),
props: { nodeType: 'block' , isExpanded: false },
children: [ 'accordion-list-item-heading' , 'accordion-list-item-content' ],
},
'accordion-list-item-heading' : {
render : ( props ) => < summary { ... props . attributes }>{props. children } </ summary > ,
props: { nodeType: 'block' },
},
'accordion-list-item-content' : {
render : ( props ) => < div { ... props . attributes }>{props. children } </ div > ,
props: { nodeType: 'block' },
// Allow nesting other plugins
injectElementsFromPlugins: [ 'Paragraph' , 'HeadingOne' , 'HeadingTwo' ],
},
},
});
Use injectElementsFromPlugins to allow elements from other plugins to be nested within your element. This is powerful for container-like plugins such as accordions, tabs, or callouts.
Void Elements
Void elements don’t contain editable content:
const Divider = new YooptaPlugin ({
type: 'Divider' ,
elements: {
divider: {
render : ( props ) => (
< div { ... props . attributes } contentEditable = { false } >
< hr />
{ props . children } { /* Required empty text node */ }
</ div >
),
props: { nodeType: 'void' },
},
},
});
Inline Elements
Inline elements flow within text:
const Link = new YooptaPlugin ({
type: 'Link' ,
elements: {
link: {
render : ( props ) => (
< a
href = {props.element.props. url }
target = {props.element.props. target }
{ ... props . attributes }
>
{ props . children }
</ a >
),
props: {
nodeType: 'inline' ,
url: '' ,
target: '_blank'
},
},
},
});
Plugin Options
Options provide metadata and configuration:
packages/core/editor/src/plugins/types.ts
type PluginOptions < T > = Partial <{
display ?: {
title ?: string ; // Display name
description ?: string ; // Description
icon ?: ReactNode ; // Icon component
};
shortcuts ?: string []; // Slash command shortcuts
HTMLAttributes ?: HTMLAttributes < HTMLElement >;
} & T >;
const HeadingOne = new YooptaPlugin ({
type: 'HeadingOne' ,
elements: { /* ... */ },
options: {
display: {
title: 'Heading 1' ,
description: 'Large section heading' ,
icon: < Heading1Icon />,
},
shortcuts: [ 'h1' , 'heading1' , '#' ],
},
});
These options are used by UI components like slash command menus.
Plugin Events
Handle DOM events at the plugin level:
packages/core/editor/src/plugins/types.ts
type PluginDOMEvents = {
[ key in keyof EditorEventHandlers ] : (
editor : YooEditor ,
slate : SlateEditor ,
options : PluginEventHandlerOptions ,
) => EditorEventHandlers [ key ] | void ;
};
Keyboard Events
const MyPlugin = new YooptaPlugin ({
type: 'MyPlugin' ,
elements: { /* ... */ },
events: {
onKeyDown : ( editor , slate , options ) => ( event ) => {
const { hotkeys , currentBlock } = options ;
// Handle Enter key
if ( event . key === 'Enter' && ! event . shiftKey ) {
event . preventDefault ();
editor . splitBlock ();
return ;
}
// Handle backspace at start
if ( event . key === 'Backspace' ) {
const { selection } = slate ;
if ( selection && Range . isCollapsed ( selection )) {
const [ start ] = Range . edges ( selection );
if ( start . offset === 0 && start . path [ 0 ] === 0 ) {
event . preventDefault ();
editor . toggleBlock ( currentBlock . id , { type: 'Paragraph' });
return ;
}
}
}
},
},
});
Available Event Handlers
onKeyDown - Keyboard press
onKeyUp - Keyboard release
onClick - Mouse click
onMouseDown - Mouse button press
onMouseUp - Mouse button release
onFocus - Element receives focus
onBlur - Element loses focus
onPaste - Paste event
onCopy - Copy event
onCut - Cut event
Plugin Lifecycle
Hook into block creation and destruction:
packages/core/editor/src/plugins/types.ts
type PluginLifeCycleEvents = {
beforeCreate ?: ( editor : YooEditor ) => SlateElement ;
onCreate ?: ( editor : YooEditor , blockId : string ) => void ;
onDestroy ?: ( editor : YooEditor , blockId : string ) => void ;
};
const MyPlugin = new YooptaPlugin ({
type: 'MyPlugin' ,
elements: { /* ... */ },
lifecycle: {
beforeCreate : ( editor ) => {
// Return custom initial structure
return editor . y ( 'my-element' , {
props: { customProp: 'initial value' },
children: [ editor . y . text ( 'Default content' )]
});
},
onCreate : ( editor , blockId ) => {
console . log ( 'Block created:' , blockId );
// Initialize external resources
},
onDestroy : ( editor , blockId ) => {
console . log ( 'Block destroyed:' , blockId );
// Cleanup external resources
},
},
});
Plugin Parsers
Handle serialization and deserialization:
packages/core/editor/src/plugins/types.ts
type PluginParsers = {
deserialize ?: PluginDeserializeParser ;
serialize ?: PluginSerializeParser ;
};
type PluginDeserializeParser = {
nodeNames : string []; // HTML tag names to parse
parse ?: ( el : HTMLElement , editor : YooEditor ) => SlateElement | YooptaBlockData [] | void ;
};
type PluginSerializeParser = (
element : SlateElement ,
content : string ,
blockMetaData ?: YooptaBlockBaseMeta ,
) => string ;
HTML Parser
packages/plugins/paragraph/src/plugin/paragraph-plugin.tsx
const Paragraph = new YooptaPlugin ({
type: 'Paragraph' ,
elements: { /* ... */ },
parsers: {
html: {
deserialize: {
nodeNames: [ 'P' ], // Parse <p> tags
},
serialize : ( element , text , blockMeta ) => {
const { align = 'left' , depth = 0 } = blockMeta || {};
return `<p
data-meta-align=" ${ align } "
data-meta-depth=" ${ depth } "
style="margin-left: ${ depth * 20 } px; text-align: ${ align } "
> ${ serializeTextNodes ( element . children ) } </p>` ;
},
},
},
});
Markdown Parser
packages/plugins/paragraph/src/plugin/paragraph-plugin.tsx
const Paragraph = new YooptaPlugin ({
type: 'Paragraph' ,
elements: { /* ... */ },
parsers: {
markdown: {
serialize : ( element ) =>
` ${ serializeTextNodesIntoMarkdown ( element . children ) } \n ` ,
},
},
});
Email Parser
const Paragraph = new YooptaPlugin ({
type: 'Paragraph' ,
elements: { /* ... */ },
parsers: {
email: {
serialize : ( element , text , blockMeta ) => {
const { align = 'left' , depth = 0 } = blockMeta || {};
return `<table style="width: 100%">
<tbody>
<tr>
<td>
<p style="font-size: 16px; line-height: 1.75rem; margin: .5rem 0 0">
${ serializeTextNodes ( element . children ) }
</p>
</td>
</tr>
</tbody>
</table>` ;
},
},
},
});
The serializeTextNodes and serializeTextNodesIntoMarkdown helper functions handle text formatting (bold, italic, etc.) automatically.
Plugin Commands
Add custom methods to the editor:
const Paragraph = new YooptaPlugin ({
type: 'Paragraph' ,
elements: { /* ... */ },
commands: {
convertToParagraph : ( editor , blockId : string ) => {
editor . toggleBlock ( blockId , { type: 'Paragraph' });
},
insertParagraphAfter : ( editor , blockId : string ) => {
const block = editor . getBlock ({ id: blockId });
if ( ! block ) return ;
editor . insertBlock ( 'Paragraph' , {
at: block . meta . order + 1 ,
focus: true ,
});
},
},
});
// Use commands
editor . plugins . Paragraph . commands . convertToParagraph ( editor , blockId );
Plugin Extensions
Customize the Slate editor for your plugin:
const withMyPlugin = ( slate : SlateEditor , editor : YooEditor , blockId : string ) => {
const { insertText , insertBreak , normalizeNode } = slate ;
// Override insertText
slate . insertText = ( text ) => {
// Custom text insertion logic
insertText ( text );
};
// Override insertBreak
slate . insertBreak = () => {
// Custom break behavior
editor . splitBlock ();
};
// Override normalizeNode
slate . normalizeNode = ( entry ) => {
// Custom normalization
normalizeNode ( entry );
};
return slate ;
};
const MyPlugin = new YooptaPlugin ({
type: 'MyPlugin' ,
elements: { /* ... */ },
extensions: withMyPlugin ,
});
Extensions modify Slate editor behavior at a low level. Use them carefully and ensure you don’t break core functionality.
Extending Plugins
You can extend existing plugins to customize them:
packages/core/editor/src/plugins/create-yoopta-plugin.tsx
const CustomParagraph = Paragraph . extend ({
options: {
display: {
title: 'Custom Paragraph' ,
description: 'A customized paragraph' ,
},
},
elements: {
paragraph: {
render : ( props ) => (
< p className = "custom-paragraph" { ... props . attributes } >
{ props . children }
</ p >
),
},
},
});
Extend Options
type ExtendPlugin < TElementMap , TOptions > = {
options ?: Partial < PluginOptions < TOptions >>;
events ?: Partial < PluginDOMEvents >;
lifecycle ?: Partial < PluginLifeCycleEvents >;
injectElementsFromPlugins ?: YooptaPlugin < any , any >[];
elements ?: {
[ K in keyof TElementMap ] ?: {
render ?: ( props : PluginElementRenderProps ) => JSX . Element ;
props ?: Record < string , unknown >;
injectElementsFromPlugins ?: YooptaPlugin < any , any >[];
placeholder ?: string ;
};
};
};
Injecting Elements
Allow other plugin elements to be nested:
import { Paragraph , HeadingOne , HeadingTwo , Image } from '@yoopta/plugins' ;
const CustomAccordion = Accordion . extend ({
injectElementsFromPlugins: [
Paragraph ,
HeadingOne ,
HeadingTwo ,
Image ,
],
});
Now accordions can contain paragraphs, headings, and images.
Using Plugins
Register plugins when creating the editor:
import { createYooptaEditor } from '@yoopta/editor' ;
import { Paragraph , HeadingOne , HeadingTwo } from '@yoopta/plugins' ;
const PLUGINS = [ Paragraph , HeadingOne , HeadingTwo ];
const editor = useMemo (() => createYooptaEditor ({
plugins: PLUGINS ,
marks: MARKS ,
}), []);
Real-World Example
Here’s a complete plugin example:
import { YooptaPlugin , serializeTextNodes } from '@yoopta/editor' ;
type QuoteElementMap = {
quote : {
type : 'quote' ;
props : {
nodeType : 'block' ;
author ?: string ;
source ?: string ;
};
};
};
const Quote = new YooptaPlugin < QuoteElementMap >({
type: 'Quote' ,
elements: {
quote: {
render : ( props ) => {
const { author , source } = props . element . props || {};
return (
< blockquote { ... props . attributes } >
< div className = "quote-content" > {props. children } </ div >
{( author || source ) && (
< footer className = "quote-footer" >
{ author && < cite >{ author }</ cite >}
{ source && < span > from { source }</ span >}
</ footer >
)}
</ blockquote >
);
},
props: {
nodeType: 'block' ,
},
placeholder: 'Enter quote...' ,
},
},
options: {
display: {
title: 'Quote' ,
description: 'Insert a quotation' ,
icon: < QuoteIcon />,
},
shortcuts: [ 'quote' , 'blockquote' , '>' ],
},
parsers: {
html: {
deserialize: {
nodeNames: [ 'BLOCKQUOTE' ],
},
serialize : ( element , text ) => {
const { author , source } = element . props || {};
let html = `<blockquote> ${ serializeTextNodes ( element . children ) } ` ;
if ( author || source ) {
html += '<footer>' ;
if ( author ) html += `<cite> ${ author } </cite>` ;
if ( source ) html += ` from ${ source } ` ;
html += '</footer>' ;
}
html += '</blockquote>' ;
return html ;
},
},
markdown: {
serialize : ( element ) => `> ${ serializeTextNodesIntoMarkdown ( element . children ) } \n ` ,
},
},
commands: {
setAuthor : ( editor , blockId : string , author : string ) => {
const block = editor . getBlock ({ id: blockId });
if ( ! block ) return ;
editor . updateElement ({
blockId ,
elementId: block . value [ 0 ]. id ,
props: { author },
});
},
},
});
export { Quote };
Best Practices
1. Type Your Plugin
Define proper TypeScript types for your element map:
type MyElementMap = {
'my-element' : {
type : 'my-element' ;
props : {
nodeType : 'block' ;
customProp : string ;
};
};
};
const MyPlugin = new YooptaPlugin < MyElementMap >({ /* ... */ });
2. Use Semantic HTML
Render with appropriate HTML elements:
// Good
render : ( props ) => < h1 { ... props . attributes }>{props. children } </ h1 >
// Bad
render : ( props ) => < div className = "heading" { ... props . attributes }>{props. children } </ div >
3. Handle Edge Cases
Always handle empty content, missing props, etc.:
render : ( props ) => {
const { url , alt } = props . element . props || {};
if ( ! url ) return < div { ... props . attributes }>{props. children } </ div > ;
return (
< img src = { url } alt = {alt || '' } { ... props . attributes } />
);
}
4. Provide Shortcuts
Make plugins discoverable with intuitive shortcuts:
options : {
shortcuts : [ 'h1' , 'heading1' , '#' ] // Multiple aliases
}
5. Add Placeholders
Guide users with helpful placeholder text:
elements : {
'my-element' : {
placeholder: 'Type something here...' ,
}
}
Next Steps
Themes Style your plugins with themes
Elements Understand element structures
Events Handle editor events
Built-in Plugins Explore available plugins