Marks are inline formatting attributes applied to text selections. Unlike nodes, marks don’t represent standalone content but modify the appearance or behavior of text.
The marks menu is triggered by selecting text and pressing /:
Select text
Highlight the text you want to format
Open marks menu
Press / with text selected
Apply mark
Choose a mark from the menu to apply it
~/workspace/source/packages/webeditor/src/editor/index.tsx:296-308
// Check if we have a text selection - show marks menu
if ( ! empty && hasTextSelection ( state )) {
const coords = view . coordsAtPos ( state . selection . from );
const { top , left } = clampDialogPosition ( coords . bottom + 5 , coords . left );
setMarksDialogPosition ({ top , left });
setCurrentMenuType ( "marks" );
openMarksDialog ();
return true ;
}
The same / key opens either the command menu (empty line) or marks menu (text selected), providing a consistent interface for both.
Available marks
WebEditor supports five text marks:
Bold
Italic
Strikethrough
Inline code
Makes text bold using the strong mark. Markdown syntax: **bold text**{
id : "bold" ,
name : "Bold" ,
description : "Make text bold" ,
icon : Bold ,
execute : ( state , dispatch ) => {
const { from , to } = state . selection ;
if ( from === to ) return false ;
if ( dispatch ) {
const mark = state . schema . marks . strong . create ();
const tr = state . tr . addMark ( from , to , mark ). removeStoredMark ( mark );
dispatch ( tr );
}
return true ;
},
}
Makes text italic using the em mark. Markdown syntax: _italic text_{
id : "italic" ,
name : "Italic" ,
description : "Make text italic" ,
icon : Italic ,
execute : ( state , dispatch ) => {
const { from , to } = state . selection ;
if ( from === to ) return false ;
if ( dispatch ) {
const mark = state . schema . marks . em . create ();
const tr = state . tr . addMark ( from , to , mark ). removeStoredMark ( mark );
dispatch ( tr );
}
return true ;
},
}
Strikes through text using the strikethrough mark. Markdown syntax: ~~strikethrough text~~{
id : "strikethrough" ,
name : "Strikethrough" ,
description : "Strike through text" ,
icon : Strikethrough ,
execute : ( state , dispatch ) => {
const { from , to } = state . selection ;
if ( from === to ) return false ;
if ( dispatch ) {
const mark = state . schema . marks . strikethrough . create ();
const tr = state . tr . addMark ( from , to , mark ). removeStoredMark ( mark );
dispatch ( tr );
}
return true ;
},
}
Formats text as inline code using the code mark. Markdown syntax: `inline code`{
id : "code" ,
name : "Inline Code" ,
description : "Format as inline code" ,
icon : Code ,
execute : ( state , dispatch ) => {
const { from , to } = state . selection ;
if ( from === to ) return false ;
if ( dispatch ) {
const mark = state . schema . marks . code . create ();
const tr = state . tr . addMark ( from , to , mark ). removeStoredMark ( mark );
dispatch ( tr );
}
return true ;
},
}
The tooltip mark adds hover tooltips to text:
{
id : "tooltip" ,
name : "Tooltip" ,
description : "Add a tooltip to the selected text" ,
icon : Info ,
type : "tooltip" ,
requiresAttributes : true ,
}
Usage:
Select text
Press / to open marks menu
Select “Tooltip”
Enter tooltip text in the modal
Tooltips can be edited or removed by clicking on text with a tooltip already applied.
Mark schema definitions
Marks are defined in the ProseMirror schema:
Strikethrough mark
~/workspace/source/packages/webeditor/src/editor/schema.ts:38-54
strikethrough : {
inclusive : false ,
parseDOM : [
{ tag: "s" },
{ tag: "del" },
{ tag: "strike" },
{
style: "text-decoration" ,
getAttrs : ( value : string | Node ) => {
return typeof value === "string" && value . includes ( "line-through" ) ? {} : false ;
},
},
],
toDOM : () => [ "s" , 0 ],
}
~/workspace/source/packages/webeditor/src/editor/schema.ts:56-79
tooltip_mark : {
attrs : {
tooltip : { default : "" },
},
inclusive : false ,
parseDOM : [
{
tag: "span[data-tooltip]" ,
getAttrs : ( dom ) => ({
tooltip: ( dom as HTMLElement ). getAttribute ( "data-tooltip" ) || "" ,
}),
},
],
toDOM : ( mark ) => [
"span" ,
{
"data-tooltip" : mark . attrs . tooltip ,
class:
"underline decoration-dotted decoration-muted-foreground cursor-help hover:decoration-foreground transition-colors relative group" ,
},
0 ,
],
}
Mark commands
Mark commands come in two types:
Simple mark commands Directly apply or toggle a mark on the selection interface SimpleMarkCommand {
id : string ;
name : string ;
description : string ;
icon : typeof Bold ;
execute : ( state , dispatch ? ) => boolean ;
}
Tooltip mark command Requires additional input before applying interface TooltipMarkCommand {
id : string ;
name : string ;
description : string ;
icon : typeof Info ;
type : "tooltip" ;
requiresAttributes : true ;
}
Applying marks programmatically
You can apply marks using ProseMirror transactions:
Simple mark application
const { from , to } = state . selection ;
const mark = state . schema . marks . strong . create ();
const tr = state . tr . addMark ( from , to , mark );
dispatch ( tr );
~/workspace/source/packages/webeditor/src/editor/utils/marks-system.tsx:165-178
export function executeTooltipMark (
state : EditorState ,
dispatch : ( tr : Transaction ) => void ,
tooltipText : string ,
) : boolean {
const { from , to } = state . selection ;
if ( from === to ) return false ; // No selection
const mark = state . schema . marks . tooltip_mark . create ({ tooltip: tooltipText });
const tr = state . tr . addMark ( from , to , mark ). removeStoredMark ( mark );
dispatch ( tr );
return true ;
}
Tooltips have special click handling for editing:
export function createTooltipClickPlugin ({
onTooltipClick ,
} : {
onTooltipClick : ( tooltipText : string , from : number , to : number ) => void ;
}) {
return new Plugin ({
props: {
handleClick ( view , pos , event ) {
const target = event . target as HTMLElement ;
// Check if clicked element has a tooltip
if ( target . hasAttribute ( 'data-tooltip' )) {
const tooltipText = target . getAttribute ( 'data-tooltip' ) || '' ;
// Find the range of the tooltip mark
const $pos = view . state . doc . resolve ( pos );
const mark = $pos . marks (). find ( m => m . type . name === 'tooltip_mark' );
if ( mark ) {
// Calculate the full range of the mark
let from = pos ;
let to = pos ;
onTooltipClick ( tooltipText , from , to );
return true ;
}
}
return false ;
},
},
});
}
Clicking on text with a tooltip opens the tooltip editor modal, allowing you to modify or remove the tooltip.
All mark commands are defined in marksMenuSetup:
~/workspace/source/packages/webeditor/src/editor/utils/marks-system.tsx:33-111
export const marksMenuSetup : MarkCommandItem [] = [
{
id: "bold" ,
name: "Bold" ,
description: "Make text bold" ,
icon: Bold ,
execute : ( state , dispatch ) => {
const { from , to } = state . selection ;
if ( from === to ) return false ;
if ( dispatch ) {
const mark = state . schema . marks . strong . create ();
const tr = state . tr . addMark ( from , to , mark ). removeStoredMark ( mark );
dispatch ( tr );
}
return true ;
},
},
{
id: "italic" ,
name: "Italic" ,
description: "Make text italic" ,
icon: Italic ,
execute : ( state , dispatch ) => {
const { from , to } = state . selection ;
if ( from === to ) return false ;
if ( dispatch ) {
const mark = state . schema . marks . em . create ();
const tr = state . tr . addMark ( from , to , mark ). removeStoredMark ( mark );
dispatch ( tr );
}
return true ;
},
},
{
id: "strikethrough" ,
name: "Strikethrough" ,
description: "Strike through text" ,
icon: Strikethrough ,
execute : ( state , dispatch ) => {
const { from , to } = state . selection ;
if ( from === to ) return false ;
if ( dispatch ) {
const mark = state . schema . marks . strikethrough . create ();
const tr = state . tr . addMark ( from , to , mark ). removeStoredMark ( mark );
dispatch ( tr );
}
return true ;
},
},
{
id: "code" ,
name: "Inline Code" ,
description: "Format as inline code" ,
icon: Code ,
execute : ( state , dispatch ) => {
const { from , to } = state . selection ;
if ( from === to ) return false ;
if ( dispatch ) {
const mark = state . schema . marks . code . create ();
const tr = state . tr . addMark ( from , to , mark ). removeStoredMark ( mark );
dispatch ( tr );
}
return true ;
},
},
{ separator: true },
{
id: "tooltip" ,
name: "Tooltip" ,
description: "Add a tooltip to the selected text" ,
icon: Info ,
type: "tooltip" ,
requiresAttributes: true ,
},
];
Helper functions
The marks system provides utility functions:
// Check if text is selected
export function hasTextSelection ( state : EditorState ) : boolean {
return ! state . selection . empty ;
}
// Check if command is a simple mark toggle
export function isSimpleMarkCommand ( command : MarkCommand ) : command is SimpleMarkCommand {
return "execute" in command ;
}
// Check if command requires attributes (tooltip)
export function isTooltipMarkCommand ( command : MarkCommand ) : command is TooltipMarkCommand {
return "requiresAttributes" in command && command . requiresAttributes === true ;
}
Next steps
Theme system Learn about light, dark, and auto theme modes
Components Explore the component system and command menu