Mark Anatomy
Marks are created using thecreateYooptaMark function:
import { createYooptaMark } from '@yoopta/editor';
import type { YooptaMarkProps } from '@yoopta/editor';
type MyMarkProps = YooptaMarkProps<'myMark', MyMarkValue>;
const MyMark = createYooptaMark<MyMarkProps>({
type: 'myMark', // Unique identifier
hotkey: 'mod+m', // Optional keyboard shortcut
render: (props) => { // Render function
return <span className="my-mark">{props.children}</span>;
},
});
Simple Mark Example
Let’s create a basic bold mark:import { createYooptaMark, YooptaMarkProps } from '@yoopta/editor';
type BoldMarkProps = YooptaMarkProps<'bold', boolean>;
export const Bold = createYooptaMark<BoldMarkProps>({
type: 'bold',
hotkey: 'mod+b',
render: (props) => (
<strong className="yoopta-mark-bold">
{props.children}
</strong>
),
});
Mark with Values
Create marks with custom values like colors:type HighlightValue = {
color: string;
backgroundColor: string;
};
type HighlightMarkProps = YooptaMarkProps<'highlight', HighlightValue>;
export const Highlight = createYooptaMark<HighlightMarkProps>({
type: 'highlight',
render: (props) => {
const { leaf } = props;
const value = leaf.highlight;
if (!value) return <>{props.children}</>;
return (
<span
className="yoopta-mark-highlight"
style={{
color: value.color,
backgroundColor: value.backgroundColor,
}}
>
{props.children}
</span>
);
},
});
Mark Props Type
TheYooptaMarkProps type provides the structure for mark render props:
type YooptaMarkProps<K extends string, V> = {
children: React.ReactNode;
leaf: ExtendedLeaf<K, V>;
};
type ExtendedLeaf<K extends string, V> = {
text: string;
// The mark's value
[key in K]: V;
// Other marks may be present
bold?: boolean;
italic?: boolean;
// etc...
};
Keyboard Shortcuts
Marks can have keyboard shortcuts:const Bold = createYooptaMark({
type: 'bold',
hotkey: 'mod+b', // Cmd+B on Mac, Ctrl+B on Windows
render: (props) => <strong>{props.children}</strong>,
});
const Italic = createYooptaMark({
type: 'italic',
hotkey: 'mod+i', // Cmd+I on Mac, Ctrl+I on Windows
render: (props) => <em>{props.children}</em>,
});
const Underline = createYooptaMark({
type: 'underline',
hotkey: 'mod+u', // Cmd+U on Mac, Ctrl+U on Windows
render: (props) => <u>{props.children}</u>,
});
mod key automatically maps to:
Cmdon macOSCtrlon Windows/Linux
Using Marks
Add marks to your editor:import { createYooptaEditor } from '@yoopta/editor';
import { Bold, Italic, Underline, Highlight } from './marks';
const editor = createYooptaEditor({
plugins: PLUGINS,
marks: [Bold, Italic, Underline, Highlight],
});
Marks API
Use theMarks namespace to interact with marks:
import { Marks } from '@yoopta/editor';
// Toggle a boolean mark
Marks.toggle(editor, { type: 'bold' });
Marks.toggle(editor, { type: 'italic' });
// Check if mark is active
const isBold = Marks.isActive(editor, { type: 'bold' });
// Add a mark with value
Marks.add(editor, {
type: 'highlight',
value: {
color: '#000000',
backgroundColor: '#FFFF00',
},
});
// Update mark value
Marks.update(editor, {
type: 'highlight',
value: {
color: '#FFFFFF',
backgroundColor: '#FF0000',
},
});
// Remove a mark
Marks.remove(editor, { type: 'highlight' });
// Remove all marks
Marks.clear(editor, {});
Built-in Marks
Yoopta provides common marks in the@yoopta/marks package:
import {
Bold,
Italic,
Underline,
Strike,
CodeMark,
Highlight,
} from '@yoopta/marks';
const MARKS = [
Bold, // mod+b
Italic, // mod+i
Underline, // mod+u
Strike, // mod+shift+s
CodeMark, // mod+e
Highlight, // Custom color highlighting
];
Complex Mark Examples
Text Color Mark
type TextColorValue = string; // hex color
type TextColorMarkProps = YooptaMarkProps<'textColor', TextColorValue>;
export const TextColor = createYooptaMark<TextColorMarkProps>({
type: 'textColor',
render: (props) => {
const color = props.leaf.textColor;
if (!color) return <>{props.children}</>;
return (
<span style={{ color }}>
{props.children}
</span>
);
},
});
Font Size Mark
type FontSizeValue = number; // in pixels
type FontSizeMarkProps = YooptaMarkProps<'fontSize', FontSizeValue>;
export const FontSize = createYooptaMark<FontSizeMarkProps>({
type: 'fontSize',
render: (props) => {
const size = props.leaf.fontSize;
if (!size) return <>{props.children}</>;
return (
<span style={{ fontSize: `${size}px` }}>
{props.children}
</span>
);
},
});
Custom Class Mark
type ClassNameValue = string;
type ClassNameMarkProps = YooptaMarkProps<'className', ClassNameValue>;
export const ClassName = createYooptaMark<ClassNameMarkProps>({
type: 'className',
render: (props) => {
const className = props.leaf.className;
if (!className) return <>{props.children}</>;
return (
<span className={className}>
{props.children}
</span>
);
},
});
Tooltip Mark
type TooltipValue = string; // tooltip text
type TooltipMarkProps = YooptaMarkProps<'tooltip', TooltipValue>;
export const Tooltip = createYooptaMark<TooltipMarkProps>({
type: 'tooltip',
render: (props) => {
const tooltip = props.leaf.tooltip;
if (!tooltip) return <>{props.children}</>;
return (
<abbr title={tooltip} className="tooltip">
{props.children}
</abbr>
);
},
});
Creating a Toolbar for Marks
import { Marks } from '@yoopta/editor';
import { useYooptaEditor } from '@yoopta/editor';
function MarkToolbar() {
const editor = useYooptaEditor();
const toggleBold = () => Marks.toggle(editor, { type: 'bold' });
const toggleItalic = () => Marks.toggle(editor, { type: 'italic' });
const toggleUnderline = () => Marks.toggle(editor, { type: 'underline' });
const isBold = Marks.isActive(editor, { type: 'bold' });
const isItalic = Marks.isActive(editor, { type: 'italic' });
const isUnderline = Marks.isActive(editor, { type: 'underline' });
return (
<div className="mark-toolbar">
<button
onClick={toggleBold}
className={isBold ? 'active' : ''}
>
Bold
</button>
<button
onClick={toggleItalic}
className={isItalic ? 'active' : ''}
>
Italic
</button>
<button
onClick={toggleUnderline}
className={isUnderline ? 'active' : ''}
>
Underline
</button>
</div>
);
}
Color Picker Example
import { Marks } from '@yoopta/editor';
import { useState } from 'react';
function ColorPicker() {
const editor = useYooptaEditor();
const [color, setColor] = useState('#000000');
const [bgColor, setBgColor] = useState('#FFFF00');
const applyHighlight = () => {
Marks.add(editor, {
type: 'highlight',
value: {
color,
backgroundColor: bgColor,
},
});
};
const removeHighlight = () => {
Marks.remove(editor, { type: 'highlight' });
};
return (
<div>
<input
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
/>
<input
type="color"
value={bgColor}
onChange={(e) => setBgColor(e.target.value)}
/>
<button onClick={applyHighlight}>Apply</button>
<button onClick={removeHighlight}>Remove</button>
</div>
);
}
Mark Serialization
Marks are automatically serialized by the editor’s parser:// Text with marks
const text = [
{ text: 'Normal ' },
{ text: 'bold', bold: true },
{ text: ' and ' },
{ text: 'italic', italic: true },
];
// HTML output
// "Normal <strong>bold</strong> and <em>italic</em>"
// Markdown output
// "Normal **bold** and _italic_"
Custom Serialization
For custom marks, you’ll need to handle serialization in text node serializers:function serializeCustomTextNodes(children: any[]) {
return children.map(child => {
if (typeof child === 'string') return child;
let text = child.text;
if (child.bold) text = `<strong>${text}</strong>`;
if (child.italic) text = `<em>${text}</em>`;
if (child.highlight) {
const { color, backgroundColor } = child.highlight;
text = `<span style="color: ${color}; background-color: ${backgroundColor}">${text}</span>`;
}
return text;
}).join('');
}
Best Practices
Keep marks lightweight
Keep marks lightweight
Marks are applied to individual characters, so keep rendering logic simple:
// Good
render: (props) => <strong>{props.children}</strong>
// Avoid heavy computations or side effects
render: (props) => {
// Don't do this
const expensiveValue = computeExpensiveValue();
return <strong>{props.children}</strong>;
}
Use semantic HTML
Use semantic HTML
Use semantic elements for better accessibility:
// Good
<strong>{children}</strong> // Bold
<em>{children}</em> // Italic
<mark>{children}</mark> // Highlight
// Avoid
<span style="font-weight: bold">{children}</span>
Type your mark values
Type your mark values
Always provide TypeScript types:
type HighlightValue = {
color: string;
backgroundColor: string;
};
type HighlightMarkProps = YooptaMarkProps<'highlight', HighlightValue>;
Handle null values
Handle null values
Always check if the mark value exists:
render: (props) => {
const value = props.leaf.myMark;
if (!value) return <>{props.children}</>;
// Use value
}
Testing Custom Marks
import { describe, it, expect } from 'vitest';
import { Marks } from '@yoopta/editor';
import { createYooptaEditor } from '@yoopta/editor';
import MyMark from './my-mark';
describe('MyMark', () => {
it('should toggle mark', () => {
const editor = createYooptaEditor({
plugins: [],
marks: [MyMark],
});
Marks.toggle(editor, { type: 'myMark' });
const isActive = Marks.isActive(editor, { type: 'myMark' });
expect(isActive).toBe(true);
});
it('should add mark with value', () => {
const editor = createYooptaEditor({
plugins: [],
marks: [MyMark],
});
Marks.add(editor, {
type: 'myMark',
value: { color: '#FF0000' },
});
// Test that mark was applied
});
});